From 1cd3d237a8258a589a52b175e1a522f85b4c1b2f Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 07:52:09 -0400 Subject: [PATCH 01/79] Initial wip using builder pattern --- build.sbt | 2 +- .../scala/fs2/io/net/NetworkPlatform.scala | 25 +++++-- ...etwork.scala => DatagramSocketGroup.scala} | 10 +-- .../src/main/scala/fs2/io/net/Network.scala | 67 +++++++++++++++++++ 4 files changed, 88 insertions(+), 16 deletions(-) rename io/native/src/main/scala/fs2/io/net/{Network.scala => DatagramSocketGroup.scala} (78%) rename io/{js-jvm => shared}/src/main/scala/fs2/io/net/Network.scala (52%) diff --git a/build.sbt b/build.sbt index 9bf8f8cc0b..515ce696a0 100644 --- a/build.sbt +++ b/build.sbt @@ -353,7 +353,7 @@ lazy val io = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "fs2-io", tlVersionIntroduced ~= { _.updated("3", "3.1.0") }, - libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.6.0", + libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.6.0-91-51bd018-SNAPSHOT", tlJdkRelease := None ) .jvmSettings( diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 4c258c4021..9640180306 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -28,7 +28,7 @@ import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} import fs2.internal.ThreadFactories import fs2.io.net.tls.TLSContext @@ -80,7 +80,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = - new UnsealedNetwork[F] { + new AsyncNetwork[F] { private lazy val fallback = forAsync[F] private def tryGetSelector = @@ -132,15 +132,13 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N protocolFamily: Option[ProtocolFamily] ): Resource[F, DatagramSocket[F]] = fallback.openDatagramSocket(address, port, options, protocolFamily) - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] } def forAsync[F[_]](implicit F: Async[F]): Network[F] = forAsyncAndDns(F, Dns.forAsync(F)) def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = - new UnsealedNetwork[F] { + new AsyncNetwork[F] { private lazy val globalSocketGroup = SocketGroup.unsafe[F](globalAcg) private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) @@ -185,8 +183,23 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N protocolFamily: Option[ProtocolFamily] ): Resource[F, DatagramSocket[F]] = globalDatagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) + } + + + private abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends UnsealedNetwork[F] { - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + + private def unixSockets = fs2.io.net.unixsocket.UnixSockets.forAsync[F] + + def tcp: TcpBuilder.NeedAddress[F] = new TcpBuilder.UnsealedNeedAddress[F] { + def mkClient(address: GenSocketAddress, options: List[SocketOption]) = + address match { + case sa: SocketAddress[_] => client(sa, options) + case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) + case _ => ??? + } } + } } diff --git a/io/native/src/main/scala/fs2/io/net/Network.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala similarity index 78% rename from io/native/src/main/scala/fs2/io/net/Network.scala rename to io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala index 4733608bf0..6db41d8fe0 100644 --- a/io/native/src/main/scala/fs2/io/net/Network.scala +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala @@ -23,14 +23,6 @@ package fs2 package io package net -import fs2.io.net.tls.TLSContext - -sealed trait Network[F[_]] extends NetworkPlatform[F] with SocketGroup[F] { - def tlsContext: TLSContext.Builder[F] +trait DatagramSocketGroup[F[_]] { } -object Network extends NetworkCompanionPlatform { - private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] - - def apply[F[_]](implicit F: Network[F]): F.type = F -} diff --git a/io/js-jvm/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala similarity index 52% rename from io/js-jvm/src/main/scala/fs2/io/net/Network.scala rename to io/shared/src/main/scala/fs2/io/net/Network.scala index f50c99fb48..7bba5602b7 100644 --- a/io/js-jvm/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -51,6 +51,8 @@ sealed trait Network[F[_]] with SocketGroup[F] with DatagramSocketGroup[F] { + def tcp: TcpBuilder.NeedAddress[F] + /** Returns a builder for `TLSContext[F]` values. * * For example, `Network[IO].tlsContext.system` returns a `F[TLSContext[F]]`. @@ -63,3 +65,68 @@ object Network extends NetworkCompanionPlatform { def apply[F[_]](implicit F: Network[F]): F.type = F } + +import cats.effect.Resource +import com.comcast.ip4s.* + +sealed trait BoundServer[F[_]] { + def serverSocket: Socket[F] + def clients: Stream[F, Socket[F]] +} +private[net] trait UnsealedBoundServer[F[_]] extends BoundServer[F] + + +sealed trait TcpBuilder[F[_]] { + def withAddress(address: GenSocketAddress): TcpBuilder[F] + def withSocketOptions(options: List[SocketOption]): TcpBuilder[F] + def connect: Resource[F, Socket[F]] + def bind: Resource[F, BoundServer[F]] + def bindAndServe: Stream[F, Socket[F]] +} + +object TcpBuilder { + sealed trait NeedAddress[F[_]] { + protected def mkClient(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] + + def address(address: GenSocketAddress): TcpBuilder[F] = + TcpBuilder[F](mkClient, address, Nil) + + def hostAndPort(host: Host, port: Port): TcpBuilder[F] = + address(SocketAddress(host, port)) + + def port(port: Port): TcpBuilder[F] = + hostAndPort(Ipv4Address.Wildcard, port) + + def port(p: Int): TcpBuilder[F] = + Port.fromInt(p) match { + case Some(pp) => port(pp) + case None => ??? // TODO + } + + def hostAndEphemeralPort(host: Host): TcpBuilder[F] = + hostAndPort(host, Port.Wildcard) + + def ephemeralPort: TcpBuilder[F] = + address(SocketAddress.Wildcard) + + def unixSocket(path: fs2.io.file.Path): TcpBuilder[F] = + address(UnixSocketAddress(path.toString)) + } + + private[net] trait UnsealedNeedAddress[F[_]] extends NeedAddress[F] + + private[net] def apply[F[_]]( + mkClient: (GenSocketAddress, List[SocketOption]) => Resource[F, Socket[F]], + address: GenSocketAddress, + options: List[SocketOption] + ): TcpBuilder[F] = new TcpBuilder[F] { + private def copy(address: GenSocketAddress = address, options: List[SocketOption] = options): TcpBuilder[F] = + apply[F](mkClient, address, options) + def withSocketOptions(options: List[SocketOption]) = copy(options = options) + def withAddress(address: GenSocketAddress) = copy(address = address) + def connect: Resource[F, Socket[F]] = mkClient(address, options) + def bind: Resource[F, BoundServer[F]]= ??? + def bindAndServe: Stream[F, Socket[F]] = ??? //Stream.resource(bind).flatMap(_.clients) + } + +} From 5e5b39e02e44d805c86c18675c4b925eac84cdd3 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 08:35:05 -0400 Subject: [PATCH 02/79] Generalize NeedAddress --- .../scala/fs2/io/net/NetworkPlatform.scala | 19 ++++---- .../src/main/scala/fs2/io/net/Network.scala | 46 +++++++++---------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 9640180306..6fad95938f 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -192,14 +192,17 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private def unixSockets = fs2.io.net.unixsocket.UnixSockets.forAsync[F] - def tcp: TcpBuilder.NeedAddress[F] = new TcpBuilder.UnsealedNeedAddress[F] { - def mkClient(address: GenSocketAddress, options: List[SocketOption]) = - address match { - case sa: SocketAddress[_] => client(sa, options) - case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) - case _ => ??? - } - } + def tcp: NeedAddress[F, TcpBuilder[F]] = new UnsealedNeedAddress[F, TcpBuilder[F]] { + def address(address: GenSocketAddress): TcpBuilder[F] = { + def mkClient(address: GenSocketAddress, options: List[SocketOption]) = + address match { + case sa: SocketAddress[_] => client(sa, options) + case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) + case _ => ??? + } + TcpBuilder[F](mkClient, address, Nil) + } + } } } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 7bba5602b7..b7bf20a722 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -51,7 +51,7 @@ sealed trait Network[F[_]] with SocketGroup[F] with DatagramSocketGroup[F] { - def tcp: TcpBuilder.NeedAddress[F] + def tcp: NeedAddress[F, TcpBuilder[F]] /** Returns a builder for `TLSContext[F]` values. * @@ -84,37 +84,35 @@ sealed trait TcpBuilder[F[_]] { def bindAndServe: Stream[F, Socket[F]] } -object TcpBuilder { - sealed trait NeedAddress[F[_]] { - protected def mkClient(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] - - def address(address: GenSocketAddress): TcpBuilder[F] = - TcpBuilder[F](mkClient, address, Nil) +sealed trait NeedAddress[F[_], Builder] { - def hostAndPort(host: Host, port: Port): TcpBuilder[F] = - address(SocketAddress(host, port)) + def address(address: GenSocketAddress): Builder - def port(port: Port): TcpBuilder[F] = - hostAndPort(Ipv4Address.Wildcard, port) + def hostAndPort(host: Host, port: Port): Builder = + address(SocketAddress(host, port)) - def port(p: Int): TcpBuilder[F] = - Port.fromInt(p) match { - case Some(pp) => port(pp) - case None => ??? // TODO - } + def port(port: Port): Builder = + hostAndPort(Ipv4Address.Wildcard, port) - def hostAndEphemeralPort(host: Host): TcpBuilder[F] = - hostAndPort(host, Port.Wildcard) + def port(p: Int): Builder = + Port.fromInt(p) match { + case Some(pp) => port(pp) + case None => ??? // TODO + } - def ephemeralPort: TcpBuilder[F] = - address(SocketAddress.Wildcard) + def hostAndEphemeralPort(host: Host): Builder = + hostAndPort(host, Port.Wildcard) - def unixSocket(path: fs2.io.file.Path): TcpBuilder[F] = - address(UnixSocketAddress(path.toString)) - } + def ephemeralPort: Builder = + address(SocketAddress.Wildcard) - private[net] trait UnsealedNeedAddress[F[_]] extends NeedAddress[F] + def unixSocket(path: fs2.io.file.Path): Builder = + address(UnixSocketAddress(path.toString)) +} +private[net] trait UnsealedNeedAddress[F[_], Builder] extends NeedAddress[F, Builder] + +object TcpBuilder { private[net] def apply[F[_]]( mkClient: (GenSocketAddress, List[SocketOption]) => Resource[F, Socket[F]], address: GenSocketAddress, From 5e27b71417047afe5fe730e2406acfd1800385da Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 08:50:45 -0400 Subject: [PATCH 03/79] Dump builder pattern --- .../scala/fs2/io/net/NetworkPlatform.scala | 20 +++--- .../src/main/scala/fs2/io/net/Network.scala | 62 ++----------------- 2 files changed, 15 insertions(+), 67 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 6fad95938f..ec6dd0e9cb 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -192,17 +192,17 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private def unixSockets = fs2.io.net.unixsocket.UnixSockets.forAsync[F] - def tcp: NeedAddress[F, TcpBuilder[F]] = new UnsealedNeedAddress[F, TcpBuilder[F]] { - def address(address: GenSocketAddress): TcpBuilder[F] = { - def mkClient(address: GenSocketAddress, options: List[SocketOption]) = - address match { - case sa: SocketAddress[_] => client(sa, options) - case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) - case _ => ??? - } - TcpBuilder[F](mkClient, address, Nil) + def connect(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = { + address match { + case sa: SocketAddress[_] => client(sa, options) + case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) + case _ => ??? } - } + } + + def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, BoundServer[F]] = ??? + + def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = ??? } } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index b7bf20a722..d0717e97bb 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -23,6 +23,8 @@ package fs2 package io package net +import cats.effect.Resource +import com.comcast.ip4s.GenSocketAddress import fs2.io.net.tls.TLSContext /** Provides the ability to work with TCP, UDP, and TLS. @@ -51,7 +53,9 @@ sealed trait Network[F[_]] with SocketGroup[F] with DatagramSocketGroup[F] { - def tcp: NeedAddress[F, TcpBuilder[F]] + def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] + def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, BoundServer[F]] + def bindAndAccept(address: GenSocketAddress, options: List[SocketOption] = Nil): Stream[F, Socket[F]] /** Returns a builder for `TLSContext[F]` values. * @@ -66,65 +70,9 @@ object Network extends NetworkCompanionPlatform { def apply[F[_]](implicit F: Network[F]): F.type = F } -import cats.effect.Resource -import com.comcast.ip4s.* - sealed trait BoundServer[F[_]] { def serverSocket: Socket[F] def clients: Stream[F, Socket[F]] } private[net] trait UnsealedBoundServer[F[_]] extends BoundServer[F] - -sealed trait TcpBuilder[F[_]] { - def withAddress(address: GenSocketAddress): TcpBuilder[F] - def withSocketOptions(options: List[SocketOption]): TcpBuilder[F] - def connect: Resource[F, Socket[F]] - def bind: Resource[F, BoundServer[F]] - def bindAndServe: Stream[F, Socket[F]] -} - -sealed trait NeedAddress[F[_], Builder] { - - def address(address: GenSocketAddress): Builder - - def hostAndPort(host: Host, port: Port): Builder = - address(SocketAddress(host, port)) - - def port(port: Port): Builder = - hostAndPort(Ipv4Address.Wildcard, port) - - def port(p: Int): Builder = - Port.fromInt(p) match { - case Some(pp) => port(pp) - case None => ??? // TODO - } - - def hostAndEphemeralPort(host: Host): Builder = - hostAndPort(host, Port.Wildcard) - - def ephemeralPort: Builder = - address(SocketAddress.Wildcard) - - def unixSocket(path: fs2.io.file.Path): Builder = - address(UnixSocketAddress(path.toString)) -} - -private[net] trait UnsealedNeedAddress[F[_], Builder] extends NeedAddress[F, Builder] - -object TcpBuilder { - private[net] def apply[F[_]]( - mkClient: (GenSocketAddress, List[SocketOption]) => Resource[F, Socket[F]], - address: GenSocketAddress, - options: List[SocketOption] - ): TcpBuilder[F] = new TcpBuilder[F] { - private def copy(address: GenSocketAddress = address, options: List[SocketOption] = options): TcpBuilder[F] = - apply[F](mkClient, address, options) - def withSocketOptions(options: List[SocketOption]) = copy(options = options) - def withAddress(address: GenSocketAddress) = copy(address = address) - def connect: Resource[F, Socket[F]] = mkClient(address, options) - def bind: Resource[F, BoundServer[F]]= ??? - def bindAndServe: Stream[F, Socket[F]] = ??? //Stream.resource(bind).flatMap(_.clients) - } - -} From a86b18d5772a2795678b940bae200b22740215f5 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 15:09:18 -0400 Subject: [PATCH 04/79] wip --- .../fs2/io/net/SocketGroupPlatform.scala | 28 +++++-- .../scala/fs2/io/net/SocketInfoPlatform.scala | 83 +++++++++++++++++++ .../scala/fs2/io/net/SocketPlatform.scala | 16 ++-- .../scala/fs2/io/net/NetworkPlatform.scala | 35 +++++++- .../scala/fs2/io/net/SelectingSocket.scala | 15 ++-- .../fs2/io/net/SelectingSocketGroup.scala | 43 ++++------ .../fs2/io/net/tls/TLSSocketPlatform.scala | 17 +++- .../net/unixsocket/UnixSocketsPlatform.scala | 15 +++- .../src/main/scala/fs2/io/net/Network.scala | 4 +- .../src/main/scala/fs2/io/net/Socket.scala | 8 +- .../main/scala/fs2/io/net/SocketGroup.scala | 5 ++ .../main/scala/fs2/io/net/SocketInfo.scala | 43 ++++++++++ 12 files changed, 252 insertions(+), 60 deletions(-) create mode 100644 io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala create mode 100644 io/shared/src/main/scala/fs2/io/net/SocketInfo.scala diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 0e3729a706..3b96f94562 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -33,7 +33,7 @@ import java.nio.channels.{ import java.nio.channels.AsynchronousChannelGroup import cats.syntax.all._ import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} +import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => private[fs2] def unsafe[F[_]: Async: Dns]( @@ -84,10 +84,18 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => address: Option[Host], port: Option[Port], options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = { + ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => + bound.serverSocketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } + } + + def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, BoundServer[F]] = { val setup: Resource[F, AsynchronousServerSocketChannel] = - Resource.eval(address.traverse(_.resolve[F])).flatMap { addr => + Resource.eval(address.host.resolve[F]).flatMap { addr => Resource .make( Async[F].delay( @@ -98,8 +106,8 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => Async[F].delay( ch.bind( new InetSocketAddress( - addr.map(_.toInetAddress).orNull, - port.map(_.value).getOrElse(0) + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value ) ) ) @@ -152,9 +160,13 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => } setup.map { sch => - val jLocalAddress = sch.getLocalAddress.asInstanceOf[java.net.InetSocketAddress] - val localAddress = SocketAddress.fromInetSocketAddress(jLocalAddress) - (localAddress, acceptIncoming(sch)) + // val jLocalAddress = sch.getLocalAddress.asInstanceOf[java.net.InetSocketAddress] + // val localAddress = SocketAddress.fromInetSocketAddress(jLocalAddress) + // (localAddress, acceptIncoming(sch)) + new UnsealedBoundServer[F] { + def serverSocketInfo = SocketInfo.forAsync(sch) + def clients = acceptIncoming(sch) + } } } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala new file mode 100644 index 0000000000..cea76c130d --- /dev/null +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import cats.effect.Async + +import java.net.InetSocketAddress +import java.nio.channels.NetworkChannel + +import scala.jdk.CollectionConverters.* + +private[net] trait SocketInfoCompanionPlatform { + private[net] def forAsync[F[_]](ch: NetworkChannel)(implicit F: Async[F]): SocketInfo[F] = + new AsyncSocketInfo[F] { + def asyncInstance = F + def channel = ch + } + + private[net] trait AsyncSocketInfo[F[_]] extends SocketInfo[F] { + + implicit protected def asyncInstance: Async[F] + protected def channel: NetworkChannel + + override def localAddress: F[SocketAddress[IpAddress]] = + asyncInstance.delay( + SocketAddress.fromInetSocketAddress( + channel.getLocalAddress.asInstanceOf[InetSocketAddress] + ) + ) + + override def localAddressGen: F[GenSocketAddress] = + asyncInstance.delay( + channel.getLocalAddress match { + case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) + // TODO handle unix sockets + } + ) + + override def supportedOptions: F[Set[SocketOption.Key[_]]] = + asyncInstance.delay { + channel.supportedOptions.asScala.toSet + } + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + asyncInstance.delay { + try { + Some(channel.getOption(key)) + } catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + asyncInstance.delay { + channel.setOption(key, value) + () + } + } + +} + diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index 257a435fea..616cd6dc0e 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import cats.effect.Async import cats.effect.std.Mutex import cats.syntax.all._ @@ -108,7 +108,10 @@ private[net] trait SocketCompanionPlatform { readMutex: Mutex[F], writeMutex: Mutex[F] )(implicit F: Async[F]) - extends BufferedReads[F](readMutex) { + extends BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch protected def readChunk(buffer: ByteBuffer): F[Int] = F.async[Int] { cb => @@ -139,13 +142,6 @@ private[net] trait SocketCompanionPlatform { } } - def localAddress: F[SocketAddress[IpAddress]] = - F.delay( - SocketAddress.fromInetSocketAddress( - ch.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - ) - def remoteAddress: F[SocketAddress[IpAddress]] = F.delay( SocketAddress.fromInetSocketAddress( @@ -153,6 +149,8 @@ private[net] trait SocketCompanionPlatform { ) ) + override def remoteAddressGen: F[GenSocketAddress] = ??? + def isOpen: F[Boolean] = F.delay(ch.isOpen) def endOfOutput: F[Unit] = diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index ec6dd0e9cb..1a60721bdc 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -125,6 +125,16 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N case None => fallback.serverResource(address, port, options) } + def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, BoundServer[F]] = + Resource.eval(tryGetSelector).flatMap { + case Some(selector) => + new SelectingSocketGroup(selector).serverBound(address, options) + case None => fallback.serverBound(address, options) + } + def openDatagramSocket( address: Option[Host], port: Option[Port], @@ -176,6 +186,12 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = globalSocketGroup.serverResource(address, port, options) + def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, BoundServer[F]] = + globalSocketGroup.serverBound(address, options) + def openDatagramSocket( address: Option[Host], port: Option[Port], @@ -200,9 +216,24 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N } } - def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, BoundServer[F]] = ??? + def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, BoundServer[F]] = { + address match { + case sa: SocketAddress[_] => + // val host = sa.host match { + // case ip: IpAddress if ip.isWildcard => None + // case other => Some(other) + // } + // val port = if (sa.port == Port.Wildcard) None else Some(sa.port) + serverBound(sa, options) + case ua: UnixSocketAddress => + unixSockets.server(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) + ??? + case _ => ??? + } + } - def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = ??? + def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = + Stream.resource(bind(address, options)).flatMap(_.clients) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala index 3c1b424912..c144839e16 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala @@ -29,8 +29,7 @@ import cats.effect.Selector import cats.effect.kernel.Async import cats.effect.std.Mutex import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import java.nio.ByteBuffer import java.nio.channels.SelectionKey.OP_READ @@ -42,10 +41,15 @@ private final class SelectingSocket[F[_]: LiftIO] private ( ch: SocketChannel, readMutex: Mutex[F], writeMutex: Mutex[F], - val localAddress: F[SocketAddress[IpAddress]], val remoteAddress: F[SocketAddress[IpAddress]] )(implicit F: Async[F]) - extends Socket.BufferedReads(readMutex) { + extends Socket.BufferedReads(readMutex) with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch + + def remoteAddressGen: F[GenSocketAddress] = + remoteAddress.map(a => a: GenSocketAddress) protected def readChunk(buf: ByteBuffer): F[Int] = F.delay(ch.read(buf)).flatMap { readed => @@ -79,6 +83,7 @@ private final class SelectingSocket[F[_]: LiftIO] private ( F.delay { ch.shutdownInput(); () } + override def sendFile( file: FileHandle[F], offset: Long, @@ -111,7 +116,6 @@ private object SelectingSocket { def apply[F[_]: LiftIO]( selector: Selector, ch: SocketChannel, - localAddress: F[SocketAddress[IpAddress]], remoteAddress: F[SocketAddress[IpAddress]] )(implicit F: Async[F]): F[Socket[F]] = (Mutex[F], Mutex[F]).flatMapN { (readMutex, writeMutex) => @@ -121,7 +125,6 @@ private object SelectingSocket { ch, readMutex, writeMutex, - localAddress, remoteAddress ) } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala index 2bcb1ac1fe..f488f2cb29 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala @@ -27,11 +27,7 @@ import cats.effect.Selector import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.syntax.all._ -import com.comcast.ip4s.Dns -import com.comcast.ip4s.Host -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} import java.net.InetSocketAddress import java.nio.channels.AsynchronousCloseException @@ -71,7 +67,6 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( val make = SelectingSocket[F]( selector, ch, - localAddress(ch), remoteAddress(ch) ) @@ -98,18 +93,27 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( port: Option[Port], options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => + bound.serverSocketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } + } + + + def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, BoundServer[F]] = Resource .make(F.delay(selector.provider.openServerSocketChannel())) { ch => F.delay(ch.close()) } .evalMap { serverCh => - val configure = address.traverse(_.resolve).flatMap { ip => + val configure = address.host.resolve.flatMap { addr => F.delay { serverCh.configureBlocking(false) serverCh.bind( new InetSocketAddress( - ip.map(_.toInetAddress).orNull, - port.map(_.value).getOrElse(0) + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value ) ) } @@ -130,34 +134,23 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( case ex => Stream.raiseError(ex) } - val clients = acceptLoop.evalMap { ch => + val clients0 = acceptLoop.evalMap { ch => F.delay { ch.configureBlocking(false) options.foreach(opt => ch.setOption(opt.key, opt.value)) } *> SelectingSocket[F]( selector, ch, - localAddress(ch), remoteAddress(ch) ) } - val socketAddress = F.delay { - SocketAddress.fromInetSocketAddress( - serverCh.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - } - - configure *> socketAddress.tupleRight(clients) + configure.as(new UnsealedBoundServer[F] { + def serverSocketInfo = SocketInfo.forAsync(serverCh) + def clients = clients0 + }) } - private def localAddress(ch: SocketChannel) = - F.delay { - SocketAddress.fromInetSocketAddress( - ch.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - } - private def remoteAddress(ch: SocketChannel) = F.delay { SocketAddress.fromInetSocketAddress( diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 66ecb83612..229ae53f46 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -29,7 +29,7 @@ import cats.effect.std.Mutex import cats.effect.kernel._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} private[tls] trait TLSSocketPlatform[F[_]] { @@ -91,9 +91,24 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + def localAddressGen: F[GenSocketAddress] = + socket.localAddressGen + def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress + def remoteAddressGen: F[GenSocketAddress] = + socket.remoteAddressGen + + def supportedOptions: F[Set[SocketOption.Key[_]]] = + socket.supportedOptions + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + socket.getOption(key) + + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + socket.setOption(key, value) + def beginHandshake: F[Unit] = engine.beginHandshake diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index c12348984e..cf6268661e 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -27,10 +27,10 @@ import cats.effect.kernel.{Async, Resource} import cats.effect.std.Mutex import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.{Chunk, Stream} import fs2.io.file.{Files, Path} -import fs2.io.net.Socket +import fs2.io.net.{Socket, SocketInfo} import fs2.io.evalOnVirtualThreadIfAvailable import java.nio.ByteBuffer import java.nio.channels.SocketChannel @@ -100,7 +100,10 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { readMutex: Mutex[F], writeMutex: Mutex[F] )(implicit F: Async[F]) - extends Socket.BufferedReads[F](readMutex) { + extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch def readChunk(buff: ByteBuffer): F[Int] = evalOnVirtualThreadIfAvailable(F.blocking(ch.read(buff))) @@ -116,8 +119,12 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { } } - def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + + def remoteAddressGen: F[GenSocketAddress] = ??? // TODO + private def raiseIpAddressError[A]: F[A] = F.raiseError(new UnsupportedOperationException("UnixSockets do not use IP addressing")) diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index d0717e97bb..9940b8f01d 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -54,7 +54,9 @@ sealed trait Network[F[_]] with DatagramSocketGroup[F] { def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] + def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, BoundServer[F]] + def bindAndAccept(address: GenSocketAddress, options: List[SocketOption] = Nil): Stream[F, Socket[F]] /** Returns a builder for `TLSContext[F]` values. @@ -71,7 +73,7 @@ object Network extends NetworkCompanionPlatform { } sealed trait BoundServer[F[_]] { - def serverSocket: Socket[F] + def serverSocketInfo: SocketInfo[F] def clients: Stream[F, Socket[F]] } private[net] trait UnsealedBoundServer[F[_]] extends BoundServer[F] diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index 736317c781..e6120ae958 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -23,12 +23,12 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.file.FileHandle /** Provides the ability to read/write from a TCP socket in the effect `F`. */ -trait Socket[F[_]] { +trait Socket[F[_]] extends SocketInfo[F] { /** Reads up to `maxBytes` from the peer. * @@ -58,8 +58,8 @@ trait Socket[F[_]] { /** Asks for the remote address of the peer. */ def remoteAddress: F[SocketAddress[IpAddress]] - /** Asks for the local address of the socket. */ - def localAddress: F[SocketAddress[IpAddress]] + // TODO + def remoteAddressGen: F[GenSocketAddress] /** Writes `bytes` to the peer. * diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 016609a129..2231f09c9b 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -69,6 +69,11 @@ trait SocketGroup[F[_]] { port: Option[Port] = None, options: List[SocketOption] = List.empty ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] + + def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] = Nil + ): Resource[F, BoundServer[F]] } private[net] object SocketGroup extends SocketGroupCompanionPlatform { diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala new file mode 100644 index 0000000000..d07146ea58 --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} + +trait SocketInfo[F[_]] { + + /** Asks for the local address of the socket. */ + def localAddressGen: F[GenSocketAddress] + + def localAddress: F[SocketAddress[IpAddress]] + + def supportedOptions: F[Set[SocketOption.Key[_]]] + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] + + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] +} + +object SocketInfo extends SocketInfoCompanionPlatform + From c06e969c62586affcd18fb303a2e5a2c364ca2d4 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 21:45:40 -0400 Subject: [PATCH 05/79] wip --- .../fs2/io/net/SocketGroupPlatform.scala | 8 +- ...hronousChannelGroupIpSocketsProvider.scala | 171 ++++++++++++++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 161 +++++++++-------- .../io/net/SelectingIpSocketsProvider.scala | 136 ++++++++++++++ .../fs2/io/net/SelectingSocketGroup.scala | 8 +- .../src/main/scala/fs2/io/net/Bind.scala | 31 ++++ .../scala/fs2/io/net/IpSocketsProvider.scala | 40 ++++ .../src/main/scala/fs2/io/net/Network.scala | 22 ++- .../main/scala/fs2/io/net/SocketGroup.scala | 2 +- 9 files changed, 489 insertions(+), 90 deletions(-) create mode 100644 io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala create mode 100644 io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala create mode 100644 io/shared/src/main/scala/fs2/io/net/Bind.scala create mode 100644 io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 3b96f94562..39a30b2918 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -86,13 +86,13 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => - bound.serverSocketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } + bound.socketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } } def serverBound( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, BoundServer[F]] = { + ): Resource[F, Bind[F]] = { val setup: Resource[F, AsynchronousServerSocketChannel] = Resource.eval(address.host.resolve[F]).flatMap { addr => @@ -163,8 +163,8 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => // val jLocalAddress = sch.getLocalAddress.asInstanceOf[java.net.InetSocketAddress] // val localAddress = SocketAddress.fromInetSocketAddress(jLocalAddress) // (localAddress, acceptIncoming(sch)) - new UnsealedBoundServer[F] { - def serverSocketInfo = SocketInfo.forAsync(sch) + new UnsealedBind[F] { + def socketInfo = SocketInfo.forAsync(sch) def clients = acceptIncoming(sch) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala new file mode 100644 index 0000000000..6df5af0e30 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + + +import java.net.InetSocketAddress +import java.nio.channels.{ + AsynchronousCloseException, + AsynchronousServerSocketChannel, + AsynchronousSocketChannel, + CompletionHandler +} +import java.nio.channels.AsynchronousChannelGroup + +import cats.syntax.all._ +import cats.effect.{Async, Resource} +import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} + +import fs2.internal.ThreadFactories + +private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( + channelGroup: AsynchronousChannelGroup +)(implicit F: Async[F]) extends IpSocketsProvider[F] { + + private implicit val dns: Dns[F] = Dns.forAsync[F] + + override def connect( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = { + + def setup: Resource[F, AsynchronousSocketChannel] = + Resource + .make( + F.delay( + AsynchronousSocketChannel.open(channelGroup) + ) + )(ch => F.delay(if (ch.isOpen) ch.close else ())) + .evalTap(ch => F.delay(options.foreach(opt => ch.setOption(opt.key, opt.value)))) + + def connect(ch: AsynchronousSocketChannel): F[AsynchronousSocketChannel] = + address.resolve[F].flatMap { ip => + F.async[AsynchronousSocketChannel] { cb => + F.delay { + ch.connect( + ip.toInetSocketAddress, + null, + new CompletionHandler[Void, Void] { + def completed(result: Void, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + } + .as(Some(F.delay(ch.close()))) + } + } + + setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) + } + + override def bind( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Bind[F]] = { + + + val setup: Resource[F, AsynchronousServerSocketChannel] = + Resource.eval(address.host.resolve[F]).flatMap { addr => + Resource + .make( + F.delay( + AsynchronousServerSocketChannel.open(channelGroup) + ) + )(sch => F.delay(if (sch.isOpen) sch.close())) + .evalTap(ch => + F.delay( + ch.bind( + new InetSocketAddress( + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value + ) + ) + ) + ) + } + + def acceptIncoming( + sch: AsynchronousServerSocketChannel + ): Stream[F, Socket[F]] = { + def go: Stream[F, Socket[F]] = { + def acceptChannel = Resource.makeFull[F, AsynchronousSocketChannel] { poll => + poll { + F.async[AsynchronousSocketChannel] { cb => + F.delay { + sch.accept( + null, + new CompletionHandler[AsynchronousSocketChannel, Void] { + def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + } + .as(Some(F.delay(sch.close()))) + } + } + }(ch => F.delay(if (ch.isOpen) ch.close else ())) + + def setOpts(ch: AsynchronousSocketChannel) = + F.delay { + options.foreach(o => ch.setOption(o.key, o.value)) + } + + Stream.resource(acceptChannel.attempt).flatMap { + case Left(_) => Stream.empty[F] + case Right(accepted) => Stream.eval(setOpts(accepted) *> Socket.forAsync(accepted)) + } ++ go + } + + go.handleErrorWith { + case err: AsynchronousCloseException => + Stream.eval(F.delay(sch.isOpen)).flatMap { isOpen => + if (isOpen) Stream.raiseError[F](err) + else Stream.empty + } + case err => Stream.raiseError[F](err) + } + } + + setup.map { sch => + new UnsealedBind[F] { + def socketInfo = SocketInfo.forAsync(sch) + def clients = acceptIncoming(sch) + } + } + } +} + +private[net] object AsynchronousChannelGroupIpSocketsProvider { + private lazy val globalAcg = AsynchronousChannelGroup.withFixedThreadPool( + 1, + ThreadFactories.named("fs2-global-tcp", true) + ) + + def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = + new AsynchronousChannelGroupIpSocketsProvider[F](globalAcg) +} diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 1a60721bdc..a663fc9cbd 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,12 +23,13 @@ package fs2 package io package net +import cats.ApplicativeThrow import cats.effect.IO import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress, UnixSocketAddress} import fs2.internal.ThreadFactories import fs2.io.net.tls.TLSContext @@ -70,13 +71,16 @@ private[net] trait NetworkPlatform[F[_]] { } private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - private lazy val globalAcg = AsynchronousChannelGroup.withFixedThreadPool( - 1, - ThreadFactories.named("fs2-global-tcp", true) - ) private lazy val globalAdsg = AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) + private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + address match { + case sa: SocketAddress[Host] => ifIp(sa) + case ua: UnixSocketAddress => ifUnix(ua) + case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) + } + def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = @@ -88,15 +92,49 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private implicit def dns: Dns[F] = Dns.forAsync[F] + def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + matchAddress(address, + sa => + Resource.eval(tryGetSelector).flatMap { + case Some(selector) => new SelectingIpSocketsProvider(selector).connect(sa, options) + case None => fallback.connect(sa, options) + }, + ua => ???) + + def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Bind[F]] = + matchAddress(address, + sa => + Resource.eval(tryGetSelector).flatMap { + case Some(selector) => new SelectingIpSocketsProvider(selector).bind(sa, options) + case None => fallback.bind(sa, options) + }, + ua => ???) + + def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = + fallback.datagramSocketGroup(threadFactory) + + def openDatagramSocket( + address: Option[Host], + port: Option[Port], + options: List[SocketOption], + protocolFamily: Option[ProtocolFamily] + ): Resource[F, DatagramSocket[F]] = + fallback.openDatagramSocket(address, port, options, protocolFamily) + + // Implementations of deprecated operations + def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = Resource.eval(tryGetSelector).flatMap { case Some(selector) => Resource.pure(new SelectingSocketGroup[F](selector)) case None => fallback.socketGroup(threadCount, threadFactory) } - def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - fallback.datagramSocketGroup(threadFactory) - def client( to: SocketAddress[Host], options: List[SocketOption] @@ -128,20 +166,13 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def serverBound( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, BoundServer[F]] = + ): Resource[F, Bind[F]] = Resource.eval(tryGetSelector).flatMap { case Some(selector) => new SelectingSocketGroup(selector).serverBound(address, options) case None => fallback.serverBound(address, options) } - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - fallback.openDatagramSocket(address, port, options, protocolFamily) } def forAsync[F[_]](implicit F: Async[F]): Network[F] = @@ -149,9 +180,43 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = new AsyncNetwork[F] { - private lazy val globalSocketGroup = SocketGroup.unsafe[F](globalAcg) + private lazy val ipSockets = AsynchronousChannelGroupIpSocketsProvider.forAsync[F] private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) + def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + matchAddress(address, + sa => ipSockets.connect(sa, options), + ua => ???) + + def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Bind[F]] = + matchAddress(address, + sa => ipSockets.bind(sa, options), + ua => ???) + + def openDatagramSocket( + address: Option[Host], + port: Option[Port], + options: List[SocketOption], + protocolFamily: Option[ProtocolFamily] + ): Resource[F, DatagramSocket[F]] = + globalDatagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) + + def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = + Resource + .make(F.delay(AsynchronousDatagramSocketGroup.unsafe(threadFactory)))(adsg => + F.delay(adsg.close()) + ) + .map(DatagramSocketGroup.unsafe[F](_)) + + // Implementations of deprecated operations + + // TODO adapt SocketGroup to IpSocketsProvider and make this a Resource.pure def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = Resource .make( @@ -161,79 +226,29 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N )(acg => F.delay(acg.shutdown())) .map(SocketGroup.unsafe[F](_)) - def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - Resource - .make(F.delay(AsynchronousDatagramSocketGroup.unsafe(threadFactory)))(adsg => - F.delay(adsg.close()) - ) - .map(DatagramSocketGroup.unsafe[F](_)) - def client( to: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) + ): Resource[F, Socket[F]] = ipSockets.connect(to, options) def server( address: Option[Host], port: Option[Port], options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) + ): Stream[F, Socket[F]] = Stream.resource(serverResource(address, port, options)).flatMap(_._2) def serverResource( address: Option[Host], port: Option[Port], options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) + serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) + .flatMap(b => Resource.eval(b.socketInfo.localAddress).map(a => (a, b.clients))) def serverBound( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, BoundServer[F]] = - globalSocketGroup.serverBound(address, options) - - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - globalDatagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) - } - - - private abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends UnsealedNetwork[F] { - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] - - private def unixSockets = fs2.io.net.unixsocket.UnixSockets.forAsync[F] - - def connect(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = { - address match { - case sa: SocketAddress[_] => client(sa, options) - case ua: UnixSocketAddress => unixSockets.client(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) - case _ => ??? - } + ): Resource[F, Bind[F]] = + ipSockets.bind(address, options) } - - def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, BoundServer[F]] = { - address match { - case sa: SocketAddress[_] => - // val host = sa.host match { - // case ip: IpAddress if ip.isWildcard => None - // case other => Some(other) - // } - // val port = if (sa.port == Port.Wildcard) None else Some(sa.port) - serverBound(sa, options) - case ua: UnixSocketAddress => - unixSockets.server(fs2.io.net.unixsocket.UnixSocketAddress(ua.path)) - ??? - case _ => ??? - } - } - - def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = - Stream.resource(bind(address, options)).flatMap(_.clients) - } - } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala new file mode 100644 index 0000000000..0813bb6a34 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io.net + +import cats.effect.LiftIO +import cats.effect.Selector +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import cats.syntax.all._ +import com.comcast.ip4s.{Dns, Host, SocketAddress} + +import java.net.InetSocketAddress +import java.nio.channels.AsynchronousCloseException +import java.nio.channels.ClosedChannelException +import java.nio.channels.SelectionKey.OP_ACCEPT +import java.nio.channels.SelectionKey.OP_CONNECT +import java.nio.channels.SocketChannel + +private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implicit + F: Async[F], F2: LiftIO[F], F3: Dns[F] +) extends IpSocketsProvider[F] { + + def connect( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + Resource + .make(F.delay(selector.provider.openSocketChannel())) { ch => + F.delay(ch.close()) + } + .evalMap { ch => + val configure = F.delay { + ch.configureBlocking(false) + options.foreach(opt => ch.setOption(opt.key, opt.value)) + } + + val connect = to.resolve.flatMap { ip => + F.delay(ch.connect(ip.toInetSocketAddress)).flatMap { connected => + selector + .select(ch, OP_CONNECT) + .to + .untilM_(F.delay(ch.finishConnect())) + .unlessA(connected) + } + } + + val make = SelectingSocket[F]( + selector, + ch, + remoteAddress(ch) + ) + + configure *> connect *> make + } + + def bind( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Bind[F]] = + Resource + .make(F.delay(selector.provider.openServerSocketChannel())) { ch => + F.delay(ch.close()) + } + .evalMap { serverCh => + val configure = address.host.resolve.flatMap { addr => + F.delay { + serverCh.configureBlocking(false) + serverCh.bind( + new InetSocketAddress( + if (addr.isWildcard) null else addr.toInetAddress, + address.port.value + ) + ) + } + } + + def acceptLoop: Stream[F, SocketChannel] = Stream + .bracketFull[F, SocketChannel] { poll => + def go: F[SocketChannel] = + F.delay(serverCh.accept()).flatMap { + case null => poll(selector.select(serverCh, OP_ACCEPT).to) *> go + case ch => F.pure(ch) + } + go + }((ch, _) => F.delay(ch.close())) + .repeat + .handleErrorWith { + case _: AsynchronousCloseException | _: ClosedChannelException => acceptLoop + case ex => Stream.raiseError(ex) + } + + val clients0 = acceptLoop.evalMap { ch => + F.delay { + ch.configureBlocking(false) + options.foreach(opt => ch.setOption(opt.key, opt.value)) + } *> SelectingSocket[F]( + selector, + ch, + remoteAddress(ch) + ) + } + + configure.as(new UnsealedBind[F] { + def socketInfo = SocketInfo.forAsync(serverCh) + def clients = clients0 + }) + } + + private def remoteAddress(ch: SocketChannel) = + F.delay { + SocketAddress.fromInetSocketAddress( + ch.getRemoteAddress.asInstanceOf[InetSocketAddress] + ) + } + +} diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala index f488f2cb29..f6ca5fd6f1 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala @@ -94,14 +94,14 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => - bound.serverSocketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } + bound.socketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } } def serverBound( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, BoundServer[F]] = + ): Resource[F, Bind[F]] = Resource .make(F.delay(selector.provider.openServerSocketChannel())) { ch => F.delay(ch.close()) @@ -145,8 +145,8 @@ private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)( ) } - configure.as(new UnsealedBoundServer[F] { - def serverSocketInfo = SocketInfo.forAsync(serverCh) + configure.as(new UnsealedBind[F] { + def socketInfo = SocketInfo.forAsync(serverCh) def clients = clients0 }) } diff --git a/io/shared/src/main/scala/fs2/io/net/Bind.scala b/io/shared/src/main/scala/fs2/io/net/Bind.scala new file mode 100644 index 0000000000..afe144eac4 --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/Bind.scala @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +sealed trait Bind[F[_]] { + def socketInfo: SocketInfo[F] + def clients: Stream[F, Socket[F]] +} + +private[net] trait UnsealedBind[F[_]] extends Bind[F] diff --git a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala new file mode 100644 index 0000000000..0071631d25 --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.kernel.Resource +import com.comcast.ip4s.{Host, SocketAddress} + +private[net] trait IpSocketsProvider[F[_]] { + + def connect( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] + + def bind( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Bind[F]] +} diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 9940b8f01d..c116c8e71a 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import cats.effect.Resource +import cats.effect.{Async, Resource} import com.comcast.ip4s.GenSocketAddress import fs2.io.net.tls.TLSContext @@ -55,7 +55,7 @@ sealed trait Network[F[_]] def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] - def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, BoundServer[F]] + def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Bind[F]] def bindAndAccept(address: GenSocketAddress, options: List[SocketOption] = Nil): Stream[F, Socket[F]] @@ -69,12 +69,18 @@ sealed trait Network[F[_]] object Network extends NetworkCompanionPlatform { private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] - def apply[F[_]](implicit F: Network[F]): F.type = F -} + private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { + + override def connect(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] + + override def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Bind[F]] -sealed trait BoundServer[F[_]] { - def serverSocketInfo: SocketInfo[F] - def clients: Stream[F, Socket[F]] + override def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = + Stream.resource(bind(address, options)).flatMap(_.clients) + + override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + } + + def apply[F[_]](implicit F: Network[F]): F.type = F } -private[net] trait UnsealedBoundServer[F[_]] extends BoundServer[F] diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 2231f09c9b..1128776123 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -73,7 +73,7 @@ trait SocketGroup[F[_]] { def serverBound( address: SocketAddress[Host], options: List[SocketOption] = Nil - ): Resource[F, BoundServer[F]] + ): Resource[F, Bind[F]] } private[net] object SocketGroup extends SocketGroupCompanionPlatform { From eff4b58239359b7a0c4633eb9633376968982340 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 8 Apr 2025 22:03:07 -0400 Subject: [PATCH 06/79] wip --- .../scala/fs2/io/net/NetworkPlatform.scala | 82 ++----------------- .../src/main/scala/fs2/io/net/Network.scala | 28 ++++++- 2 files changed, 35 insertions(+), 75 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index a663fc9cbd..62c7746b28 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -92,16 +92,18 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private implicit def dns: Dns[F] = Dns.forAsync[F] + private def selecting[A](ifSelecting: SelectingIpSocketsProvider[F] => Resource[F, A], orElse: => Resource[F, A]): Resource[F, A] = + Resource.eval(tryGetSelector).flatMap { + case Some(selector) => ifSelecting(new SelectingIpSocketsProvider(selector)) + case None => orElse + } + def connect( address: GenSocketAddress, options: List[SocketOption] ): Resource[F, Socket[F]] = matchAddress(address, - sa => - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingIpSocketsProvider(selector).connect(sa, options) - case None => fallback.connect(sa, options) - }, + sa => selecting(_.connect(sa, options), fallback.connect(sa, options)), ua => ???) def bind( @@ -109,11 +111,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N options: List[SocketOption] ): Resource[F, Bind[F]] = matchAddress(address, - sa => - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingIpSocketsProvider(selector).bind(sa, options) - case None => fallback.bind(sa, options) - }, + sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), ua => ???) def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = @@ -134,45 +132,6 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N case Some(selector) => Resource.pure(new SelectingSocketGroup[F](selector)) case None => fallback.socketGroup(threadCount, threadFactory) } - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = Resource.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingSocketGroup(selector).client(to, options) - case None => fallback.client(to, options) - } - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = Stream.eval(tryGetSelector).flatMap { - case Some(selector) => new SelectingSocketGroup(selector).server(address, port, options) - case None => fallback.server(address, port, options) - } - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => - new SelectingSocketGroup(selector).serverResource(address, port, options) - case None => fallback.serverResource(address, port, options) - } - - def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Bind[F]] = - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => - new SelectingSocketGroup(selector).serverBound(address, options) - case None => fallback.serverBound(address, options) - } - } def forAsync[F[_]](implicit F: Async[F]): Network[F] = @@ -225,30 +184,5 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ) )(acg => F.delay(acg.shutdown())) .map(SocketGroup.unsafe[F](_)) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = ipSockets.connect(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = Stream.resource(serverResource(address, port, options)).flatMap(_._2) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) - .flatMap(b => Resource.eval(b.socketInfo.localAddress).map(a => (a, b.clients))) - - def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Bind[F]] = - ipSockets.bind(address, options) } } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index c116c8e71a..6931fc8bef 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -24,7 +24,7 @@ package io package net import cats.effect.{Async, Resource} -import com.comcast.ip4s.GenSocketAddress +import com.comcast.ip4s.{GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress} import fs2.io.net.tls.TLSContext /** Provides the ability to work with TCP, UDP, and TLS. @@ -79,6 +79,32 @@ object Network extends NetworkCompanionPlatform { Stream.resource(bind(address, options)).flatMap(_.clients) override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + + // Implementations of deprecated operations + + override def client( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = connect(to, options) + + override def server( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Stream[F, Socket[F]] = Stream.resource(serverResource(address, port, options)).flatMap(_._2) + + override def serverResource( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) + .flatMap(b => Resource.eval(b.socketInfo.localAddress).map(a => (a, b.clients))) + + override def serverBound( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Bind[F]] = bind(address, options) } def apply[F[_]](implicit F: Network[F]): F.type = F From 89362b0e6ac8da10eb159256ec228c88d0e58d72 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 9 Apr 2025 22:21:04 -0400 Subject: [PATCH 07/79] wip --- .../scala/fs2/io/net/SocketOptionPlatform.scala | 17 +++++++++++++++++ .../main/scala/fs2/io/net/NetworkPlatform.scala | 9 +++++---- .../net/unixsocket/FdPollingUnixSockets.scala | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index cdb050fcb7..724f0e12e9 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -71,4 +71,21 @@ private[net] trait SocketOptionCompanionPlatform { def noDelay(value: Boolean): SocketOption = boolean(StandardSocketOptions.TCP_NODELAY, value) + + val UnixServerSocketDeleteIfExists: Key[JBoolean] = new Key[JBoolean] { + def name() = "FS2_UNIX_DELETE_IF_EXISTS" + def `type`() = classOf[JBoolean] + } + + def unixServerSocketDeleteIfExists(value: JBoolean): SocketOption = + boolean(UnixServerSocketDeleteIfExists, value) + + val UnixServerSocketDeleteOnClose: Key[JBoolean] = new Key[JBoolean] { + def name() = "FS2_UNIX_DELETE_ON_CLOSE" + def `type`() = classOf[JBoolean] + } + + def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = + boolean(UnixServerSocketDeleteOnClose, value) + } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 62c7746b28..c1c4610b62 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -104,7 +104,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Socket[F]] = matchAddress(address, sa => selecting(_.connect(sa, options), fallback.connect(sa, options)), - ua => ???) + ua => fallback.connect(address, options)) def bind( address: GenSocketAddress, @@ -112,7 +112,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Bind[F]] = matchAddress(address, sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), - ua => ???) + ua => fallback.bind(address, options)) def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = fallback.datagramSocketGroup(threadFactory) @@ -140,6 +140,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = new AsyncNetwork[F] { private lazy val ipSockets = AsynchronousChannelGroupIpSocketsProvider.forAsync[F] + private lazy val unixSockets = UnixSocketsProvider.forAsync[F] private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) def connect( @@ -148,7 +149,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Socket[F]] = matchAddress(address, sa => ipSockets.connect(sa, options), - ua => ???) + ua => unixSockets.connect(ua, options)) def bind( address: GenSocketAddress, @@ -156,7 +157,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Bind[F]] = matchAddress(address, sa => ipSockets.bind(sa, options), - ua => ???) + ua => unixSockets.bind(ua, options)) def openDatagramSocket( address: Option[Host], diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala b/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala index 98ef205543..8ddbed9519 100644 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala +++ b/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala @@ -142,6 +142,6 @@ private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[ } private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("UnixSockets do not use IP addressing")) + F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) } From fcd9233673cae753d22e4724bd27f40e8bb85d31 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 9 Apr 2025 22:37:08 -0400 Subject: [PATCH 08/79] wip --- ...ets.scala => JdkUnixSocketsProvider.scala} | 34 ++-- ...ets.scala => JnrUnixSocketsProvider.scala} | 43 ++-- .../io/net/UnixSocketsProviderPlatform.scala | 192 ++++++++++++++++++ .../net/unixsocket/UnixSocketsPlatform.scala | 144 ++----------- .../fs2/io/net/UnixSocketsProvider.scala | 41 ++++ 5 files changed, 302 insertions(+), 152 deletions(-) rename io/jvm/src/main/scala/fs2/io/net/{unixsocket/JdkUnixSockets.scala => JdkUnixSocketsProvider.scala} (71%) rename io/jvm/src/main/scala/fs2/io/net/{unixsocket/JnrUnixSockets.scala => JnrUnixSocketsProvider.scala} (58%) create mode 100644 io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala create mode 100644 io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala similarity index 71% rename from io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala rename to io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala index d1e476568a..e488282e19 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JdkUnixSockets.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala @@ -19,28 +19,33 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net -import cats.effect.kernel.{Async, Resource} +import cats.effect.{Async, Resource} import cats.effect.syntax.all._ + +import com.comcast.ip4s.UnixSocketAddress + import fs2.io.file.Files -import fs2.io.evalOnVirtualThreadIfAvailable + import java.net.{StandardProtocolFamily, UnixDomainSocketAddress} import java.nio.channels.{ServerSocketChannel, SocketChannel} -object JdkUnixSockets { +private[net] object JdkUnixSocketsProvider { def supported: Boolean = StandardProtocolFamily.values.size > 2 - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - new JdkUnixSocketsImpl[F] + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + new JdkUnixSocketsProvider[F] - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = forAsyncAndFiles(F, Files.forAsync[F]) } -private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) - extends UnixSockets.AsyncUnixSockets[F] { +private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) + extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { protected def openChannel(address: UnixSocketAddress) = evalOnVirtualThreadIfAvailable( Resource @@ -53,7 +58,7 @@ private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) } ) - protected def openServerChannel(address: UnixSocketAddress) = + protected def openServerChannel(address: UnixSocketAddress, options: List[SocketOption]) = evalOnVirtualThreadIfAvailable( Resource .make( @@ -64,10 +69,11 @@ private[unixsocket] class JdkUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) .cancelable(F.blocking(sch.close())) } .map { sch => - Resource.makeFull[F, SocketChannel] { poll => - poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) - }(ch => F.blocking(ch.close())) + SocketInfo.forAsync(sch) -> + Resource.makeFull[F, SocketChannel] { poll => + poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) + }(ch => F.blocking(ch.close())) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } ) - } diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala similarity index 58% rename from io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala rename to io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index 787b721412..be1e79bee3 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/JnrUnixSockets.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -19,11 +19,17 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net -import cats.effect.kernel.{Async, Resource} +import cats.effect.{Async, Resource} import cats.effect.syntax.all._ + +import com.comcast.ip4s.UnixSocketAddress + import fs2.io.file.Files + import java.nio.channels.SocketChannel import jnr.unixsocket.{ UnixServerSocketChannel, @@ -31,7 +37,7 @@ import jnr.unixsocket.{ UnixSocketChannel } -object JnrUnixSockets { +private[net] object JnrUnixSocketsProvider { lazy val supported: Boolean = try { @@ -41,21 +47,22 @@ object JnrUnixSockets { case _: ClassNotFoundException => false } - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - new JnrUnixSocketsImpl[F] + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + new JnrUnixSocketsProvider[F] - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = forAsyncAndFiles(F, Files.forAsync[F]) } -private[unixsocket] class JnrUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) - extends UnixSockets.AsyncUnixSockets[F] { +private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) + extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { + protected def openChannel(address: UnixSocketAddress) = Resource.make(F.blocking(UnixSocketChannel.open(new JnrUnixSocketAddress(address.path))))(ch => F.blocking(ch.close()) ) - protected def openServerChannel(address: UnixSocketAddress) = + protected def openServerChannel(address: UnixSocketAddress, options: List[SocketOption]) = Resource .make(F.blocking(UnixServerSocketChannel.open()))(ch => F.blocking(ch.close())) .evalTap { sch => @@ -63,9 +70,19 @@ private[unixsocket] class JnrUnixSocketsImpl[F[_]: Files](implicit F: Async[F]) .cancelable(F.blocking(sch.close())) } .map { sch => - Resource.makeFull[F, SocketChannel] { poll => - F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) - }(ch => F.blocking(ch.close())) + def raiseOptionError[A]: F[A] = + F.raiseError(new UnsupportedOperationException("JNR unix server sockets do not support socket options")) + val info: SocketInfo[F] = new SocketInfo[F] { + def supportedOptions = F.pure(Set.empty) + def getOption[A](key: SocketOption.Key[A]) = raiseOptionError + def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError + def localAddress = F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) + def localAddressGen = F.pure(address) + } + info -> + Resource.makeFull[F, SocketChannel] { poll => + F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) + }(ch => F.blocking(ch.close())) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } - } diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala new file mode 100644 index 0000000000..5986e2aa90 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, IO, LiftIO, Resource} +import cats.effect.std.Mutex +import cats.effect.syntax.all._ +import cats.syntax.all._ + +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress, UnixSocketAddress} + +import fs2.io.file.{Files, FileHandle, Path, SyncFileHandle} + +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel + +private[net] trait UnixSocketsProviderCompanionPlatform { + def forIO: UnixSocketsProvider[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + if (JdkUnixSocketsProvider.supported) JdkUnixSocketsProvider.forAsyncAndFiles + else if (JnrUnixSocketsProvider.supported) JnrUnixSocketsProvider.forAsyncAndFiles + else + throw new UnsupportedOperationException( + """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" + ) + + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync(F)) + + abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) + extends UnixSocketsProvider[F] { + + protected def openChannel(address: UnixSocketAddress): Resource[F, SocketChannel] + + protected def openServerChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] + + def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = + openChannel(address).evalMap(makeSocket[F](_)) + + def bind( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Bind[F]] = { + var deleteIfExists: Boolean = false + var deleteOnClose: Boolean = true + + val filteredOptions = options.filter { opt => + opt.key match { + case SocketOption.UnixServerSocketDeleteIfExists => + deleteIfExists = opt.value.asInstanceOf[java.lang.Boolean] + false + case SocketOption.UnixServerSocketDeleteOnClose => + deleteOnClose = opt.value.asInstanceOf[java.lang.Boolean] + false + case _ => true + } + } + + val delete = Resource.make { + Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) + } { _ => + Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) + } + + (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => + val clients0 = + Stream + .resource(accept.attempt) + .flatMap { + case Left(_) => Stream.empty[F] + case Right(accepted) => Stream.eval(makeSocket(accepted)) + } + .repeat + new UnsealedBind[F] { + def socketInfo = info + def clients = clients0 + } + } + } + } + + private def makeSocket[F[_]: Async]( + ch: SocketChannel + ): F[Socket[F]] = + (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => + new AsyncSocket[F](ch, readMutex, writeMutex) + } + + private final class AsyncSocket[F[_]]( + ch: SocketChannel, + readMutex: Mutex[F], + writeMutex: Mutex[F] + )(implicit F: Async[F]) + extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { + + protected def asyncInstance = F + protected def channel = ch + + def readChunk(buff: ByteBuffer): F[Int] = + evalOnVirtualThreadIfAvailable(F.blocking(ch.read(buff))) + .cancelable(close) + + def write(bytes: Chunk[Byte]): F[Unit] = { + def go(buff: ByteBuffer): F[Unit] = + F.blocking(ch.write(buff)).cancelable(close) *> + F.delay(buff.remaining <= 0).ifM(F.unit, go(buff)) + + writeMutex.lock.surround { + F.delay(bytes.toByteBuffer).flatMap(buffer => evalOnVirtualThreadIfAvailable(go(buffer))) + } + } + + override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + + def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + + def remoteAddressGen: F[GenSocketAddress] = ??? // TODO + + private def raiseIpAddressError[A]: F[A] = + F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) + + def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) + def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) + def endOfOutput: F[Unit] = + evalOnVirtualThreadIfAvailable( + F.blocking { + ch.shutdownOutput(); () + } + ) + def endOfInput: F[Unit] = + evalOnVirtualThreadIfAvailable( + F.blocking { + ch.shutdownInput(); () + } + ) + override def sendFile( + file: FileHandle[F], + offset: Long, + count: Long, + chunkSize: Int + ): Stream[F, Nothing] = file match { + case syncFileHandle: SyncFileHandle[F] => + val fileChannel = syncFileHandle.chan + + def go(currOffset: Long, remaining: Long): F[Unit] = + if (remaining <= 0) F.unit + else { + F.blocking(fileChannel.transferTo(currOffset, remaining, ch)).flatMap { written => + if (written == 0) F.unit + else { + go(currOffset + written, remaining - written) + } + } + } + + Stream.exec(writeMutex.lock.surround(go(offset, count))) + + case _ => + super.sendFile(file, offset, count, chunkSize) + } + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index cf6268661e..af776bb61a 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -20,21 +20,14 @@ */ package fs2.io.net.unixsocket -import fs2.io.file.SyncFileHandle -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.{Async, Resource} -import cats.effect.std.Mutex -import cats.effect.syntax.all._ -import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} -import fs2.{Chunk, Stream} -import fs2.io.file.{Files, Path} -import fs2.io.net.{Socket, SocketInfo} -import fs2.io.evalOnVirtualThreadIfAvailable -import java.nio.ByteBuffer -import java.nio.channels.SocketChannel -import fs2.io.file.FileHandle + +import cats.effect.{Async, IO, LiftIO, Resource} + +import com.comcast.ip4s.{UnixSocketAddress => Ip4sUnixSocketAddress} + +import fs2.Stream +import fs2.io.file.Files +import fs2.io.net.{Socket, SocketOption, UnixSocketsProvider} private[unixsocket] trait UnixSocketsCompanionPlatform { def forIO: UnixSockets[IO] = forLiftIO @@ -45,127 +38,28 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { } def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - if (JdkUnixSockets.supported) JdkUnixSockets.forAsyncAndFiles - else if (JnrUnixSockets.supported) JnrUnixSockets.forAsyncAndFiles - else - throw new UnsupportedOperationException( - """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" - ) + new AsyncUnixSockets[F] def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = forAsyncAndFiles(F, Files.forAsync(F)) - private[unixsocket] abstract class AsyncUnixSockets[F[_]: Files](implicit F: Async[F]) + private class AsyncUnixSockets[F[_]: Files](implicit F: Async[F]) extends UnixSockets[F] { - protected def openChannel(address: UnixSocketAddress): Resource[F, SocketChannel] - protected def openServerChannel( - address: UnixSocketAddress - ): Resource[F, Resource[F, SocketChannel]] + + private val delegate = UnixSocketsProvider.forAsyncAndFiles[F] def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - openChannel(address).evalMap(makeSocket[F](_)) + delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) def server( address: UnixSocketAddress, deleteIfExists: Boolean, deleteOnClose: Boolean - ): Stream[F, Socket[F]] = { - val delete = Resource.make { - Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) - } { _ => - Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) - } - - Stream.resource(delete *> openServerChannel(address)).flatMap { accept => - Stream - .resource(accept.attempt) - .flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(makeSocket(accepted)) - } - .repeat - } - } - } - - private def makeSocket[F[_]: Async]( - ch: SocketChannel - ): F[Socket[F]] = - (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => - new AsyncSocket[F](ch, readMutex, writeMutex) - } - - private final class AsyncSocket[F[_]]( - ch: SocketChannel, - readMutex: Mutex[F], - writeMutex: Mutex[F] - )(implicit F: Async[F]) - extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { - - protected def asyncInstance = F - protected def channel = ch - - def readChunk(buff: ByteBuffer): F[Int] = - evalOnVirtualThreadIfAvailable(F.blocking(ch.read(buff))) - .cancelable(close) - - def write(bytes: Chunk[Byte]): F[Unit] = { - def go(buff: ByteBuffer): F[Unit] = - F.blocking(ch.write(buff)).cancelable(close) *> - F.delay(buff.remaining <= 0).ifM(F.unit, go(buff)) - - writeMutex.lock.surround { - F.delay(bytes.toByteBuffer).flatMap(buffer => evalOnVirtualThreadIfAvailable(go(buffer))) - } - } - - override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - - def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - - def remoteAddressGen: F[GenSocketAddress] = ??? // TODO - - private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("UnixSockets do not use IP addressing")) - - def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) - def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) - def endOfOutput: F[Unit] = - evalOnVirtualThreadIfAvailable( - F.blocking { - ch.shutdownOutput(); () - } - ) - def endOfInput: F[Unit] = - evalOnVirtualThreadIfAvailable( - F.blocking { - ch.shutdownInput(); () - } - ) - override def sendFile( - file: FileHandle[F], - offset: Long, - count: Long, - chunkSize: Int - ): Stream[F, Nothing] = file match { - case syncFileHandle: SyncFileHandle[F] => - val fileChannel = syncFileHandle.chan - - def go(currOffset: Long, remaining: Long): F[Unit] = - if (remaining <= 0) F.unit - else { - F.blocking(fileChannel.transferTo(currOffset, remaining, ch)).flatMap { written => - if (written == 0) F.unit - else { - go(currOffset + written, remaining - written) - } - } - } - - Stream.exec(writeMutex.lock.surround(go(offset, count))) - - case _ => - super.sendFile(file, offset, count, chunkSize) - } + ): Stream[F, Socket[F]] = + Stream.resource( + delegate.bind(Ip4sUnixSocketAddress(address.path), + List(SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), + SocketOption.unixServerSocketDeleteOnClose(deleteOnClose))) + ).flatMap(_.clients) } } diff --git a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala new file mode 100644 index 0000000000..9c5a66f18c --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.Resource +import com.comcast.ip4s.UnixSocketAddress + +private[net] trait UnixSocketsProvider[F[_]] { + + def connect( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] + + def bind( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Bind[F]]} + +private[net] object UnixSocketsProvider extends UnixSocketsProviderCompanionPlatform From 5e912ab3de910d0a9b3ea1bcad101592dc607571 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 08:06:39 -0400 Subject: [PATCH 09/79] Drop JVM socket group implementations --- .../fs2/io/net/SocketGroupPlatform.scala | 148 +--------------- ...hronousChannelGroupIpSocketsProvider.scala | 9 +- .../scala/fs2/io/net/NetworkPlatform.scala | 22 +-- .../fs2/io/net/SelectingSocketGroup.scala | 161 ------------------ .../src/main/scala/fs2/io/net/Network.scala | 7 +- .../main/scala/fs2/io/net/SocketGroup.scala | 5 - 6 files changed, 22 insertions(+), 330 deletions(-) delete mode 100644 io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 39a30b2918..1cfe3d29d9 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -23,152 +23,20 @@ package fs2 package io package net -import java.net.InetSocketAddress -import java.nio.channels.{ - AsynchronousCloseException, - AsynchronousServerSocketChannel, - AsynchronousSocketChannel, - CompletionHandler -} -import java.nio.channels.AsynchronousChannelGroup import cats.syntax.all._ import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} +import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - private[fs2] def unsafe[F[_]: Async: Dns]( - channelGroup: AsynchronousChannelGroup - ): SocketGroup[F] = - new AsyncSocketGroup[F](channelGroup) - - private final class AsyncSocketGroup[F[_]: Async: Dns](channelGroup: AsynchronousChannelGroup) - extends AbstractAsyncSocketGroup[F] { - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = { - def setup: Resource[F, AsynchronousSocketChannel] = - Resource - .make( - Async[F].delay( - AsynchronousSocketChannel.open(channelGroup) - ) - )(ch => Async[F].delay(if (ch.isOpen) ch.close else ())) - .evalTap(ch => Async[F].delay(options.foreach(opt => ch.setOption(opt.key, opt.value)))) - - def connect(ch: AsynchronousSocketChannel): F[AsynchronousSocketChannel] = - to.resolve[F].flatMap { ip => - Async[F].async[AsynchronousSocketChannel] { cb => - Async[F] - .delay { - ch.connect( - ip.toInetSocketAddress, - null, - new CompletionHandler[Void, Void] { - def completed(result: Void, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(Async[F].delay(ch.close()))) - } - } - - setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) - } - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => - bound.socketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } - } - def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Bind[F]] = { + def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = new SocketGroup[F] { + def client(to: SocketAddress[Host], options: List[SocketOption]) = + ipSockets.connect(to, options) - val setup: Resource[F, AsynchronousServerSocketChannel] = - Resource.eval(address.host.resolve[F]).flatMap { addr => - Resource - .make( - Async[F].delay( - AsynchronousServerSocketChannel.open(channelGroup) - ) - )(sch => Async[F].delay(if (sch.isOpen) sch.close())) - .evalTap(ch => - Async[F].delay( - ch.bind( - new InetSocketAddress( - if (addr.isWildcard) null else addr.toInetAddress, - address.port.value - ) - ) - ) - ) - } + def server(address: Option[Host], port: Option[Port], options: List[SocketOption]): Stream[F, Socket[F]] = + Stream.resource(serverResource(address, port, options)).flatMap(_._2) - def acceptIncoming( - sch: AsynchronousServerSocketChannel - ): Stream[F, Socket[F]] = { - def go: Stream[F, Socket[F]] = { - def acceptChannel = Resource.makeFull[F, AsynchronousSocketChannel] { poll => - poll { - Async[F].async[AsynchronousSocketChannel] { cb => - Async[F] - .delay { - sch.accept( - null, - new CompletionHandler[AsynchronousSocketChannel, Void] { - def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(Async[F].delay(sch.close()))) - } - } - }(ch => Async[F].delay(if (ch.isOpen) ch.close else ())) - - def setOpts(ch: AsynchronousSocketChannel) = - Async[F].delay { - options.foreach(o => ch.setOption(o.key, o.value)) - } - - Stream.resource(acceptChannel.attempt).flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(setOpts(accepted) *> Socket.forAsync(accepted)) - } ++ go - } - - go.handleErrorWith { - case err: AsynchronousCloseException => - Stream.eval(Async[F].delay(sch.isOpen)).flatMap { isOpen => - if (isOpen) Stream.raiseError[F](err) - else Stream.empty - } - case err => Stream.raiseError[F](err) - } - } - - setup.map { sch => - // val jLocalAddress = sch.getLocalAddress.asInstanceOf[java.net.InetSocketAddress] - // val localAddress = SocketAddress.fromInetSocketAddress(jLocalAddress) - // (localAddress, acceptIncoming(sch)) - new UnsealedBind[F] { - def socketInfo = SocketInfo.forAsync(sch) - def clients = acceptIncoming(sch) - } - } - } + def serverResource(address: Option[Host], port: Option[Port], options: List[SocketOption]): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.socketInfo.localAddress.tupleRight(b.clients)) } - } diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 6df5af0e30..d064bbf79e 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -41,9 +41,7 @@ import fs2.internal.ThreadFactories private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( channelGroup: AsynchronousChannelGroup -)(implicit F: Async[F]) extends IpSocketsProvider[F] { - - private implicit val dns: Dns[F] = Dns.forAsync[F] +)(implicit F: Async[F], F2: Dns[F]) extends IpSocketsProvider[F] { override def connect( address: SocketAddress[Host], @@ -166,6 +164,9 @@ private[net] object AsynchronousChannelGroupIpSocketsProvider { ThreadFactories.named("fs2-global-tcp", true) ) - def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = + def forAsyncAndDns[F[_]: Async: Dns]: AsynchronousChannelGroupIpSocketsProvider[F] = new AsynchronousChannelGroupIpSocketsProvider[F](globalAcg) + + def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = + forAsyncAndDns(Async[F], Dns.forAsync[F]) } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index c1c4610b62..86d8a6abde 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -29,13 +29,11 @@ import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, Port, SocketAddress, UnixSocketAddress} import fs2.internal.ThreadFactories -import fs2.io.net.tls.TLSContext import java.net.ProtocolFamily -import java.nio.channels.AsynchronousChannelGroup import java.util.concurrent.ThreadFactory private[net] trait NetworkPlatform[F[_]] { @@ -50,6 +48,7 @@ private[net] trait NetworkPlatform[F[_]] { * @param threadCount number of threads to allocate in the fixed thread pool backing the NIO channel group * @param threadFactory factory used to create fixed threads */ + @deprecated("3.13.0", "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead") def socketGroup( threadCount: Int = 1, threadFactory: ThreadFactory = ThreadFactories.named("fs2-tcp", true) @@ -104,7 +103,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Socket[F]] = matchAddress(address, sa => selecting(_.connect(sa, options), fallback.connect(sa, options)), - ua => fallback.connect(address, options)) + ua => fallback.connect(ua, options)) def bind( address: GenSocketAddress, @@ -112,7 +111,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N ): Resource[F, Bind[F]] = matchAddress(address, sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), - ua => fallback.bind(address, options)) + ua => fallback.bind(ua, options)) def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = fallback.datagramSocketGroup(threadFactory) @@ -127,9 +126,10 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N // Implementations of deprecated operations + @deprecated("3.13.0", "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead") def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = Resource.eval(tryGetSelector).flatMap { - case Some(selector) => Resource.pure(new SelectingSocketGroup[F](selector)) + case Some(selector) => Resource.pure(SocketGroup.fromIpSockets(new SelectingIpSocketsProvider(selector))) case None => fallback.socketGroup(threadCount, threadFactory) } } @@ -176,14 +176,8 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N // Implementations of deprecated operations - // TODO adapt SocketGroup to IpSocketsProvider and make this a Resource.pure def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = - Resource - .make( - F.delay( - AsynchronousChannelGroup.withFixedThreadPool(threadCount, threadFactory) - ) - )(acg => F.delay(acg.shutdown())) - .map(SocketGroup.unsafe[F](_)) + Resource.pure(SocketGroup.fromIpSockets(ipSockets)) } } + diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala deleted file mode 100644 index f6ca5fd6f1..0000000000 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocketGroup.scala +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io.net - -import cats.effect.LiftIO -import cats.effect.Selector -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.syntax.all._ -import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} - -import java.net.InetSocketAddress -import java.nio.channels.AsynchronousCloseException -import java.nio.channels.ClosedChannelException -import java.nio.channels.SelectionKey.OP_ACCEPT -import java.nio.channels.SelectionKey.OP_CONNECT -import java.nio.channels.SocketChannel - -private final class SelectingSocketGroup[F[_]: LiftIO: Dns](selector: Selector)(implicit - F: Async[F] -) extends SocketGroup[F] { - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - Resource - .make(F.delay(selector.provider.openSocketChannel())) { ch => - F.delay(ch.close()) - } - .evalMap { ch => - val configure = F.delay { - ch.configureBlocking(false) - options.foreach(opt => ch.setOption(opt.key, opt.value)) - } - - val connect = to.resolve.flatMap { ip => - F.delay(ch.connect(ip.toInetSocketAddress)).flatMap { connected => - selector - .select(ch, OP_CONNECT) - .to - .untilM_(F.delay(ch.finishConnect())) - .unlessA(connected) - } - } - - val make = SelectingSocket[F]( - selector, - ch, - remoteAddress(ch) - ) - - configure *> connect *> make - } - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream - .resource( - serverResource( - address, - port, - options - ) - ) - .flatMap { case (_, clients) => clients } - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap { bound => - bound.socketInfo.localAddress.map { case addr: SocketAddress[IpAddress] => (addr, bound.clients) } - } - - - def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Bind[F]] = - Resource - .make(F.delay(selector.provider.openServerSocketChannel())) { ch => - F.delay(ch.close()) - } - .evalMap { serverCh => - val configure = address.host.resolve.flatMap { addr => - F.delay { - serverCh.configureBlocking(false) - serverCh.bind( - new InetSocketAddress( - if (addr.isWildcard) null else addr.toInetAddress, - address.port.value - ) - ) - } - } - - def acceptLoop: Stream[F, SocketChannel] = Stream - .bracketFull[F, SocketChannel] { poll => - def go: F[SocketChannel] = - F.delay(serverCh.accept()).flatMap { - case null => poll(selector.select(serverCh, OP_ACCEPT).to) *> go - case ch => F.pure(ch) - } - go - }((ch, _) => F.delay(ch.close())) - .repeat - .handleErrorWith { - case _: AsynchronousCloseException | _: ClosedChannelException => acceptLoop - case ex => Stream.raiseError(ex) - } - - val clients0 = acceptLoop.evalMap { ch => - F.delay { - ch.configureBlocking(false) - options.foreach(opt => ch.setOption(opt.key, opt.value)) - } *> SelectingSocket[F]( - selector, - ch, - remoteAddress(ch) - ) - } - - configure.as(new UnsealedBind[F] { - def socketInfo = SocketInfo.forAsync(serverCh) - def clients = clients0 - }) - } - - private def remoteAddress(ch: SocketChannel) = - F.delay { - SocketAddress.fromInetSocketAddress( - ch.getRemoteAddress.asInstanceOf[InetSocketAddress] - ) - } - -} diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 6931fc8bef..48b422fbcb 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -98,13 +98,8 @@ object Network extends NetworkCompanionPlatform { port: Option[Port], options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - serverBound(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) + bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) .flatMap(b => Resource.eval(b.socketInfo.localAddress).map(a => (a, b.clients))) - - override def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Bind[F]] = bind(address, options) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 1128776123..016609a129 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -69,11 +69,6 @@ trait SocketGroup[F[_]] { port: Option[Port] = None, options: List[SocketOption] = List.empty ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] - - def serverBound( - address: SocketAddress[Host], - options: List[SocketOption] = Nil - ): Resource[F, Bind[F]] } private[net] object SocketGroup extends SocketGroupCompanionPlatform { From 7081b7a53af4ccc1eb4b386e86b5f064c162b60b Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 08:20:55 -0400 Subject: [PATCH 10/79] Unix socket tests --- .../{unixsocket => }/UnixSocketsSuitePlatform.scala | 6 +++--- .../io/net/{unixsocket => }/UnixSocketsSuite.scala | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) rename io/jvm/src/test/scala/fs2/io/net/{unixsocket => }/UnixSocketsSuitePlatform.scala (85%) rename io/shared/src/test/scala/fs2/io/net/{unixsocket => }/UnixSocketsSuite.scala (88%) diff --git a/io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 85% rename from io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala rename to io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index 0fffed5479..abf67a4593 100644 --- a/io/jvm/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuitePlatform.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,11 +20,11 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - if (JdkUnixSockets.supported) testProvider("jdk")(JdkUnixSockets.forAsync[IO]) - if (JnrUnixSockets.supported) testProvider("jnr")(JnrUnixSockets.forAsync[IO]) + if (JdkUnixSocketsProvider.supported) testProvider("jdk")(JdkUnixSocketsProvider.forAsync[IO]) + if (JnrUnixSocketsProvider.supported) testProvider("jnr")(JnrUnixSocketsProvider.forAsync[IO]) } diff --git a/io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala similarity index 88% rename from io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala rename to io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 09fe206d5b..020bbb9315 100644 --- a/io/shared/src/test/scala/fs2/io/net/unixsocket/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -20,26 +20,27 @@ */ package fs2 -package io.net.unixsocket +package io.net import scala.concurrent.duration._ import cats.effect.IO +import com.comcast.ip4s.UnixSocketAddress class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { - def testProvider(provider: String)(implicit sockets: UnixSockets[IO]) = + def testProvider(provider: String)(implicit sockets: UnixSocketsProvider[IO]) = test(s"echoes - $provider") { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") - val server = UnixSockets[IO] - .server(address) + val server = Stream.resource(sockets.bind(address, Nil)) + .flatMap(_.clients) .map { client => client.reads.through(client.writes) } .parJoinUnbounded - def client(msg: Chunk[Byte]) = UnixSockets[IO].client(address).use { server => + def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { server => server.write(msg) *> server.endOfOutput *> server.reads.compile .to(Chunk) .map(read => assertEquals(read, msg)) From dedda555f1c803155062fd5fd77dea0a1317f905 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 08:22:31 -0400 Subject: [PATCH 11/79] Unix socket tests --- .../src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala | 4 ++-- io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index abf67a4593..818f56fa59 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -25,6 +25,6 @@ package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - if (JdkUnixSocketsProvider.supported) testProvider("jdk")(JdkUnixSocketsProvider.forAsync[IO]) - if (JnrUnixSocketsProvider.supported) testProvider("jnr")(JnrUnixSocketsProvider.forAsync[IO]) + if (JdkUnixSocketsProvider.supported) testProvider("jdk", JdkUnixSocketsProvider.forAsync[IO]) + if (JnrUnixSocketsProvider.supported) testProvider("jnr", JnrUnixSocketsProvider.forAsync[IO]) } diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 020bbb9315..241280afc4 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -29,7 +29,7 @@ import com.comcast.ip4s.UnixSocketAddress class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { - def testProvider(provider: String)(implicit sockets: UnixSocketsProvider[IO]) = + protected def testProvider(provider: String, sockets: UnixSocketsProvider[IO]) = test(s"echoes - $provider") { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") From 17ade5c442ec5358aeaf823d9bb15989a524a5b7 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 08:35:30 -0400 Subject: [PATCH 12/79] Cleanup Bind --- .../fs2/io/net/SocketGroupPlatform.scala | 2 +- ...hronousChannelGroupIpSocketsProvider.scala | 9 ++----- .../io/net/SelectingIpSocketsProvider.scala | 7 ++--- .../io/net/UnixSocketsProviderPlatform.scala | 7 ++--- .../net/unixsocket/UnixSocketsPlatform.scala | 2 +- .../src/main/scala/fs2/io/net/Bind.scala | 26 +++++++++++++++++-- .../src/main/scala/fs2/io/net/Network.scala | 5 ++-- .../scala/fs2/io/net/UnixSocketsSuite.scala | 2 +- 8 files changed, 36 insertions(+), 24 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 1cfe3d29d9..9af1831b8f 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -37,6 +37,6 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => Stream.resource(serverResource(address, port, options)).flatMap(_._2) def serverResource(address: Option[Host], port: Option[Port], options: List[SocketOption]): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.socketInfo.localAddress.tupleRight(b.clients)) + ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.socketInfo.localAddress.tupleRight(b.accept)) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index d064bbf79e..736c1d680b 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -35,7 +35,7 @@ import java.nio.channels.AsynchronousChannelGroup import cats.syntax.all._ import cats.effect.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Ipv4Address, Port, SocketAddress} +import com.comcast.ip4s.{Dns, Host, SocketAddress} import fs2.internal.ThreadFactories @@ -149,12 +149,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( } } - setup.map { sch => - new UnsealedBind[F] { - def socketInfo = SocketInfo.forAsync(sch) - def clients = acceptIncoming(sch) - } - } + setup.map(sch => Bind(SocketInfo.forAsync(sch), acceptIncoming(sch))) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index 0813bb6a34..66a099b7ab 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -109,7 +109,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici case ex => Stream.raiseError(ex) } - val clients0 = acceptLoop.evalMap { ch => + val accept = acceptLoop.evalMap { ch => F.delay { ch.configureBlocking(false) options.foreach(opt => ch.setOption(opt.key, opt.value)) @@ -120,10 +120,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici ) } - configure.as(new UnsealedBind[F] { - def socketInfo = SocketInfo.forAsync(serverCh) - def clients = clients0 - }) + configure.as(Bind(SocketInfo.forAsync(serverCh), accept)) } private def remoteAddress(ch: SocketChannel) = diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 5986e2aa90..b01a55659f 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -93,7 +93,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { } (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => - val clients0 = + val acceptIncoming = Stream .resource(accept.attempt) .flatMap { @@ -101,10 +101,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { case Right(accepted) => Stream.eval(makeSocket(accepted)) } .repeat - new UnsealedBind[F] { - def socketInfo = info - def clients = clients0 - } + Bind(info, acceptIncoming) } } } diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index af776bb61a..f2f595563c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -60,6 +60,6 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { delegate.bind(Ip4sUnixSocketAddress(address.path), List(SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), SocketOption.unixServerSocketDeleteOnClose(deleteOnClose))) - ).flatMap(_.clients) + ).flatMap(_.accept) } } diff --git a/io/shared/src/main/scala/fs2/io/net/Bind.scala b/io/shared/src/main/scala/fs2/io/net/Bind.scala index afe144eac4..427a45ed9c 100644 --- a/io/shared/src/main/scala/fs2/io/net/Bind.scala +++ b/io/shared/src/main/scala/fs2/io/net/Bind.scala @@ -23,9 +23,31 @@ package fs2 package io package net +/** Represents a bound TCP server socket. + * + * The socket can be inspected, e.g. for its bound port, via the `socketInfo` method. + * Note some platforms do not support getting and setting socket options on server sockets + * so take care when using `socketInfo`. + * + * Client sockets can be accepted by pulling on the `accept` stream. A concurrent server + * that is limited to `maxAccept` concurrent clients is accomplished by + * `b.accept.map(handleClientSocket).parJoin(maxAccept)`. + */ sealed trait Bind[F[_]] { + /** Get information about the bound server socket. */ def socketInfo: SocketInfo[F] - def clients: Stream[F, Socket[F]] + + /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ + def accept: Stream[F, Socket[F]] } -private[net] trait UnsealedBind[F[_]] extends Bind[F] +object Bind { + private[net] def apply[F[_]](socketInfo: SocketInfo[F], accept: Stream[F, Socket[F]]): Bind[F] = { + val socketInfo0 = socketInfo + val accept0 = accept + new Bind[F] { + val socketInfo = socketInfo0 + val accept = accept0 + } + } +} diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 48b422fbcb..a1b7d9c417 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -24,6 +24,7 @@ package io package net import cats.effect.{Async, Resource} +import cats.syntax.all.* import com.comcast.ip4s.{GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress} import fs2.io.net.tls.TLSContext @@ -76,7 +77,7 @@ object Network extends NetworkCompanionPlatform { override def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Bind[F]] override def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = - Stream.resource(bind(address, options)).flatMap(_.clients) + Stream.resource(bind(address, options)).flatMap(_.accept) override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] @@ -99,7 +100,7 @@ object Network extends NetworkCompanionPlatform { options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) - .flatMap(b => Resource.eval(b.socketInfo.localAddress).map(a => (a, b.clients))) + .flatMap(b => Resource.eval(b.socketInfo.localAddress).tupleRight(b.accept)) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 241280afc4..e75e3d0345 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -34,7 +34,7 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") val server = Stream.resource(sockets.bind(address, Nil)) - .flatMap(_.clients) + .flatMap(_.accept) .map { client => client.reads.through(client.writes) } From e2fafadc046fe0e621c3c73666dc61cfdfba94f8 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 16:28:46 -0400 Subject: [PATCH 13/79] s/Bind/ServerSocket/ --- .../fs2/io/net/SocketGroupPlatform.scala | 2 +- ...hronousChannelGroupIpSocketsProvider.scala | 4 +- .../scala/fs2/io/net/NetworkPlatform.scala | 4 +- .../io/net/SelectingIpSocketsProvider.scala | 4 +- .../fs2/io/net/ServerSocketPlatform.scala | 41 +++++++++++++++++++ .../io/net/UnixSocketsProviderPlatform.scala | 4 +- .../scala/fs2/io/net/IpSocketsProvider.scala | 2 +- .../src/main/scala/fs2/io/net/Network.scala | 6 +-- .../io/net/{Bind.scala => ServerSocket.scala} | 21 +++------- .../fs2/io/net/UnixSocketsProvider.scala | 2 +- 10 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala rename io/shared/src/main/scala/fs2/io/net/{Bind.scala => ServerSocket.scala} (76%) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 9af1831b8f..43f07ed3a8 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -37,6 +37,6 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => Stream.resource(serverResource(address, port, options)).flatMap(_._2) def serverResource(address: Option[Host], port: Option[Port], options: List[SocketOption]): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.socketInfo.localAddress.tupleRight(b.accept)) + ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.localAddress.tupleRight(b.accept)) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 736c1d680b..82909cc0cb 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -82,7 +82,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( override def bind( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, Bind[F]] = { + ): Resource[F, ServerSocket[F]] = { val setup: Resource[F, AsynchronousServerSocketChannel] = @@ -149,7 +149,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( } } - setup.map(sch => Bind(SocketInfo.forAsync(sch), acceptIncoming(sch))) + setup.map(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 86d8a6abde..5a8a2692a4 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -108,7 +108,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def bind( address: GenSocketAddress, options: List[SocketOption] - ): Resource[F, Bind[F]] = + ): Resource[F, ServerSocket[F]] = matchAddress(address, sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), ua => fallback.bind(ua, options)) @@ -154,7 +154,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def bind( address: GenSocketAddress, options: List[SocketOption] - ): Resource[F, Bind[F]] = + ): Resource[F, ServerSocket[F]] = matchAddress(address, sa => ipSockets.bind(sa, options), ua => unixSockets.bind(ua, options)) diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index 66a099b7ab..a2a348fbe7 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -76,7 +76,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici def bind( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, Bind[F]] = + ): Resource[F, ServerSocket[F]] = Resource .make(F.delay(selector.provider.openServerSocketChannel())) { ch => F.delay(ch.close()) @@ -120,7 +120,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici ) } - configure.as(Bind(SocketInfo.forAsync(serverCh), accept)) + configure.as(ServerSocket(SocketInfo.forAsync(serverCh), accept)) } private def remoteAddress(ch: SocketChannel) = diff --git a/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala new file mode 100644 index 0000000000..df6984649e --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +private[net] trait ServerSocketCompanionPlatform { + private[net] def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { + val accept0 = accept + new UnsealedServerSocket[F] { + def accept: Stream[F, Socket[F]] = accept0 + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) + def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) + def supportedOptions = info.supportedOptions + + def localAddress = info.localAddress + def localAddressGen = info.localAddressGen + } + } +} + diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index b01a55659f..30a57cf69b 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -70,7 +70,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { def bind( address: UnixSocketAddress, options: List[SocketOption] - ): Resource[F, Bind[F]] = { + ): Resource[F, ServerSocket[F]] = { var deleteIfExists: Boolean = false var deleteOnClose: Boolean = true @@ -101,7 +101,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { case Right(accepted) => Stream.eval(makeSocket(accepted)) } .repeat - Bind(info, acceptIncoming) + ServerSocket(info, acceptIncoming) } } } diff --git a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala index 0071631d25..c006371595 100644 --- a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -36,5 +36,5 @@ private[net] trait IpSocketsProvider[F[_]] { def bind( address: SocketAddress[Host], options: List[SocketOption] - ): Resource[F, Bind[F]] + ): Resource[F, ServerSocket[F]] } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index a1b7d9c417..a584fd8caa 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -56,7 +56,7 @@ sealed trait Network[F[_]] def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] - def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Bind[F]] + def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, ServerSocket[F]] def bindAndAccept(address: GenSocketAddress, options: List[SocketOption] = Nil): Stream[F, Socket[F]] @@ -74,7 +74,7 @@ object Network extends NetworkCompanionPlatform { override def connect(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] - override def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Bind[F]] + override def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, ServerSocket[F]] override def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = Stream.resource(bind(address, options)).flatMap(_.accept) @@ -100,7 +100,7 @@ object Network extends NetworkCompanionPlatform { options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) - .flatMap(b => Resource.eval(b.socketInfo.localAddress).tupleRight(b.accept)) + .flatMap(b => Resource.eval(b.localAddress).tupleRight(b.accept)) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/main/scala/fs2/io/net/Bind.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala similarity index 76% rename from io/shared/src/main/scala/fs2/io/net/Bind.scala rename to io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 427a45ed9c..4bb9f5e95a 100644 --- a/io/shared/src/main/scala/fs2/io/net/Bind.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -25,29 +25,20 @@ package net /** Represents a bound TCP server socket. * - * The socket can be inspected, e.g. for its bound port, via the `socketInfo` method. * Note some platforms do not support getting and setting socket options on server sockets - * so take care when using `socketInfo`. + * so take care when using `getOption` and `setOption`. * * Client sockets can be accepted by pulling on the `accept` stream. A concurrent server * that is limited to `maxAccept` concurrent clients is accomplished by * `b.accept.map(handleClientSocket).parJoin(maxAccept)`. */ -sealed trait Bind[F[_]] { - /** Get information about the bound server socket. */ - def socketInfo: SocketInfo[F] +sealed trait ServerSocket[F[_]] extends SocketInfo[F] { /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ def accept: Stream[F, Socket[F]] } -object Bind { - private[net] def apply[F[_]](socketInfo: SocketInfo[F], accept: Stream[F, Socket[F]]): Bind[F] = { - val socketInfo0 = socketInfo - val accept0 = accept - new Bind[F] { - val socketInfo = socketInfo0 - val accept = accept0 - } - } -} +private[net] trait UnsealedServerSocket[F[_]] extends ServerSocket[F] + +object ServerSocket extends ServerSocketCompanionPlatform + diff --git a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala index 9c5a66f18c..d9da6ebc7e 100644 --- a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala @@ -36,6 +36,6 @@ private[net] trait UnixSocketsProvider[F[_]] { def bind( address: UnixSocketAddress, options: List[SocketOption] - ): Resource[F, Bind[F]]} + ): Resource[F, ServerSocket[F]]} private[net] object UnixSocketsProvider extends UnixSocketsProviderCompanionPlatform From fad0e045e4b2c02b08d4c4fce5f5204c5f1378ac Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 10 Apr 2025 16:52:16 -0400 Subject: [PATCH 14/79] Push old localAddress back down to Socket --- .../src/main/scala/fs2/io/net/SocketGroupPlatform.scala | 2 +- .../src/main/scala/fs2/io/net/SocketInfoPlatform.scala | 7 ------- .../src/main/scala/fs2/io/net/SocketPlatform.scala | 7 +++++++ .../main/scala/fs2/io/net/JnrUnixSocketsProvider.scala | 1 - io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala | 9 +++++++++ .../src/main/scala/fs2/io/net/ServerSocketPlatform.scala | 1 - io/shared/src/main/scala/fs2/io/net/Network.scala | 2 +- io/shared/src/main/scala/fs2/io/net/Socket.scala | 2 ++ io/shared/src/main/scala/fs2/io/net/SocketInfo.scala | 4 +--- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 43f07ed3a8..bce0d38a24 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -37,6 +37,6 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => Stream.resource(serverResource(address, port, options)).flatMap(_._2) def serverResource(address: Option[Host], port: Option[Port], options: List[SocketOption]): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.localAddress.tupleRight(b.accept)) + ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept)) } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index cea76c130d..8c093fd8b6 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -43,13 +43,6 @@ private[net] trait SocketInfoCompanionPlatform { implicit protected def asyncInstance: Async[F] protected def channel: NetworkChannel - override def localAddress: F[SocketAddress[IpAddress]] = - asyncInstance.delay( - SocketAddress.fromInetSocketAddress( - channel.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - ) - override def localAddressGen: F[GenSocketAddress] = asyncInstance.delay( channel.getLocalAddress match { diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index 616cd6dc0e..51210ffc90 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -142,6 +142,13 @@ private[net] trait SocketCompanionPlatform { } } + override def localAddress: F[SocketAddress[IpAddress]] = + asyncInstance.delay( + SocketAddress.fromInetSocketAddress( + channel.getLocalAddress.asInstanceOf[InetSocketAddress] + ) + ) + def remoteAddress: F[SocketAddress[IpAddress]] = F.delay( SocketAddress.fromInetSocketAddress( diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index be1e79bee3..90ef2d62c7 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -76,7 +76,6 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ def supportedOptions = F.pure(Set.empty) def getOption[A](key: SocketOption.Key[A]) = raiseOptionError def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError - def localAddress = F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) def localAddressGen = F.pure(address) } info -> diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala index c144839e16..1e0ba9fa76 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala @@ -31,6 +31,7 @@ import cats.effect.std.Mutex import cats.syntax.all._ import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SelectionKey.OP_READ import java.nio.channels.SelectionKey.OP_WRITE @@ -48,6 +49,14 @@ private final class SelectingSocket[F[_]: LiftIO] private ( protected def asyncInstance = F protected def channel = ch + + override def localAddress: F[SocketAddress[IpAddress]] = + asyncInstance.delay( + SocketAddress.fromInetSocketAddress( + ch.getLocalAddress.asInstanceOf[InetSocketAddress] + ) + ) + def remoteAddressGen: F[GenSocketAddress] = remoteAddress.map(a => a: GenSocketAddress) diff --git a/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala index df6984649e..415ed0f290 100644 --- a/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala @@ -33,7 +33,6 @@ private[net] trait ServerSocketCompanionPlatform { def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) def supportedOptions = info.supportedOptions - def localAddress = info.localAddress def localAddressGen = info.localAddressGen } } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index a584fd8caa..0e4319aac4 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -100,7 +100,7 @@ object Network extends NetworkCompanionPlatform { options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) - .flatMap(b => Resource.eval(b.localAddress).tupleRight(b.accept)) + .flatMap(b => Resource.eval(b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]])).tupleRight(b.accept)) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index e6120ae958..c6fd098168 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -55,6 +55,8 @@ trait Socket[F[_]] extends SocketInfo[F] { def isOpen: F[Boolean] + def localAddress: F[SocketAddress[IpAddress]] + /** Asks for the remote address of the peer. */ def remoteAddress: F[SocketAddress[IpAddress]] diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index d07146ea58..8d9b23d3b1 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -23,15 +23,13 @@ package fs2 package io package net -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import com.comcast.ip4s.GenSocketAddress trait SocketInfo[F[_]] { /** Asks for the local address of the socket. */ def localAddressGen: F[GenSocketAddress] - def localAddress: F[SocketAddress[IpAddress]] - def supportedOptions: F[Set[SocketOption.Key[_]]] def getOption[A](key: SocketOption.Key[A]): F[Option[A]] From 598d3ed4789b06c634051a14a26e62fc3703f5be Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 11 Apr 2025 17:07:24 -0400 Subject: [PATCH 15/79] Native --- .../scala/fs2/io/net/NetworkLowPriority.scala | 0 ...hronousChannelGroupIpSocketsProvider.scala | 8 +- .../fs2/io/net/ServerSocketPlatform.scala | 0 .../scala/fs2/io/net/SocketInfoPlatform.scala | 2 +- .../net/unixsocket/UnixSocketsPlatform.scala | 65 ------------- .../scala/fs2/io/internal/SocketHelpers.scala | 47 ++++++++++ ...scala => FdPollingIpSocketsProvider.scala} | 44 +++++---- .../scala/fs2/io/net/FdPollingSocket.scala | 21 +++-- ...ala => FdPollingUnixSocketsProvider.scala} | 94 ++++++++++++------- .../scala/fs2/io/net/NetworkLowPriority.scala | 33 +++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 82 ++++++---------- ...cala => UnixSocketsProviderPlatform.scala} | 13 +-- .../fs2/io/net/tls/TLSSocketPlatform.scala | 21 ++++- .../UnixSocketsSuitePlatform.scala | 4 +- .../fs2/io/net/unixsocket/UnixSockets.scala | 33 ++++++- 15 files changed, 260 insertions(+), 207 deletions(-) rename io/{shared => js-jvm}/src/main/scala/fs2/io/net/NetworkLowPriority.scala (100%) rename io/{jvm => jvm-native}/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala (95%) rename io/{jvm => jvm-native}/src/main/scala/fs2/io/net/ServerSocketPlatform.scala (100%) delete mode 100644 io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala rename io/native/src/main/scala/fs2/io/net/{FdPollingSocketGroup.scala => FdPollingIpSocketsProvider.scala} (81%) rename io/native/src/main/scala/fs2/io/net/{unixsocket/FdPollingUnixSockets.scala => FdPollingUnixSocketsProvider.scala} (63%) create mode 100644 io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala rename io/native/src/main/scala/fs2/io/net/{unixsocket/UnixSocketsPlatform.scala => UnixSocketsProviderPlatform.scala} (82%) rename io/native/src/test/scala/fs2/io/net/{unixsockets => }/UnixSocketsSuitePlatform.scala (94%) diff --git a/io/shared/src/main/scala/fs2/io/net/NetworkLowPriority.scala b/io/js-jvm/src/main/scala/fs2/io/net/NetworkLowPriority.scala similarity index 100% rename from io/shared/src/main/scala/fs2/io/net/NetworkLowPriority.scala rename to io/js-jvm/src/main/scala/fs2/io/net/NetworkLowPriority.scala diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala similarity index 95% rename from io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala rename to io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 82909cc0cb..ac6a4e475c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -37,8 +37,6 @@ import cats.syntax.all._ import cats.effect.{Async, Resource} import com.comcast.ip4s.{Dns, Host, SocketAddress} -import fs2.internal.ThreadFactories - private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( channelGroup: AsynchronousChannelGroup )(implicit F: Async[F], F2: Dns[F]) extends IpSocketsProvider[F] { @@ -154,13 +152,9 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( } private[net] object AsynchronousChannelGroupIpSocketsProvider { - private lazy val globalAcg = AsynchronousChannelGroup.withFixedThreadPool( - 1, - ThreadFactories.named("fs2-global-tcp", true) - ) def forAsyncAndDns[F[_]: Async: Dns]: AsynchronousChannelGroupIpSocketsProvider[F] = - new AsynchronousChannelGroupIpSocketsProvider[F](globalAcg) + new AsynchronousChannelGroupIpSocketsProvider[F](null) def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = forAsyncAndDns(Async[F], Dns.forAsync[F]) diff --git a/io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala similarity index 100% rename from io/jvm/src/main/scala/fs2/io/net/ServerSocketPlatform.scala rename to io/jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 8c093fd8b6..966bf10db3 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, SocketAddress} import cats.effect.Async import java.net.InetSocketAddress diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala deleted file mode 100644 index f2f595563c..0000000000 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2.io.net.unixsocket - -import cats.effect.{Async, IO, LiftIO, Resource} - -import com.comcast.ip4s.{UnixSocketAddress => Ip4sUnixSocketAddress} - -import fs2.Stream -import fs2.io.file.Files -import fs2.io.net.{Socket, SocketOption, UnixSocketsProvider} - -private[unixsocket] trait UnixSocketsCompanionPlatform { - def forIO: UnixSockets[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { - val _ = LiftIO[F] - forAsyncAndFiles - } - - def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = - new AsyncUnixSockets[F] - - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = - forAsyncAndFiles(F, Files.forAsync(F)) - - private class AsyncUnixSockets[F[_]: Files](implicit F: Async[F]) - extends UnixSockets[F] { - - private val delegate = UnixSocketsProvider.forAsyncAndFiles[F] - - def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) - - def server( - address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): Stream[F, Socket[F]] = - Stream.resource( - delegate.bind(Ip4sUnixSocketAddress(address.path), - List(SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), - SocketOption.unixServerSocketDeleteOnClose(deleteOnClose))) - ).flatMap(_.accept) - } -} diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index a1d8535168..52b91213bf 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -34,6 +34,7 @@ import java.net.SocketOption import java.net.StandardSocketOptions import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.arpa.inet._ +import scala.scalanative.posix.errno.ENOPROTOOPT import scala.scalanative.posix.netinet.in.IPPROTO_TCP import scala.scalanative.posix.netinet.tcp._ import scala.scalanative.posix.string._ @@ -66,6 +67,52 @@ private[io] object SocketHelpers { (if (LinktimeInfo.isMac) setNoSigPipe(fd) else F.unit) } + def getOption[F[_]: Sync, A](fd: CInt, name: SocketOption[A]): F[Option[A]] = (name match { + case StandardSocketOptions.SO_SNDBUF => + getOptionInt(fd, SO_SNDBUF) + case StandardSocketOptions.SO_RCVBUF => + getOptionInt(fd, SO_RCVBUF) + case StandardSocketOptions.SO_REUSEADDR => + getOptionInt(fd, SO_REUSEADDR) + case StandardSocketOptions.SO_REUSEPORT => + getOptionBool(fd, SO_REUSEPORT) + case StandardSocketOptions.SO_KEEPALIVE => + getOptionBool(fd, SO_KEEPALIVE) + case StandardSocketOptions.TCP_NODELAY => + getTcpOptionBool(fd, TCP_NODELAY) + case _ => Sync[F].pure(None) + }).asInstanceOf[F[Option[A]]] + + def getOptionBool[F[_]: Sync](fd: CInt, option: CInt): F[Option[Boolean]] = + getOptionInt(fd, option).map(_.map(v => if (v == 0) false else true)) + + def getOptionInt[F[_]: Sync](fd: CInt, option: CInt): F[Option[Int]] = + getOptionImpl(fd, SOL_SOCKET, option) + + def getTcpOptionBool[F[_]: Sync](fd: CInt, option: CInt): F[Option[Boolean]] = + getTcpOptionInt(fd, option).map(_.map(v => if (v == 0) false else true)) + + def getTcpOptionInt[F[_]: Sync](fd: CInt, option: CInt): F[Option[Int]] = + getOptionImpl(fd, IPPROTO_TCP /* aka SOL_TCP */, option) + + def getOptionImpl[F[_]](fd: CInt, level: CInt, option: CInt)(implicit + F: Sync[F] + ): F[Option[Int]] = + F.delay { + val ptr = stackalloc[CInt]() + val szPtr = stackalloc[UInt]() + val ret = guardMask( + getsockopt( + fd, + level, + option, + ptr.asInstanceOf[Ptr[Byte]], + szPtr + ) + )(_ == ENOPROTOOPT) + if (ret == ENOPROTOOPT) None else Some(!ptr) + } + // macOS-only def setNoSigPipe[F[_]: Sync](fd: CInt): F[Unit] = setOption(fd, SO_NOSIGPIPE, true) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala similarity index 81% rename from io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala rename to io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 875ae46f57..87d5253b83 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocketGroup.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -31,7 +31,7 @@ import cats.syntax.all._ import com.comcast.ip4s._ import fs2.io.internal.NativeUtil._ import fs2.io.internal.SocketHelpers -import fs2.io.internal.syssocket._ +import fs2.io.internal.syssocket.{connect => sconnect, bind => sbind, _} import scala.scalanative.libc.errno._ import scala.scalanative.meta.LinktimeInfo @@ -39,10 +39,10 @@ import scala.scalanative.posix.errno._ import scala.scalanative.posix.sys.socket.{bind => _, connect => _, accept => _, _} import scala.scalanative.posix.unistd._ -private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F]) - extends SocketGroup[F] { +private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: Async[F]) + extends IpSocketsProvider[F] { - def client(to: SocketAddress[Host], options: List[SocketOption]): Resource[F, Socket[F]] = for { + def connect(to: SocketAddress[Host], options: List[SocketOption]): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) address <- Resource.eval(to.resolve) ipv4 = address.host.isInstanceOf[Ipv4Address] @@ -56,7 +56,7 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] else IO { SocketHelpers.toSockaddr(address) { (addr, len) => - if (connect(fd, addr, len) < 0) { + if (sconnect(fd, addr, len) < 0) { val e = errno if (e == EINPROGRESS) Left(true) // we will be connected when we unblock @@ -77,29 +77,21 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] ) } yield socket - def server( - address: Option[Host], - port: Option[Port], + def bind( + address: SocketAddress[Host], options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream.resource(serverResource(address, port, options)).flatMap(_._2) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = for { + ): Resource[F, ServerSocket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) - address <- Resource.eval(address.fold(IpAddress.loopback)(_.resolve)) - ipv4 = address.isInstanceOf[Ipv4Address] + resolvedHost <- Resource.eval(address.host.resolve) + ipv4 = resolvedHost.isInstanceOf[Ipv4Address] fd <- SocketHelpers.openNonBlocking(if (ipv4) AF_INET else AF_INET6, SOCK_STREAM) handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) _ <- Resource.eval { val bindF = F.delay { - val socketAddress = SocketAddress(address, port.getOrElse(port"0")) - SocketHelpers.toSockaddr(socketAddress) { (addr, len) => - guard_(bind(fd, addr, len)) + val resolvedAddress = SocketAddress(resolvedHost, address.port) + SocketHelpers.toSockaddr(resolvedAddress) { (addr, len) => + guard_(sbind(fd, addr, len)) } } @@ -151,7 +143,13 @@ private final class FdPollingSocketGroup[F[_]: Dns: LiftIO](implicit F: Async[F] .repeat .unNone - serverAddress <- Resource.eval(SocketHelpers.getLocalAddress(fd, ipv4)) - } yield (serverAddress, sockets) + info = new SocketInfo[F] { + def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) + def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) + def supportedOptions = ??? + def localAddressGen = SocketHelpers.getLocalAddress[F](fd, ipv4).map(a => a: GenSocketAddress) + } + + } yield ServerSocket(info, sockets) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index 1392a8cdaf..397f62f12d 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -22,16 +22,11 @@ package fs2 package io.net -import cats.effect.FileDescriptorPollHandle -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, FileDescriptorPollHandle, IO, LiftIO, Resource} import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.internal.NativeUtil._ -import fs2.io.internal.ResizableBuffer +import fs2.io.internal.{ResizableBuffer, SocketHelpers} import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.errno._ @@ -52,6 +47,9 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( )(implicit F: Async[F]) extends Socket[F] { + def localAddressGen = localAddress.map(a => a: GenSocketAddress) + def remoteAddressGen = remoteAddress.map(a => a: GenSocketAddress) + def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) private[this] def shutdownF(how: Int): F[Unit] = F.delay { @@ -119,6 +117,13 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( def writes: Pipe[F, Byte, Nothing] = _.chunks.foreach(write(_)) + def getOption[A](key: SocketOption.Key[A]) = + SocketHelpers.getOption[F, A](fd, key) + + def setOption[A](key: SocketOption.Key[A], value: A) = + SocketHelpers.setOption(fd, key, value) + + def supportedOptions = ??? } private object FdPollingSocket { diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala similarity index 63% rename from io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala rename to io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 8ddbed9519..7c41ace91a 100644 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/FdPollingUnixSockets.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -22,18 +22,16 @@ package fs2 package io package net -package unixsocket -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, IO, LiftIO, Resource} import cats.syntax.all._ -import fs2.io.file.Files -import fs2.io.file.Path + +import com.comcast.ip4s.UnixSocketAddress + +import fs2.io.file.{Files, Path} import fs2.io.internal.NativeUtil._ import fs2.io.internal.SocketHelpers -import fs2.io.internal.syssocket._ +import fs2.io.internal.syssocket.{connect => uconnect, bind => ubind, _} import fs2.io.internal.sysun._ import fs2.io.internal.sysunOps._ @@ -44,12 +42,15 @@ import scala.scalanative.posix.unistd._ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ -private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[F]) - extends UnixSockets[F] { +private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F: Async[F]) + extends UnixSocketsProvider[F] { - def client(address: UnixSocketAddress): Resource[F, Socket[F]] = for { + // TODO socket options + + def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + _ <- Resource.eval(options.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) _ <- Resource.eval { handle @@ -58,7 +59,7 @@ private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[ else IO { toSockaddrUn(address.path) { addr => - if (guard(connect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) + if (guard(uconnect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) Left(true) // we will be connected when unblocked else Either.unit[Boolean] @@ -70,28 +71,54 @@ private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[ socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) } yield socket - def server( + def bind( address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): Stream[F, Socket[F]] = for { - poller <- Stream.eval(fileDescriptorPoller[F]) + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + + var deleteIfExists: Boolean = false + var deleteOnClose: Boolean = true + + val filteredOptions = options.filter { opt => + opt.key match { + case SocketOption.UnixServerSocketDeleteIfExists => + deleteIfExists = opt.value.asInstanceOf[java.lang.Boolean] + false + case SocketOption.UnixServerSocketDeleteOnClose => + deleteOnClose = opt.value.asInstanceOf[java.lang.Boolean] + false + case _ => true + } + } - _ <- Stream.bracket(Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists)) { _ => + val delete = Resource.make { + Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) + } { _ => Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) } - fd <- Stream.resource(SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM)) - handle <- Stream.resource(poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK)) + for { + poller <- Resource.eval(fileDescriptorPoller[F]) - _ <- Stream.eval { - F.delay { - toSockaddrUn(address.path)(addr => guard_(bind(fd, addr, sizeof[sockaddr_un].toUInt))) - } *> F.delay(guard_(listen(fd, 0))) - } + _ <- delete + + fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + + handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) + _ <- Resource.eval { + F.delay { + toSockaddrUn(address.path)(addr => guard_(ubind(fd, addr, sizeof[sockaddr_un].toUInt))) + } *> F.delay(guard_(listen(fd, 0))) + } + + info = new SocketInfo[F] { + def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) + def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) + def supportedOptions = ??? + def localAddressGen = ??? + } - socket <- Stream - .resource { + clients = Stream.resource { val accepted = for { fd <- Resource.makeFull[F, Int] { poll => poll { @@ -117,17 +144,17 @@ private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[ if (!LinktimeInfo.isLinux) Resource.eval(setNonBlocking(fd)) else Resource.unit[F] + + _ <- Resource.eval(filteredOptions.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) } yield socket - accepted.attempt - .map(_.toOption) - } - .repeat - .unNone + accepted.attempt.map(_.toOption) + }.repeat.unNone - } yield socket + } yield ServerSocket(info, clients) + } private def toSockaddrUn[A](path: String)(f: Ptr[sockaddr] => A): A = { val pathBytes = path.getBytes @@ -143,5 +170,4 @@ private final class FdPollingUnixSockets[F[_]: Files: LiftIO](implicit F: Async[ private def raiseIpAddressError[A]: F[A] = F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) - } diff --git a/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala new file mode 100644 index 0000000000..4dc3493af0 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.io.net + +import cats.effect.{Async, LiftIO} + +private[fs2] trait NetworkLowPriority { this: Network.type => + @deprecated("Add Network constraint or use forAsync", "3.7.0") + implicit def implicitForAsync[F[_]: Async]: Network[F] = + Async[F] match { + case l: LiftIO[_] => forLiftIO(Async[F], l.asInstanceOf[LiftIO[F]]) + case _ => throw new UnsupportedOperationException + } +} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index 1d45570f75..279263e6cd 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,72 +23,46 @@ package fs2 package io package net +import cats.ApplicativeThrow import cats.effect.IO import cats.effect.LiftIO import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress} - -import fs2.io.net.tls.TLSContext +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - def forIO: Network[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = - new UnsealedNetwork[F] { - private lazy val globalSocketGroup = - new FdPollingSocketGroup[F]()(Dns.forAsync, implicitly, implicitly) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) - - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync + private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + address match { + case sa: SocketAddress[Host] => ifIp(sa) + case ua: UnixSocketAddress => ifUnix(ua) + case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) } - def forAsync[F[_]](implicit F: Async[F]): Network[F] = - forAsyncAndDns(F, Dns.forAsync(F)) - - def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = - new UnsealedNetwork[F] { - private lazy val globalSocketGroup = SocketGroup.unsafe[F](null) - - def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = globalSocketGroup.client(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = globalSocketGroup.server(address, port, options) + def forIO: Network[IO] = forLiftIO - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - globalSocketGroup.serverResource(address, port, options) + implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = + new AsyncNetwork[F] { + private lazy val ipSockets = new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) + private lazy val unixSockets = new FdPollingUnixSocketsProvider[F] + + def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + matchAddress(address, + sa => ipSockets.connect(sa, options), + ua => unixSockets.connect(ua, options)) + + def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + matchAddress(address, + sa => ipSockets.bind(sa, options), + ua => unixSockets.bind(ua, options)) - def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync(F) } - } diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala similarity index 82% rename from io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala rename to io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index d35bd8014e..e35b9c69e1 100644 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -19,12 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.unixsocket +package fs2 +package io +package net -import cats.effect.LiftIO -import cats.effect.kernel.Async +import cats.effect.{Async, LiftIO} -private[unixsocket] trait UnixSocketsCompanionPlatform { - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = - new FdPollingUnixSockets[F] +private[net] trait UnixSocketsProviderCompanionPlatform { + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = + new FdPollingUnixSocketsProvider[F] } diff --git a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 2aa0be334d..992e84153f 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -24,12 +24,10 @@ package io package net package tls -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, Resource} import cats.effect.std.Mutex import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} private[tls] trait TLSSocketPlatform[F[_]] @@ -90,9 +88,24 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + def localAddressGen: F[GenSocketAddress] = + socket.localAddressGen + def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress + def remoteAddressGen: F[GenSocketAddress] = + socket.remoteAddressGen + + def supportedOptions: F[Set[SocketOption.Key[_]]] = + socket.supportedOptions + + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + socket.getOption(key) + + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + socket.setOption(key, value) + def session: F[SSLSession] = connection.session def applicationProtocol: F[String] = connection.applicationProtocol diff --git a/io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 94% rename from io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala rename to io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index fa9ecc98b9..6f27975eaa 100644 --- a/io/native/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala +++ b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,10 +20,10 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("native")(UnixSockets.forLiftIO[IO]) + testProvider("native", UnixSocketsProvider.forLiftIO[IO]) } diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 5cdf9bb83c..613ac9baff 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -21,9 +21,12 @@ package fs2.io.net.unixsocket -import cats.effect.kernel.Resource +import cats.effect.{Async, IO, LiftIO, Resource} + +import com.comcast.ip4s.{UnixSocketAddress => Ip4sUnixSocketAddress} + import fs2.Stream -import fs2.io.net.Socket +import fs2.io.net.{Socket, SocketOption, UnixSocketsProvider} /** Capability of working with AF_UNIX sockets. */ trait UnixSockets[F[_]] { @@ -47,6 +50,30 @@ trait UnixSockets[F[_]] { ): Stream[F, Socket[F]] } -object UnixSockets extends UnixSocketsCompanionPlatform { +object UnixSockets { def apply[F[_]](implicit F: UnixSockets[F]): UnixSockets[F] = F + + def forIO: UnixSockets[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = + new AsyncUnixSockets[F] + + private class AsyncUnixSockets[F[_]: Async: LiftIO] extends UnixSockets[F] { + + private val delegate = UnixSocketsProvider.forLiftIO[F] + + def client(address: UnixSocketAddress): Resource[F, Socket[F]] = + delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) + + def server( + address: UnixSocketAddress, + deleteIfExists: Boolean, + deleteOnClose: Boolean + ): Stream[F, Socket[F]] = + Stream.resource( + delegate.bind(Ip4sUnixSocketAddress(address.path), + List(SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), + SocketOption.unixServerSocketDeleteOnClose(deleteOnClose))) + ).flatMap(_.accept) + } } From b649d001e16042d6e9e815c7b4b5fded832c50a6 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 16 Apr 2025 08:49:31 -0400 Subject: [PATCH 16/79] JS tests passing --- .../scala/fs2/io/internal/facade/net.scala | 3 + .../io/net/IpSocketsProviderPlatform.scala | 138 ++++++++++++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 54 +++---- .../fs2/io/net/SocketInfoPlatform.scala} | 38 +++-- .../fs2/io/net/SocketOptionPlatform.scala | 37 ++++- .../scala/fs2/io/net/SocketPlatform.scala | 38 +++-- .../io/net/UnixSocketsProviderPlatform.scala | 149 ++++++++++++++++++ .../fs2/io/net/tls/TLSSocketPlatform.scala | 5 + .../net/unixsocket/UnixSocketsPlatform.scala | 122 -------------- .../UnixSocketsSuitePlatform.scala | 4 +- .../scala/fs2/io/internal/NativeUtil.scala | 2 +- .../scala/fs2/io/internal/SocketHelpers.scala | 65 +++++--- .../io/net/FdPollingIpSocketsProvider.scala | 14 +- .../scala/fs2/io/net/FdPollingSocket.scala | 20 ++- .../io/net/FdPollingUnixSocketsProvider.scala | 12 +- .../scala/fs2/io/net/IpSocketsProvider.scala | 3 + .../main/scala/fs2/io/net/ServerSocket.scala | 16 +- 17 files changed, 495 insertions(+), 225 deletions(-) create mode 100644 io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala rename io/{jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala => js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala} (57%) create mode 100644 io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala delete mode 100644 io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala rename io/js/src/test/scala/fs2/io/net/{unixsockets => }/UnixSocketsSuitePlatform.scala (94%) diff --git a/io/js/src/main/scala/fs2/io/internal/facade/net.scala b/io/js/src/main/scala/fs2/io/internal/facade/net.scala index 3df32775b7..ac81493d05 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/net.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/net.scala @@ -59,6 +59,7 @@ private[io] object net { trait ServerAddress extends js.Object { def address: String = js.native def port: Int = js.native + def path: String = js.native } trait ServerOptions extends js.Object { @@ -110,6 +111,8 @@ private[io] object net { def setTimeout(timeout: Double): Socket = js.native + def timeout: Double = js.native + } } diff --git a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala new file mode 100644 index 0000000000..1b8b2440dd --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Dispatcher +import cats.effect.syntax.all._ +import cats.syntax.all._ +import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} +import fs2.concurrent.Channel +import fs2.io.internal.facade + +import scala.scalajs.js + +private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => + + private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = + new AsyncIpSocketsProvider[F]()(implicitly, Dns.forAsync[F]) + + private[net] final class AsyncIpSocketsProvider[F[_]](implicit F: Async[F], F2: Dns[F]) + extends IpSocketsProvider[F] { + + private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = + options.traverse_(option => option.key.set(socket, option.value)) + + override def connect( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + (for { + sock <- Resource + .make( + F.delay( + new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) + ) + )(sock => + F.delay { + if (!sock.destroyed) + sock.destroy() + } + ) + .evalTap(setSocketOptions(options)) + socket <- Socket.forAsync(sock) + _ <- F + .async[Unit] { cb => + sock + .registerOneTimeListener[F, js.Error]("error") { error => + cb(Left(js.JavaScriptException(error))) + } <* F.delay { + sock.connect(to.port.value, to.host.toString, () => cb(Right(()))) + } + } + .toResource + } yield socket).adaptError { case IOException(ex) => ex } + + override def bind( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + (for { + dispatcher <- Dispatcher.sequential[F] + channel <- Channel.unbounded[F, facade.net.Socket].toResource + server <- Resource.make( + F + .delay( + facade.net.createServer( + new facade.net.ServerOptions { + pauseOnConnect = true + allowHalfOpen = true + }, + sock => dispatcher.unsafeRunAndForget(channel.send(sock)) + ) + ) + )(server => + F.async[Unit] { cb => + if (server.listening) + F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> + channel.close.as(None) + else + F.delay(cb(Right(()))).as(None) + } + ) + ip <- Resource.eval(address.host.resolve[F]) + _ <- F + .async[Unit] { cb => + server.registerOneTimeListener[F, js.Error]("error") { e => + cb(Left(js.JavaScriptException(e))) + } <* F.delay { + if (ip.isWildcard) + server.listen(address.port.value, () => cb(Right(()))) + else + server.listen(address.port.value, ip.toString, () => cb(Right(()))) + } + } + .toResource + info = new SocketInfo[F] { + def localAddressGen = F.delay { + val address = server.address() + if (address.port ne null) + SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) + else + UnixSocketAddress(address.path) + } + + def getOption[A](key: SocketOption.Key[A]) = + F.raiseError(new UnsupportedOperationException) + def setOption[A](key: SocketOption.Key[A], value: A) = + F.raiseError(new UnsupportedOperationException) + def supportedOptions = + F.raiseError(new UnsupportedOperationException) + } + sockets = channel.stream + .evalTap(setSocketOptions(options)) + .flatMap(sock => Stream.resource(Socket.forAsync(sock))) + } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } + } +} diff --git a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala index 9fcbf4364d..a2204a6c89 100644 --- a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,14 +23,8 @@ package fs2 package io package net -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import com.comcast.ip4s.Host -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress +import cats.effect.{Async, IO, LiftIO, Resource} +import com.comcast.ip4s.{GenSocketAddress, Host, Port, SocketAddress, UnixSocketAddress} import fs2.io.net.tls.TLSContext private[net] trait NetworkPlatform[F[_]] @@ -43,31 +37,37 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N forAsync } + // TODO pull up + import cats.ApplicativeThrow + private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + address match { + case sa: SocketAddress[Host] => ifIp(sa) + case ua: UnixSocketAddress => ifUnix(ua) + case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) + } + def forAsync[F[_]](implicit F: Async[F]): Network[F] = - new UnsealedNetwork[F] { + new AsyncNetwork[F] { - private lazy val socketGroup = SocketGroup.forAsync[F] + private lazy val ipSockets = IpSocketsProvider.forAsync[F] + private lazy val unixSockets = UnixSocketsProvider.forAsync[F] private lazy val datagramSocketGroup = DatagramSocketGroup.forAsync[F] - override def client( - to: SocketAddress[Host], - options: List[SocketOption] + override def connect( + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] = - socketGroup.client(to, options) + matchAddress(address, + sa => ipSockets.connect(sa, options), + ua => unixSockets.connect(ua, options)) - override def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - socketGroup.server(address, port, options) - - override def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - socketGroup.serverResource(address, port, options) + override def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + matchAddress(address, + sa => ipSockets.bind(sa, options), + ua => unixSockets.bind(ua, options)) override def openDatagramSocket( address: Option[Host], diff --git a/io/jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala similarity index 57% rename from io/jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala rename to io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 415ed0f290..2089c8b923 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/ServerSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -23,18 +23,34 @@ package fs2 package io package net -private[net] trait ServerSocketCompanionPlatform { - private[net] def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { - val accept0 = accept - new UnsealedServerSocket[F] { - def accept: Stream[F, Socket[F]] = accept0 +import com.comcast.ip4s.{GenSocketAddress, SocketAddress} +import cats.effect.Async +import fs2.io.internal.facade - def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) - def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) - def supportedOptions = info.supportedOptions - - def localAddressGen = info.localAddressGen +private[net] trait SocketInfoCompanionPlatform { + private[net] def forAsync[F[_]](sock: facade.net.Socket)(implicit F: Async[F]): SocketInfo[F] = { + val sock0 = sock + new AsyncSocketInfo[F] { + def asyncInstance = F + def sock: facade.net.Socket = sock0 } } -} + + private[net] trait AsyncSocketInfo[F[_]] extends SocketInfo[F] { + + implicit protected def asyncInstance: Async[F] + + protected def sock: facade.net.Socket + + override def localAddressGen: F[GenSocketAddress] = ??? + + override def supportedOptions: F[Set[SocketOption.Key[_]]] = ??? + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + key.get(sock) + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + key.set(sock, value) + } +} diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 570bd6ea36..972a49d12d 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -24,11 +24,12 @@ package fs2.io.net import cats.effect.kernel.Sync import fs2.io.internal.facade -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sealed trait Key[A] { private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: A): F[Unit] + private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[A]] } private object Encoding extends Key[String] { @@ -37,6 +38,8 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setEncoding(value) () } + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[String]] = + Sync[F].raiseError(new UnsupportedOperationException) } private object KeepAlive extends Key[Boolean] { @@ -45,6 +48,8 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setKeepAlive(value) () } + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = + Sync[F].raiseError(new UnsupportedOperationException) } private object NoDelay extends Key[Boolean] { @@ -53,6 +58,9 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setNoDelay(value) () } + + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = + Sync[F].raiseError(new UnsupportedOperationException) } private object Timeout extends Key[FiniteDuration] { @@ -64,11 +72,36 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setTimeout(value.toMillis.toDouble) () } + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[FiniteDuration]] = + Sync[F].delay { + Some(sock.timeout.toLong.millis) + } + } + + object UnixServerSocketDeleteIfExists extends Key[Boolean] { + override private[net] def set[F[_]: Sync]( + sock: facade.net.Socket, + value: Boolean + ): F[Unit] = Sync[F].unit + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = + Sync[F].pure(None) + } + + object UnixServerSocketDeleteOnClose extends Key[Boolean] { + override private[net] def set[F[_]: Sync]( + sock: facade.net.Socket, + value: Boolean + ): F[Unit] = Sync[F].unit + override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = + Sync[F].pure(None) } def encoding(value: String): SocketOption = apply(Encoding, value) def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) def timeout(value: FiniteDuration): SocketOption = apply(Timeout, value) - + def unixServerSocketDeleteIfExists(value: Boolean): SocketOption = + apply(UnixServerSocketDeleteIfExists, value) + def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = + apply(UnixServerSocketDeleteOnClose, value) } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index dd7bcfce26..98536cf7b0 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -23,16 +23,11 @@ package fs2 package io package net -import cats.data.Kleisli -import cats.data.OptionT -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.data.{Kleisli, OptionT} +import cats.effect.{Async, Resource} import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress -import fs2.io.internal.SuspendedStream -import fs2.io.internal.facade +import com.comcast.ip4s.{GenSocketAddress, IpAddress, Port, SocketAddress} +import fs2.io.internal.{facade, SuspendedStream} private[net] trait SocketCompanionPlatform { @@ -87,17 +82,32 @@ private[net] trait SocketCompanionPlatform { override def isOpen: F[Boolean] = F.delay(sock.readyState == "open") + override def localAddress: F[SocketAddress[IpAddress]] = + for { + ip <- F.delay(sock.localAddress.toOption.flatMap(IpAddress.fromString).get) + port <- F.delay(sock.localPort.toOption.map(_.toInt).flatMap(Port.fromInt).get) + } yield SocketAddress(ip, port) + + override def localAddressGen: F[GenSocketAddress] = + ??? + override def remoteAddress: F[SocketAddress[IpAddress]] = for { ip <- F.delay(sock.remoteAddress.toOption.flatMap(IpAddress.fromString).get) port <- F.delay(sock.remotePort.toOption.map(_.toInt).flatMap(Port.fromInt).get) } yield SocketAddress(ip, port) - override def localAddress: F[SocketAddress[IpAddress]] = - for { - ip <- F.delay(sock.localAddress.toOption.flatMap(IpAddress.fromString).get) - port <- F.delay(sock.localPort.toOption.map(_.toInt).flatMap(Port.fromInt).get) - } yield SocketAddress(ip, port) + override def remoteAddressGen: F[GenSocketAddress] = + ??? + + override def supportedOptions: F[Set[SocketOption.Key[_]]] = + ??? + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + key.get(sock) + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + key.set(sock, value) override def write(bytes: Chunk[Byte]): F[Unit] = Stream.chunk(bytes).through(writes).compile.drain diff --git a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala new file mode 100644 index 0000000000..e97cddeffd --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, IO, LiftIO, Resource} +import cats.effect.std.Dispatcher +import cats.syntax.all._ +import com.comcast.ip4s.UnixSocketAddress +import fs2.concurrent.Channel +import fs2.io.file.{Files, Path} +import fs2.io.internal.facade + +import scala.scalajs.js + +private[net] trait UnixSocketsProviderCompanionPlatform { + def forIO: UnixSocketsProvider[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = + forAsyncAndFiles(Files.forAsync(F), F) + + def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSocketsProvider[F] = + new UnixSocketsProvider[F] { + + private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = + options.traverse_(option => option.key.set(socket, option.value)) + + override def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = + (Resource + .make( + F.delay( + new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) + ) + )(socket => + F.delay { + if (!socket.destroyed) + socket.destroy() + } + ) + .evalTap(setSocketOptions(options)) + .evalTap { socket => + F.async[Unit] { cb => + socket + .registerOneTimeListener[F, js.Error]("error") { error => + cb(Left(js.JavaScriptException(error))) + } <* F.delay { + socket.connect(address.path, () => cb(Right(()))) + } + } + } + .flatMap(Socket.forAsync[F])).adaptError { case IOException(ex) => ex } + + override def bind( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + + var deleteIfExists: Boolean = false + var deleteOnClose: Boolean = true + + // TODO use options + val filteredOptions = options.filter { opt => + if (opt.key == SocketOption.UnixServerSocketDeleteIfExists) { + deleteIfExists = opt.value.asInstanceOf[Boolean] + false + } else if (opt.key == SocketOption.UnixServerSocketDeleteOnClose) { + deleteOnClose = opt.value.asInstanceOf[Boolean] + false + } else true + } + + for { + dispatcher <- Dispatcher.sequential[F] + channel <- Resource.eval(Channel.unbounded[F, facade.net.Socket]) + server <- Resource.make( + F.delay { + facade.net.createServer( + new facade.net.ServerOptions { + pauseOnConnect = true + allowHalfOpen = true + }, + sock => dispatcher.unsafeRunAndForget(channel.send(sock)) + ) + } + )(server => + F.async_[Unit] { cb => + if (server.listening) { + server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException))) + () + } else + cb(Right(())) + } + ) + + _ <- Resource.make( + if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else F.unit + )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else F.unit) + + _ <- Resource.eval( + F.async[Unit] { cb => + server.registerOneTimeListener[F, js.Error]("error") { e => + cb(Left(js.JavaScriptException(e))) + } <* F.delay(server.listen(address.path, () => cb(Right(())))) + } + ) + + info = new SocketInfo[F] { + def localAddressGen = F.delay { UnixSocketAddress(address.path) } + def getOption[A](key: SocketOption.Key[A]) = + F.raiseError(new UnsupportedOperationException) + def setOption[A](key: SocketOption.Key[A], value: A) = + F.raiseError(new UnsupportedOperationException) + def supportedOptions = + F.raiseError(new UnsupportedOperationException) + } + accept = channel.stream + .evalTap(setSocketOptions(filteredOptions)) + .flatMap(sock => Stream.resource(Socket.forAsync(sock))) + } yield ServerSocket(info, accept) + } + + } + +} diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 050f9d8353..5f9e955278 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -71,6 +71,11 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => ) extends Socket.AsyncSocket[F](sock, readStream) with UnsealedTLSSocket[F] { override def localAddress = underlying.localAddress + override def localAddressGen = underlying.localAddressGen override def remoteAddress = underlying.remoteAddress + override def remoteAddressGen = underlying.remoteAddressGen + override def getOption[A](key: SocketOption.Key[A]) = underlying.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = underlying.setOption(key, value) + override def supportedOptions = underlying.supportedOptions } } diff --git a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala deleted file mode 100644 index 2719d64ccf..0000000000 --- a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io.net.unixsocket - -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.effect.std.Dispatcher -import cats.syntax.all._ -import fs2.concurrent.Channel -import fs2.io.file.Files -import fs2.io.file.Path -import fs2.io.net.Socket -import fs2.io.internal.facade - -import scala.scalajs.js - -private[unixsocket] trait UnixSocketsCompanionPlatform { - def forIO: UnixSockets[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { - val _ = LiftIO[F] - forAsyncAndFiles - } - - def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = - forAsyncAndFiles(Files.forAsync(F), F) - - def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = - new UnixSockets[F] { - - override def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(socket => - F.delay { - if (!socket.destroyed) - socket.destroy() - } - ) - .evalTap { socket => - F.async_[Unit] { cb => - socket.connect(address.path, () => cb(Right(()))) - () - } - } - .flatMap(Socket.forAsync[F]) - - override def server( - address: UnixSocketAddress, - deleteIfExists: Boolean, - deleteOnClose: Boolean - ): fs2.Stream[F, Socket[F]] = - for { - dispatcher <- Stream.resource(Dispatcher.sequential[F]) - channel <- Stream.eval(Channel.unbounded[F, facade.net.Socket]) - errored <- Stream.eval(F.deferred[js.JavaScriptException]) - server <- Stream.bracket( - F.delay { - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - } - )(server => - F.async_[Unit] { cb => - if (server.listening) { - server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException))) - () - } else - cb(Right(())) - } - ) - _ <- Stream - .resource( - server.registerListener[F, js.Error]("error", dispatcher) { e => - errored.complete(js.JavaScriptException(e)).void - } - ) - .concurrently(Stream.eval(errored.get.flatMap(F.raiseError[Unit]))) - _ <- Stream.bracket( - if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else F.unit - )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else F.unit) - _ <- Stream.eval( - F.async_[Unit] { cb => - server.listen(address.path, () => cb(Right(()))) - () - } - ) - socket <- channel.stream.flatMap(sock => Stream.resource(Socket.forAsync(sock))) - } yield socket - - } - -} diff --git a/io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala similarity index 94% rename from io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala rename to io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index b39cd42c62..cc5a6fd91c 100644 --- a/io/js/src/test/scala/fs2/io/net/unixsockets/UnixSocketsSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -20,10 +20,10 @@ */ package fs2 -package io.net.unixsocket +package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("node.js")(UnixSockets.forAsync[IO]) + testProvider("node.js", UnixSocketsProvider.forAsync[IO]) } diff --git a/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala b/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala index 99064c4985..48b3dbc5b0 100644 --- a/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala +++ b/io/native/src/main/scala/fs2/io/internal/NativeUtil.scala @@ -76,7 +76,7 @@ private[io] object NativeUtil { else if (e == ECONNREFUSED) new ConnectException(msg) else - new IOException(msg) + new IOException(s"errno($e) $msg") } def setNonBlocking[F[_]](fd: CInt)(implicit F: Sync[F]): F[Unit] = F.delay { diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index 52b91213bf..eddf76eef7 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -21,14 +21,9 @@ package fs2.io.internal -import cats.effect.kernel.Resource -import cats.effect.kernel.Sync +import cats.effect.{Resource, Sync} import cats.syntax.all._ -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.Ipv4Address -import com.comcast.ip4s.Ipv6Address -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress +import com.comcast.ip4s.{GenSocketAddress, IpAddress, Ipv4Address, Ipv6Address, Port, SocketAddress, UnixSocketAddress} import java.net.SocketOption import java.net.StandardSocketOptions @@ -47,6 +42,8 @@ import NativeUtil._ import netinetin._ import netinetinOps._ import syssocket._ +import sysun._ +import sysunOps._ private[io] object SocketHelpers { @@ -101,6 +98,7 @@ private[io] object SocketHelpers { F.delay { val ptr = stackalloc[CInt]() val szPtr = stackalloc[UInt]() + !szPtr = sizeof[CInt].toUInt val ret = guardMask( getsockopt( fd, @@ -208,12 +206,29 @@ private[io] object SocketHelpers { def getLocalAddress[F[_]](fd: Int, ipv4: Boolean)(implicit F: Sync[F] ): F[SocketAddress[IpAddress]] = + getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6).map { + case a: SocketAddress[IpAddress] @unchecked => a + case _ => throw new IllegalArgumentException + } + + def getLocalAddressGen[F[_]](fd: Int, domain: CInt)(implicit + F: Sync[F] + ): F[GenSocketAddress] = F.delay { - SocketHelpers.toSocketAddress(ipv4) { (addr, len) => + SocketHelpers.toSocketAddress(domain) { (addr, len) => guard_(getsockname(fd, addr, len)) } } + def getRemoteAddressGen[F[_]](fd: Int, domain: CInt)(implicit + F: Sync[F] + ): F[GenSocketAddress] = + F.delay { + SocketHelpers.toSocketAddress(domain) { (addr, len) => + guard_(getpeername(fd, addr, len)) + } + } + def toSockaddr[A]( address: SocketAddress[IpAddress] )(f: (Ptr[sockaddr], socklen_t) => A): A = @@ -299,29 +314,32 @@ private[io] object SocketHelpers { } } - def allocateSockaddr[A]( + def allocateSockaddr[A](domain: CInt)( f: (Ptr[sockaddr], Ptr[socklen_t]) => A ): A = { - val addr = // allocate enough for an IPv6 - stackalloc[sockaddr_in6]().asInstanceOf[Ptr[sockaddr]] - val len = stackalloc[socklen_t]() - !len = sizeof[sockaddr_in6].toUInt + // FIXME: Scala Native 0.4 doesn't support getsconame for unix sockets; after upgrading to 0.5, + // this can be simplified to just allocate a sockaddr_un + val (addr, lenValue) = // allocate enough for unix socket address + if (domain == AF_UNIX) (stackalloc[sockaddr_un]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_un].toUInt) + else (stackalloc[sockaddr_in6]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_in6].toUInt) + val len = stackalloc[socklen_t]() + !len = lenValue f(addr, len) } - def toSocketAddress[A](ipv4: Boolean)( + def toSocketAddress[A](domain: CInt)( f: (Ptr[sockaddr], Ptr[socklen_t]) => Unit - ): SocketAddress[IpAddress] = allocateSockaddr { (addr, len) => + ): GenSocketAddress = allocateSockaddr(domain) { (addr, len) => f(addr, len) - toSocketAddress(addr, ipv4) + toSocketAddress(addr, domain) } - def toSocketAddress(addr: Ptr[sockaddr], ipv4: Boolean): SocketAddress[IpAddress] = - if (ipv4) - toIpv4SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in]]) - else - toIpv6SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in6]]) + def toSocketAddress(addr: Ptr[sockaddr], domain: CInt): GenSocketAddress = + if (domain == AF_INET) toIpv4SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in]]) + else if (domain == AF_INET6) toIpv6SocketAddress(addr.asInstanceOf[Ptr[sockaddr_in6]]) + else if (domain == AF_UNIX) toUnixSocketAddress(addr.asInstanceOf[Ptr[sockaddr_un]]) + else throw new UnsupportedOperationException private[this] def toIpv4SocketAddress(addr: Ptr[sockaddr_in]): SocketAddress[Ipv4Address] = { val port = Port.fromInt(ntohs(addr.sin_port).toInt).get @@ -345,4 +363,9 @@ private[io] object SocketHelpers { }.get SocketAddress(host, port) } + + private[this] def toUnixSocketAddress(addr: Ptr[sockaddr_un]): UnixSocketAddress = { + val path = fromCString(addr.sun_path.asInstanceOf[CString]) + UnixSocketAddress(path) + } } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 87d5253b83..582c7f62b1 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -72,8 +72,8 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddress(fd, ipv4), - F.pure(address) + SocketHelpers.getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getRemoteAddressGen(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket @@ -106,7 +106,7 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As handle .pollReadRec(()) { _ => IO { - SocketHelpers.allocateSockaddr { (addr, len) => + SocketHelpers.allocateSockaddr(if (ipv4) AF_INET else AF_INET6) { (addr, len) => val clientFd = if (LinktimeInfo.isLinux) guard(accept4(fd, addr, len, SOCK_NONBLOCK)) @@ -114,7 +114,7 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As guard(accept(fd, addr, len)) if (clientFd >= 0) { - val address = SocketHelpers.toSocketAddress(addr, ipv4) + val address = SocketHelpers.toSocketAddress(addr, if (ipv4) AF_INET else AF_INET6).asInstanceOf[SocketAddress[IpAddress]] Right((address, clientFd)) } else Left(()) @@ -133,8 +133,8 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddress(fd, ipv4), - F.pure(address) + SocketHelpers.getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getRemoteAddressGen(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket @@ -147,7 +147,7 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) def supportedOptions = ??? - def localAddressGen = SocketHelpers.getLocalAddress[F](fd, ipv4).map(a => a: GenSocketAddress) + def localAddressGen = SocketHelpers.getLocalAddressGen[F](fd, if (ipv4) AF_INET else AF_INET6) } } yield ServerSocket(info, sockets) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index 397f62f12d..c4b00363ad 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -42,13 +42,19 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( handle: FileDescriptorPollHandle, readBuffer: ResizableBuffer[F], val isOpen: F[Boolean], - val localAddress: F[SocketAddress[IpAddress]], - val remoteAddress: F[SocketAddress[IpAddress]] + val localAddressGen: F[GenSocketAddress], + val remoteAddressGen: F[GenSocketAddress] )(implicit F: Async[F]) extends Socket[F] { - def localAddressGen = localAddress.map(a => a: GenSocketAddress) - def remoteAddressGen = remoteAddress.map(a => a: GenSocketAddress) + def localAddress = downcastAddress(localAddressGen) + def remoteAddress = downcastAddress(remoteAddressGen) + + private def downcastAddress(address: F[GenSocketAddress]): F[SocketAddress[IpAddress]] = + address.flatMap { + case a: SocketAddress[IpAddress] @unchecked => F.pure(a) + case _ => F.raiseError(new UnsupportedOperationException("invalid address type")) + } def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) @@ -132,10 +138,10 @@ private object FdPollingSocket { def apply[F[_]: LiftIO]( fd: Int, handle: FileDescriptorPollHandle, - localAddress: F[SocketAddress[IpAddress]], - remoteAddress: F[SocketAddress[IpAddress]] + localAddressGen: F[GenSocketAddress], + remoteAddressGen: F[GenSocketAddress] )(implicit F: Async[F]): Resource[F, Socket[F]] = for { buffer <- ResizableBuffer(DefaultReadSize) isOpen <- Resource.make(F.ref(true))(_.set(false)) - } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, localAddress, remoteAddress) + } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, localAddressGen, remoteAddressGen) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 7c41ace91a..f13bfa0bc7 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -45,8 +45,6 @@ import scala.scalanative.unsigned._ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F: Async[F]) extends UnixSocketsProvider[F] { - // TODO socket options - def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) @@ -68,7 +66,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F } .to } - socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) + socket <- FdPollingSocket[F](fd, handle, SocketHelpers.getLocalAddressGen(fd, AF_UNIX), SocketHelpers.getRemoteAddressGen(fd, AF_UNIX)) } yield socket def bind( @@ -104,6 +102,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) _ <- Resource.eval { F.delay { @@ -115,7 +114,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) def supportedOptions = ??? - def localAddressGen = ??? + def localAddressGen = SocketHelpers.getLocalAddressGen(fd, AF_UNIX) } clients = Stream.resource { @@ -147,7 +146,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F _ <- Resource.eval(filteredOptions.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) - socket <- FdPollingSocket[F](fd, handle, raiseIpAddressError, raiseIpAddressError) + socket <- FdPollingSocket[F](fd, handle, SocketHelpers.getLocalAddressGen(fd, AF_UNIX), SocketHelpers.getRemoteAddressGen(fd, AF_UNIX)) } yield socket accepted.attempt.map(_.toOption) @@ -167,7 +166,4 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F f(addr.asInstanceOf[Ptr[sockaddr]]) } - - private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) } diff --git a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala index c006371595..04fd5dc7db 100644 --- a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -38,3 +38,6 @@ private[net] trait IpSocketsProvider[F[_]] { options: List[SocketOption] ): Resource[F, ServerSocket[F]] } + +private[net] object IpSocketsProvider extends IpSocketsProviderCompanionPlatform + diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 4bb9f5e95a..3e66b67256 100644 --- a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -38,7 +38,17 @@ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { def accept: Stream[F, Socket[F]] } -private[net] trait UnsealedServerSocket[F[_]] extends ServerSocket[F] - -object ServerSocket extends ServerSocketCompanionPlatform +object ServerSocket { + + def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { + val accept0 = accept + new ServerSocket[F] { + def accept: Stream[F, Socket[F]] = accept0 + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) + def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) + def supportedOptions = info.supportedOptions + def localAddressGen = info.localAddressGen + } + } +} From 4a0056672284a9dc3854d0f016e2869670cf090b Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 16 Apr 2025 18:21:20 -0400 Subject: [PATCH 17/79] JS duplication reduction --- .../fs2/io/net/AsyncSocketsProvider.scala | 140 ++++++++++++++++++ .../io/net/IpSocketsProviderPlatform.scala | 107 ++----------- .../io/net/UnixSocketsProviderPlatform.scala | 102 ++----------- .../fs2/io/net/unixsocket/UnixSockets.scala | 8 +- 4 files changed, 166 insertions(+), 191 deletions(-) create mode 100644 io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala new file mode 100644 index 0000000000..7fc457fb81 --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Dispatcher +import cats.effect.syntax.all._ +import cats.syntax.all._ +import com.comcast.ip4s.{IpAddress, Port, SocketAddress, UnixSocketAddress} +import fs2.concurrent.Channel +import fs2.io.internal.facade + +import scala.scalajs.js + +private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { + + private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = + options.traverse_(option => option.key.set(socket, option.value)) + + protected def connectIpOrUnix( + to: Either[SocketAddress[IpAddress], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + (for { + sock <- Resource + .make( + F.delay( + new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) + ) + )(sock => + F.delay { + if (!sock.destroyed) + sock.destroy() + } + ) + .evalTap(setSocketOptions(options)) + socket <- Socket.forAsync(sock) + _ <- F + .async[Unit] { cb => + sock + .registerOneTimeListener[F, js.Error]("error") { error => + cb(Left(js.JavaScriptException(error))) + } <* F.delay { + to match { + case Left(addr) => + sock.connect(addr.port.value, addr.host.toString, () => cb(Right(()))) + case Right(addr) => + sock.connect(addr.path, () => cb(Right(()))) + } + } + } + .toResource + } yield socket).adaptError { case IOException(ex) => ex } + + protected def bindIpOrUnix( + address: Either[SocketAddress[IpAddress], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + (for { + dispatcher <- Dispatcher.sequential[F] + channel <- Channel.unbounded[F, facade.net.Socket].toResource + server <- Resource.make( + F + .delay( + facade.net.createServer( + new facade.net.ServerOptions { + pauseOnConnect = true + allowHalfOpen = true + }, + sock => dispatcher.unsafeRunAndForget(channel.send(sock)) + ) + ) + )(server => + F.async[Unit] { cb => + if (server.listening) + F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> + channel.close.as(None) + else + F.delay(cb(Right(()))).as(None) + } + ) + _ <- F + .async[Unit] { cb => + server.registerOneTimeListener[F, js.Error]("error") { e => + cb(Left(js.JavaScriptException(e))) + } <* F.delay { + address match { + case Left(addr) => + if (addr.host.isWildcard) + server.listen(addr.port.value, () => cb(Right(()))) + else + server.listen(addr.port.value, addr.host.toString, () => cb(Right(()))) + case Right(addr) => + server.listen(addr.path, () => cb(Right(()))) + } + } + } + .toResource + info = new SocketInfo[F] { + def localAddressGen = F.delay { + val address = server.address() + if (address.port ne null) + SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) + else + UnixSocketAddress(address.path) + } + + def getOption[A](key: SocketOption.Key[A]) = + F.raiseError(new UnsupportedOperationException) + def setOption[A](key: SocketOption.Key[A], value: A) = + F.raiseError(new UnsupportedOperationException) + def supportedOptions = + F.raiseError(new UnsupportedOperationException) + } + sockets = channel.stream + .evalTap(setSocketOptions(options)) + .flatMap(sock => Stream.resource(Socket.forAsync(sock))) + } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } +} diff --git a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala index 1b8b2440dd..3896a1f52d 100644 --- a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -24,115 +24,30 @@ package io package net import cats.effect.{Async, Resource} -import cats.effect.std.Dispatcher -import cats.effect.syntax.all._ -import cats.syntax.all._ -import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} -import fs2.concurrent.Channel -import fs2.io.internal.facade - -import scala.scalajs.js +import com.comcast.ip4s.{Dns, Host, SocketAddress} private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = - new AsyncIpSocketsProvider[F]()(implicitly, Dns.forAsync[F]) - - private[net] final class AsyncIpSocketsProvider[F[_]](implicit F: Async[F], F2: Dns[F]) - extends IpSocketsProvider[F] { + forAsyncAndDns[F](implicitly, Dns.forAsync) - private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = - options.traverse_(option => option.key.set(socket, option.value)) + private def forAsyncAndDns[F[_]: Async: Dns]: IpSocketsProvider[F] = + new AsyncSocketsProvider[F] with IpSocketsProvider[F] { override def connect( - to: SocketAddress[Host], + address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] = - (for { - sock <- Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(sock => - F.delay { - if (!sock.destroyed) - sock.destroy() - } - ) - .evalTap(setSocketOptions(options)) - socket <- Socket.forAsync(sock) - _ <- F - .async[Unit] { cb => - sock - .registerOneTimeListener[F, js.Error]("error") { error => - cb(Left(js.JavaScriptException(error))) - } <* F.delay { - sock.connect(to.port.value, to.host.toString, () => cb(Right(()))) - } - } - .toResource - } yield socket).adaptError { case IOException(ex) => ex } + Resource.eval(address.host.resolve[F]).flatMap { ip => + connectIpOrUnix(Left(SocketAddress(ip, address.port)), options) + } override def bind( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - (for { - dispatcher <- Dispatcher.sequential[F] - channel <- Channel.unbounded[F, facade.net.Socket].toResource - server <- Resource.make( - F - .delay( - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - ) - )(server => - F.async[Unit] { cb => - if (server.listening) - F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> - channel.close.as(None) - else - F.delay(cb(Right(()))).as(None) - } - ) - ip <- Resource.eval(address.host.resolve[F]) - _ <- F - .async[Unit] { cb => - server.registerOneTimeListener[F, js.Error]("error") { e => - cb(Left(js.JavaScriptException(e))) - } <* F.delay { - if (ip.isWildcard) - server.listen(address.port.value, () => cb(Right(()))) - else - server.listen(address.port.value, ip.toString, () => cb(Right(()))) - } - } - .toResource - info = new SocketInfo[F] { - def localAddressGen = F.delay { - val address = server.address() - if (address.port ne null) - SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) - else - UnixSocketAddress(address.path) - } - - def getOption[A](key: SocketOption.Key[A]) = - F.raiseError(new UnsupportedOperationException) - def setOption[A](key: SocketOption.Key[A], value: A) = - F.raiseError(new UnsupportedOperationException) - def supportedOptions = - F.raiseError(new UnsupportedOperationException) - } - sockets = channel.stream - .evalTap(setSocketOptions(options)) - .flatMap(sock => Stream.resource(Socket.forAsync(sock))) - } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } + Resource.eval(address.host.resolve[F]).flatMap { ip => + bindIpOrUnix(Left(SocketAddress(ip, address.port)), options) + } } } diff --git a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index e97cddeffd..703cc5f760 100644 --- a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -23,57 +23,21 @@ package fs2 package io package net -import cats.effect.{Async, IO, LiftIO, Resource} -import cats.effect.std.Dispatcher +import cats.effect.{Async, Resource} import cats.syntax.all._ import com.comcast.ip4s.UnixSocketAddress -import fs2.concurrent.Channel import fs2.io.file.{Files, Path} -import fs2.io.internal.facade - -import scala.scalajs.js private[net] trait UnixSocketsProviderCompanionPlatform { - def forIO: UnixSocketsProvider[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { - val _ = LiftIO[F] - forAsyncAndFiles - } - def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = - forAsyncAndFiles(Files.forAsync(F), F) + private[net] def forAsync[F[_]: Async]: UnixSocketsProvider[F] = + forAsyncAndFiles(implicitly, Files.forAsync[F]) - def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSocketsProvider[F] = - new UnixSocketsProvider[F] { - - private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = - options.traverse_(option => option.key.set(socket, option.value)) + private def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + new AsyncSocketsProvider[F] with UnixSocketsProvider[F] { override def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = - (Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(socket => - F.delay { - if (!socket.destroyed) - socket.destroy() - } - ) - .evalTap(setSocketOptions(options)) - .evalTap { socket => - F.async[Unit] { cb => - socket - .registerOneTimeListener[F, js.Error]("error") { error => - cb(Left(js.JavaScriptException(error))) - } <* F.delay { - socket.connect(address.path, () => cb(Right(()))) - } - } - } - .flatMap(Socket.forAsync[F])).adaptError { case IOException(ex) => ex } + connectIpOrUnix(Right(address), options) override def bind( address: UnixSocketAddress, @@ -83,7 +47,6 @@ private[net] trait UnixSocketsProviderCompanionPlatform { var deleteIfExists: Boolean = false var deleteOnClose: Boolean = true - // TODO use options val filteredOptions = options.filter { opt => if (opt.key == SocketOption.UnixServerSocketDeleteIfExists) { deleteIfExists = opt.value.asInstanceOf[Boolean] @@ -94,56 +57,11 @@ private[net] trait UnixSocketsProviderCompanionPlatform { } else true } - for { - dispatcher <- Dispatcher.sequential[F] - channel <- Resource.eval(Channel.unbounded[F, facade.net.Socket]) - server <- Resource.make( - F.delay { - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - } - )(server => - F.async_[Unit] { cb => - if (server.listening) { - server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException))) - () - } else - cb(Right(())) - } - ) - - _ <- Resource.make( - if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else F.unit - )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else F.unit) + val delete = Resource.make( + if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit + )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit) - _ <- Resource.eval( - F.async[Unit] { cb => - server.registerOneTimeListener[F, js.Error]("error") { e => - cb(Left(js.JavaScriptException(e))) - } <* F.delay(server.listen(address.path, () => cb(Right(())))) - } - ) - - info = new SocketInfo[F] { - def localAddressGen = F.delay { UnixSocketAddress(address.path) } - def getOption[A](key: SocketOption.Key[A]) = - F.raiseError(new UnsupportedOperationException) - def setOption[A](key: SocketOption.Key[A], value: A) = - F.raiseError(new UnsupportedOperationException) - def supportedOptions = - F.raiseError(new UnsupportedOperationException) - } - accept = channel.stream - .evalTap(setSocketOptions(filteredOptions)) - .flatMap(sock => Stream.resource(Socket.forAsync(sock))) - } yield ServerSocket(info, accept) + delete *> bindIpOrUnix(Right(address), filteredOptions) } - } - } diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 613ac9baff..5e29db3511 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -55,12 +55,14 @@ object UnixSockets { def forIO: UnixSockets[IO] = forLiftIO - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { + val _ = LiftIO[F] new AsyncUnixSockets[F] + } - private class AsyncUnixSockets[F[_]: Async: LiftIO] extends UnixSockets[F] { + private class AsyncUnixSockets[F[_]: Async] extends UnixSockets[F] { - private val delegate = UnixSocketsProvider.forLiftIO[F] + private val delegate = UnixSocketsProvider.forAsync[F] def client(address: UnixSocketAddress): Resource[F, Socket[F]] = delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) From e22041b75b84b7777b764b62f83cb388ded9eb1e Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 16 Apr 2025 18:31:37 -0400 Subject: [PATCH 18/79] Scalafmt --- .../fs2/io/net/AsyncSocketsProvider.scala | 14 +- .../io/net/IpSocketsProviderPlatform.scala | 30 ++-- .../scala/fs2/io/net/NetworkPlatform.scala | 33 +++-- .../scala/fs2/io/net/SocketInfoPlatform.scala | 3 +- .../fs2/io/net/SocketOptionPlatform.scala | 2 +- .../scala/fs2/io/net/SocketPlatform.scala | 2 +- .../io/net/UnixSocketsProviderPlatform.scala | 9 +- ...hronousChannelGroupIpSocketsProvider.scala | 63 ++++----- .../fs2/io/net/SocketGroupPlatform.scala | 32 +++-- .../scala/fs2/io/net/SocketInfoPlatform.scala | 7 +- .../scala/fs2/io/net/SocketPlatform.scala | 3 +- .../fs2/io/net/JdkUnixSocketsProvider.scala | 7 +- .../fs2/io/net/JnrUnixSocketsProvider.scala | 15 +- .../scala/fs2/io/net/NetworkPlatform.scala | 76 +++++++---- .../io/net/SelectingIpSocketsProvider.scala | 4 +- .../scala/fs2/io/net/SelectingSocket.scala | 4 +- .../io/net/UnixSocketsProviderPlatform.scala | 5 +- .../fs2/io/net/tls/TLSSocketPlatform.scala | 2 +- .../scala/fs2/io/internal/SocketHelpers.scala | 15 +- .../fs2/io/net/DatagramSocketGroup.scala | 4 +- .../io/net/FdPollingIpSocketsProvider.scala | 4 +- .../io/net/FdPollingUnixSocketsProvider.scala | 128 ++++++++++-------- .../scala/fs2/io/net/NetworkLowPriority.scala | 4 +- .../scala/fs2/io/net/NetworkPlatform.scala | 46 +++++-- .../fs2/io/net/tls/TLSSocketPlatform.scala | 2 +- .../scala/fs2/io/net/IpSocketsProvider.scala | 9 +- .../src/main/scala/fs2/io/net/Network.scala | 41 ++++-- .../main/scala/fs2/io/net/ServerSocket.scala | 19 ++- .../main/scala/fs2/io/net/SocketInfo.scala | 3 +- .../fs2/io/net/UnixSocketsProvider.scala | 11 +- .../fs2/io/net/unixsocket/UnixSockets.scala | 16 ++- .../scala/fs2/io/net/UnixSocketsSuite.scala | 3 +- 32 files changed, 374 insertions(+), 242 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 7fc457fb81..24b80debf0 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -62,12 +62,12 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { .registerOneTimeListener[F, js.Error]("error") { error => cb(Left(js.JavaScriptException(error))) } <* F.delay { - to match { - case Left(addr) => - sock.connect(addr.port.value, addr.host.toString, () => cb(Right(()))) - case Right(addr) => - sock.connect(addr.path, () => cb(Right(()))) - } + to match { + case Left(addr) => + sock.connect(addr.port.value, addr.host.toString, () => cb(Right(()))) + case Right(addr) => + sock.connect(addr.path, () => cb(Right(()))) + } } } .toResource @@ -112,7 +112,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { else server.listen(addr.port.value, addr.host.toString, () => cb(Right(()))) case Right(addr) => - server.listen(addr.path, () => cb(Right(()))) + server.listen(addr.path, () => cb(Right(()))) } } } diff --git a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala index 3896a1f52d..4d07adebf1 100644 --- a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -34,20 +34,20 @@ private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider. private def forAsyncAndDns[F[_]: Async: Dns]: IpSocketsProvider[F] = new AsyncSocketsProvider[F] with IpSocketsProvider[F] { - override def connect( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - Resource.eval(address.host.resolve[F]).flatMap { ip => - connectIpOrUnix(Left(SocketAddress(ip, address.port)), options) - } + override def connect( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + Resource.eval(address.host.resolve[F]).flatMap { ip => + connectIpOrUnix(Left(SocketAddress(ip, address.port)), options) + } - override def bind( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = - Resource.eval(address.host.resolve[F]).flatMap { ip => - bindIpOrUnix(Left(SocketAddress(ip, address.port)), options) - } - } + override def bind( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + Resource.eval(address.host.resolve[F]).flatMap { ip => + bindIpOrUnix(Left(SocketAddress(ip, address.port)), options) + } + } } diff --git a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala index a2204a6c89..1e51aed08a 100644 --- a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -39,11 +39,18 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N // TODO pull up import cats.ApplicativeThrow - private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + private def matchAddress[F[_]: ApplicativeThrow, A]( + address: GenSocketAddress, + ifIp: SocketAddress[Host] => F[A], + ifUnix: UnixSocketAddress => F[A] + ): F[A] = address match { case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) + case ua: UnixSocketAddress => ifUnix(ua) + case other => + ApplicativeThrow[F].raiseError( + new UnsupportedOperationException(s"Unsupported address type: $other") + ) } def forAsync[F[_]](implicit F: Async[F]): Network[F] = @@ -54,20 +61,24 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private lazy val datagramSocketGroup = DatagramSocketGroup.forAsync[F] override def connect( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options)) + ua => unixSockets.connect(ua, options) + ) override def bind( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options)) + ua => unixSockets.bind(ua, options) + ) override def openDatagramSocket( address: Option[Host], diff --git a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 2089c8b923..9fbc0af142 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -36,7 +36,6 @@ private[net] trait SocketInfoCompanionPlatform { } } - private[net] trait AsyncSocketInfo[F[_]] extends SocketInfo[F] { implicit protected def asyncInstance: Async[F] @@ -45,7 +44,7 @@ private[net] trait SocketInfoCompanionPlatform { override def localAddressGen: F[GenSocketAddress] = ??? - override def supportedOptions: F[Set[SocketOption.Key[_]]] = ??? + override def supportedOptions: F[Set[SocketOption.Key[?]]] = ??? override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = key.get(sock) diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 972a49d12d..27e32ccd88 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -86,7 +86,7 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = Sync[F].pure(None) } - + object UnixServerSocketDeleteOnClose extends Key[Boolean] { override private[net] def set[F[_]: Sync]( sock: facade.net.Socket, diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 98536cf7b0..8fe6390f0f 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -100,7 +100,7 @@ private[net] trait SocketCompanionPlatform { override def remoteAddressGen: F[GenSocketAddress] = ??? - override def supportedOptions: F[Set[SocketOption.Key[_]]] = + override def supportedOptions: F[Set[SocketOption.Key[?]]] = ??? override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = diff --git a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 703cc5f760..3934b11199 100644 --- a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -36,7 +36,10 @@ private[net] trait UnixSocketsProviderCompanionPlatform { private def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = new AsyncSocketsProvider[F] with UnixSocketsProvider[F] { - override def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = + override def connect( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = connectIpOrUnix(Right(address), options) override def bind( @@ -59,7 +62,9 @@ private[net] trait UnixSocketsProviderCompanionPlatform { val delete = Resource.make( if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit - )(_ => if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit) + )(_ => + if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit + ) delete *> bindIpOrUnix(Right(address), filteredOptions) } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index ac6a4e475c..53a8bade05 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -23,7 +23,6 @@ package fs2 package io package net - import java.net.InetSocketAddress import java.nio.channels.{ AsynchronousCloseException, @@ -38,14 +37,15 @@ import cats.effect.{Async, Resource} import com.comcast.ip4s.{Dns, Host, SocketAddress} private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( - channelGroup: AsynchronousChannelGroup -)(implicit F: Async[F], F2: Dns[F]) extends IpSocketsProvider[F] { + channelGroup: AsynchronousChannelGroup +)(implicit F: Async[F], F2: Dns[F]) + extends IpSocketsProvider[F] { override def connect( - address: SocketAddress[Host], - options: List[SocketOption] + address: SocketAddress[Host], + options: List[SocketOption] ): Resource[F, Socket[F]] = { - + def setup: Resource[F, AsynchronousSocketChannel] = Resource .make( @@ -59,29 +59,27 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( address.resolve[F].flatMap { ip => F.async[AsynchronousSocketChannel] { cb => F.delay { - ch.connect( - ip.toInetSocketAddress, - null, - new CompletionHandler[Void, Void] { - def completed(result: Void, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(F.delay(ch.close()))) + ch.connect( + ip.toInetSocketAddress, + null, + new CompletionHandler[Void, Void] { + def completed(result: Void, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + }.as(Some(F.delay(ch.close()))) } } setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) - } + } override def bind( - address: SocketAddress[Host], - options: List[SocketOption] + address: SocketAddress[Host], + options: List[SocketOption] ): Resource[F, ServerSocket[F]] = { - val setup: Resource[F, AsynchronousServerSocketChannel] = Resource.eval(address.host.resolve[F]).flatMap { addr => @@ -111,17 +109,16 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( poll { F.async[AsynchronousSocketChannel] { cb => F.delay { - sch.accept( - null, - new CompletionHandler[AsynchronousSocketChannel, Void] { - def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = - cb(Right(ch)) - def failed(rsn: Throwable, attachment: Void): Unit = - cb(Left(rsn)) - } - ) - } - .as(Some(F.delay(sch.close()))) + sch.accept( + null, + new CompletionHandler[AsynchronousSocketChannel, Void] { + def completed(ch: AsynchronousSocketChannel, attachment: Void): Unit = + cb(Right(ch)) + def failed(rsn: Throwable, attachment: Void): Unit = + cb(Left(rsn)) + } + ) + }.as(Some(F.delay(sch.close()))) } } }(ch => F.delay(if (ch.isOpen) ch.close else ())) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index bce0d38a24..93c7ecf94c 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -29,14 +29,30 @@ import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = new SocketGroup[F] { - def client(to: SocketAddress[Host], options: List[SocketOption]) = - ipSockets.connect(to, options) + def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = + new SocketGroup[F] { + def client(to: SocketAddress[Host], options: List[SocketOption]) = + ipSockets.connect(to, options) - def server(address: Option[Host], port: Option[Port], options: List[SocketOption]): Stream[F, Socket[F]] = - Stream.resource(serverResource(address, port, options)).flatMap(_._2) + def server( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Stream[F, Socket[F]] = + Stream.resource(serverResource(address, port, options)).flatMap(_._2) - def serverResource(address: Option[Host], port: Option[Port], options: List[SocketOption]): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets.bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options).evalMap(b => b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept)) - } + def serverResource( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + ipSockets + .bind( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), + options + ) + .evalMap(b => + b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept) + ) + } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 966bf10db3..082c2e3b79 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -51,16 +51,16 @@ private[net] trait SocketInfoCompanionPlatform { } ) - override def supportedOptions: F[Set[SocketOption.Key[_]]] = + override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { channel.supportedOptions.asScala.toSet } override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = asyncInstance.delay { - try { + try Some(channel.getOption(key)) - } catch { + catch { case _: UnsupportedOperationException => None } } @@ -73,4 +73,3 @@ private[net] trait SocketInfoCompanionPlatform { } } - diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index 51210ffc90..973f8150fb 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -108,7 +108,8 @@ private[net] trait SocketCompanionPlatform { readMutex: Mutex[F], writeMutex: Mutex[F] )(implicit F: Async[F]) - extends BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { + extends BufferedReads[F](readMutex) + with SocketInfo.AsyncSocketInfo[F] { protected def asyncInstance = F protected def channel = ch diff --git a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala index e488282e19..f59baabc81 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala @@ -70,9 +70,10 @@ private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) } .map { sch => SocketInfo.forAsync(sch) -> - Resource.makeFull[F, SocketChannel] { poll => - poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) - }(ch => F.blocking(ch.close())) + Resource + .makeFull[F, SocketChannel] { poll => + poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close()))) + }(ch => F.blocking(ch.close())) .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } ) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index 90ef2d62c7..c87d280d9f 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -70,8 +70,12 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ .cancelable(F.blocking(sch.close())) } .map { sch => - def raiseOptionError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("JNR unix server sockets do not support socket options")) + def raiseOptionError[A]: F[A] = + F.raiseError( + new UnsupportedOperationException( + "JNR unix server sockets do not support socket options" + ) + ) val info: SocketInfo[F] = new SocketInfo[F] { def supportedOptions = F.pure(Set.empty) def getOption[A](key: SocketOption.Key[A]) = raiseOptionError @@ -79,9 +83,10 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ def localAddressGen = F.pure(address) } info -> - Resource.makeFull[F, SocketChannel] { poll => - F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) - }(ch => F.blocking(ch.close())) + Resource + .makeFull[F, SocketChannel] { poll => + F.widen(poll(F.blocking(sch.accept).cancelable(F.blocking(sch.close())))) + }(ch => F.blocking(ch.close())) .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 5a8a2692a4..fca4f72622 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -48,7 +48,10 @@ private[net] trait NetworkPlatform[F[_]] { * @param threadCount number of threads to allocate in the fixed thread pool backing the NIO channel group * @param threadFactory factory used to create fixed threads */ - @deprecated("3.13.0", "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead") + @deprecated( + "3.13.0", + "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead" + ) def socketGroup( threadCount: Int = 1, threadFactory: ThreadFactory = ThreadFactories.named("fs2-tcp", true) @@ -73,11 +76,18 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private lazy val globalAdsg = AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) - private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + private def matchAddress[F[_]: ApplicativeThrow, A]( + address: GenSocketAddress, + ifIp: SocketAddress[Host] => F[A], + ifUnix: UnixSocketAddress => F[A] + ): F[A] = address match { case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) + case ua: UnixSocketAddress => ifUnix(ua) + case other => + ApplicativeThrow[F].raiseError( + new UnsupportedOperationException(s"Unsupported address type: $other") + ) } def forIO: Network[IO] = forLiftIO @@ -91,27 +101,34 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private implicit def dns: Dns[F] = Dns.forAsync[F] - private def selecting[A](ifSelecting: SelectingIpSocketsProvider[F] => Resource[F, A], orElse: => Resource[F, A]): Resource[F, A] = + private def selecting[A]( + ifSelecting: SelectingIpSocketsProvider[F] => Resource[F, A], + orElse: => Resource[F, A] + ): Resource[F, A] = Resource.eval(tryGetSelector).flatMap { case Some(selector) => ifSelecting(new SelectingIpSocketsProvider(selector)) - case None => orElse + case None => orElse } def connect( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] = - matchAddress(address, + matchAddress( + address, sa => selecting(_.connect(sa, options), fallback.connect(sa, options)), - ua => fallback.connect(ua, options)) + ua => fallback.connect(ua, options) + ) def bind( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - matchAddress(address, + matchAddress( + address, sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), - ua => fallback.bind(ua, options)) + ua => fallback.bind(ua, options) + ) def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = fallback.datagramSocketGroup(threadFactory) @@ -126,11 +143,15 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N // Implementations of deprecated operations - @deprecated("3.13.0", "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead") + @deprecated( + "3.13.0", + "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead" + ) def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = Resource.eval(tryGetSelector).flatMap { - case Some(selector) => Resource.pure(SocketGroup.fromIpSockets(new SelectingIpSocketsProvider(selector))) - case None => fallback.socketGroup(threadCount, threadFactory) + case Some(selector) => + Resource.pure(SocketGroup.fromIpSockets(new SelectingIpSocketsProvider(selector))) + case None => fallback.socketGroup(threadCount, threadFactory) } } @@ -144,20 +165,24 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) def connect( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options)) + ua => unixSockets.connect(ua, options) + ) def bind( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options)) + ua => unixSockets.bind(ua, options) + ) def openDatagramSocket( address: Option[Host], @@ -180,4 +205,3 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N Resource.pure(SocketGroup.fromIpSockets(ipSockets)) } } - diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index a2a348fbe7..e3b079da9f 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -37,7 +37,9 @@ import java.nio.channels.SelectionKey.OP_CONNECT import java.nio.channels.SocketChannel private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implicit - F: Async[F], F2: LiftIO[F], F3: Dns[F] + F: Async[F], + F2: LiftIO[F], + F3: Dns[F] ) extends IpSocketsProvider[F] { def connect( diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala index 1e0ba9fa76..36740523b4 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala @@ -44,12 +44,12 @@ private final class SelectingSocket[F[_]: LiftIO] private ( writeMutex: Mutex[F], val remoteAddress: F[SocketAddress[IpAddress]] )(implicit F: Async[F]) - extends Socket.BufferedReads(readMutex) with SocketInfo.AsyncSocketInfo[F] { + extends Socket.BufferedReads(readMutex) + with SocketInfo.AsyncSocketInfo[F] { protected def asyncInstance = F protected def channel = ch - override def localAddress: F[SocketAddress[IpAddress]] = asyncInstance.delay( SocketAddress.fromInetSocketAddress( diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 30a57cf69b..5cee78f813 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -93,7 +93,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { } (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => - val acceptIncoming = + val acceptIncoming = Stream .resource(accept.attempt) .flatMap { @@ -118,7 +118,8 @@ private[net] trait UnixSocketsProviderCompanionPlatform { readMutex: Mutex[F], writeMutex: Mutex[F] )(implicit F: Async[F]) - extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { + extends Socket.BufferedReads[F](readMutex) + with SocketInfo.AsyncSocketInfo[F] { protected def asyncInstance = F protected def channel = ch diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 229ae53f46..899005b562 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -100,7 +100,7 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def remoteAddressGen: F[GenSocketAddress] = socket.remoteAddressGen - def supportedOptions: F[Set[SocketOption.Key[_]]] = + def supportedOptions: F[Set[SocketOption.Key[?]]] = socket.supportedOptions def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index eddf76eef7..00cb11b6c6 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -23,7 +23,15 @@ package fs2.io.internal import cats.effect.{Resource, Sync} import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, Ipv4Address, Ipv6Address, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{ + GenSocketAddress, + IpAddress, + Ipv4Address, + Ipv6Address, + Port, + SocketAddress, + UnixSocketAddress +} import java.net.SocketOption import java.net.StandardSocketOptions @@ -208,7 +216,7 @@ private[io] object SocketHelpers { ): F[SocketAddress[IpAddress]] = getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6).map { case a: SocketAddress[IpAddress] @unchecked => a - case _ => throw new IllegalArgumentException + case _ => throw new IllegalArgumentException } def getLocalAddressGen[F[_]](fd: Int, domain: CInt)(implicit @@ -320,7 +328,8 @@ private[io] object SocketHelpers { // FIXME: Scala Native 0.4 doesn't support getsconame for unix sockets; after upgrading to 0.5, // this can be simplified to just allocate a sockaddr_un val (addr, lenValue) = // allocate enough for unix socket address - if (domain == AF_UNIX) (stackalloc[sockaddr_un]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_un].toUInt) + if (domain == AF_UNIX) + (stackalloc[sockaddr_un]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_un].toUInt) else (stackalloc[sockaddr_in6]().asInstanceOf[Ptr[sockaddr]], sizeof[sockaddr_in6].toUInt) val len = stackalloc[socklen_t]() diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala index 6db41d8fe0..22e2c3687c 100644 --- a/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala @@ -23,6 +23,4 @@ package fs2 package io package net -trait DatagramSocketGroup[F[_]] { -} - +trait DatagramSocketGroup[F[_]] {} diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 582c7f62b1..708998212b 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -114,7 +114,9 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As guard(accept(fd, addr, len)) if (clientFd >= 0) { - val address = SocketHelpers.toSocketAddress(addr, if (ipv4) AF_INET else AF_INET6).asInstanceOf[SocketAddress[IpAddress]] + val address = SocketHelpers + .toSocketAddress(addr, if (ipv4) AF_INET else AF_INET6) + .asInstanceOf[SocketAddress[IpAddress]] Right((address, clientFd)) } else Left(()) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index f13bfa0bc7..3d6284dd6a 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -45,29 +45,35 @@ import scala.scalanative.unsigned._ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F: Async[F]) extends UnixSocketsProvider[F] { - def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = for { - poller <- Resource.eval(fileDescriptorPoller[F]) - fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) - _ <- Resource.eval(options.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) - handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) - _ <- Resource.eval { - handle - .pollWriteRec(false) { connected => - if (connected) SocketHelpers.checkSocketError[IO](fd).as(Either.unit) - else - IO { - toSockaddrUn(address.path) { addr => - if (guard(uconnect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) - Left(true) // we will be connected when unblocked - else - Either.unit[Boolean] + def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = + for { + poller <- Resource.eval(fileDescriptorPoller[F]) + fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) + _ <- Resource.eval(options.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) + handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) + _ <- Resource.eval { + handle + .pollWriteRec(false) { connected => + if (connected) SocketHelpers.checkSocketError[IO](fd).as(Either.unit) + else + IO { + toSockaddrUn(address.path) { addr => + if (guard(uconnect(fd, addr, sizeof[sockaddr_un].toUInt)) < 0) + Left(true) // we will be connected when unblocked + else + Either.unit[Boolean] + } } - } - } - .to - } - socket <- FdPollingSocket[F](fd, handle, SocketHelpers.getLocalAddressGen(fd, AF_UNIX), SocketHelpers.getRemoteAddressGen(fd, AF_UNIX)) - } yield socket + } + .to + } + socket <- FdPollingSocket[F]( + fd, + handle, + SocketHelpers.getLocalAddressGen(fd, AF_UNIX), + SocketHelpers.getRemoteAddressGen(fd, AF_UNIX) + ) + } yield socket def bind( address: UnixSocketAddress, @@ -102,7 +108,6 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) - handle <- poller.registerFileDescriptor(fd, true, false).mapK(LiftIO.liftK) _ <- Resource.eval { F.delay { @@ -112,45 +117,56 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F info = new SocketInfo[F] { def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) - def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) + def setOption[A](key: SocketOption.Key[A], value: A) = + SocketHelpers.setOption(fd, key, value) def supportedOptions = ??? def localAddressGen = SocketHelpers.getLocalAddressGen(fd, AF_UNIX) } - clients = Stream.resource { - val accepted = for { - fd <- Resource.makeFull[F, Int] { poll => - poll { - handle - .pollReadRec(()) { _ => - IO { - val clientFd = - if (LinktimeInfo.isLinux) - guard(accept4(fd, null, null, SOCK_NONBLOCK)) + clients = Stream + .resource { + val accepted = for { + fd <- Resource.makeFull[F, Int] { poll => + poll { + handle + .pollReadRec(()) { _ => + IO { + val clientFd = + if (LinktimeInfo.isLinux) + guard(accept4(fd, null, null, SOCK_NONBLOCK)) + else + guard(accept(fd, null, null)) + + if (clientFd >= 0) + Right(clientFd) else - guard(accept(fd, null, null)) - - if (clientFd >= 0) - Right(clientFd) - else - Left(()) + Left(()) + } } - } - .to - } - }(fd => F.delay(guard_(close(fd)))) - _ <- - if (!LinktimeInfo.isLinux) - Resource.eval(setNonBlocking(fd)) - else Resource.unit[F] - - _ <- Resource.eval(filteredOptions.traverse(so => SocketHelpers.setOption(fd, so.key, so.value))) - handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) - socket <- FdPollingSocket[F](fd, handle, SocketHelpers.getLocalAddressGen(fd, AF_UNIX), SocketHelpers.getRemoteAddressGen(fd, AF_UNIX)) - } yield socket - - accepted.attempt.map(_.toOption) - }.repeat.unNone + .to + } + }(fd => F.delay(guard_(close(fd)))) + _ <- + if (!LinktimeInfo.isLinux) + Resource.eval(setNonBlocking(fd)) + else Resource.unit[F] + + _ <- Resource.eval( + filteredOptions.traverse(so => SocketHelpers.setOption(fd, so.key, so.value)) + ) + handle <- poller.registerFileDescriptor(fd, true, true).mapK(LiftIO.liftK) + socket <- FdPollingSocket[F]( + fd, + handle, + SocketHelpers.getLocalAddressGen(fd, AF_UNIX), + SocketHelpers.getRemoteAddressGen(fd, AF_UNIX) + ) + } yield socket + + accepted.attempt.map(_.toOption) + } + .repeat + .unNone } yield ServerSocket(info, clients) } diff --git a/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala index 4dc3493af0..19a791e954 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkLowPriority.scala @@ -27,7 +27,7 @@ private[fs2] trait NetworkLowPriority { this: Network.type => @deprecated("Add Network constraint or use forAsync", "3.7.0") implicit def implicitForAsync[F[_]: Async]: Network[F] = Async[F] match { - case l: LiftIO[_] => forLiftIO(Async[F], l.asInstanceOf[LiftIO[F]]) - case _ => throw new UnsupportedOperationException + case l: LiftIO[?] => forLiftIO(Async[F], l.asInstanceOf[LiftIO[F]]) + case _ => throw new UnsupportedOperationException } } diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index 279263e6cd..75c2b3c65e 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -28,41 +28,61 @@ import cats.effect.IO import cats.effect.LiftIO import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{ + Dns, + GenSocketAddress, + Host, + IpAddress, + Port, + SocketAddress, + UnixSocketAddress +} private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - private def matchAddress[F[_]: ApplicativeThrow, A](address: GenSocketAddress, ifIp: SocketAddress[Host] => F[A], ifUnix: UnixSocketAddress => F[A]): F[A] = + private def matchAddress[F[_]: ApplicativeThrow, A]( + address: GenSocketAddress, + ifIp: SocketAddress[Host] => F[A], + ifUnix: UnixSocketAddress => F[A] + ): F[A] = address match { case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => ApplicativeThrow[F].raiseError(new UnsupportedOperationException(s"Unsupported address type: $other")) + case ua: UnixSocketAddress => ifUnix(ua) + case other => + ApplicativeThrow[F].raiseError( + new UnsupportedOperationException(s"Unsupported address type: $other") + ) } def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = new AsyncNetwork[F] { - private lazy val ipSockets = new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) + private lazy val ipSockets = + new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) private lazy val unixSockets = new FdPollingUnixSocketsProvider[F] def connect( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options)) + ua => unixSockets.connect(ua, options) + ) def bind( - address: GenSocketAddress, - options: List[SocketOption] + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - matchAddress(address, + matchAddress( + address, sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options)) + ua => unixSockets.bind(ua, options) + ) } } diff --git a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 992e84153f..511f15e216 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -97,7 +97,7 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def remoteAddressGen: F[GenSocketAddress] = socket.remoteAddressGen - def supportedOptions: F[Set[SocketOption.Key[_]]] = + def supportedOptions: F[Set[SocketOption.Key[?]]] = socket.supportedOptions def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = diff --git a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala index 04fd5dc7db..ab49f57c72 100644 --- a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -29,15 +29,14 @@ import com.comcast.ip4s.{Host, SocketAddress} private[net] trait IpSocketsProvider[F[_]] { def connect( - address: SocketAddress[Host], - options: List[SocketOption] + address: SocketAddress[Host], + options: List[SocketOption] ): Resource[F, Socket[F]] def bind( - address: SocketAddress[Host], - options: List[SocketOption] + address: SocketAddress[Host], + options: List[SocketOption] ): Resource[F, ServerSocket[F]] } private[net] object IpSocketsProvider extends IpSocketsProviderCompanionPlatform - diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 0e4319aac4..e0868cb118 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -56,9 +56,15 @@ sealed trait Network[F[_]] def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] - def bind(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, ServerSocket[F]] + def bind( + address: GenSocketAddress, + options: List[SocketOption] = Nil + ): Resource[F, ServerSocket[F]] - def bindAndAccept(address: GenSocketAddress, options: List[SocketOption] = Nil): Stream[F, Socket[F]] + def bindAndAccept( + address: GenSocketAddress, + options: List[SocketOption] = Nil + ): Stream[F, Socket[F]] /** Returns a builder for `TLSContext[F]` values. * @@ -71,18 +77,27 @@ object Network extends NetworkCompanionPlatform { private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { - - override def connect(address: GenSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] - override def bind(address: GenSocketAddress, options: List[SocketOption]): Resource[F, ServerSocket[F]] + override def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] + + override def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] - override def bindAndAccept(address: GenSocketAddress, options: List[SocketOption]): Stream[F, Socket[F]] = + override def bindAndAccept( + address: GenSocketAddress, + options: List[SocketOption] + ): Stream[F, Socket[F]] = Stream.resource(bind(address, options)).flatMap(_.accept) override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] // Implementations of deprecated operations - + override def client( to: SocketAddress[Host], options: List[SocketOption] @@ -99,10 +114,16 @@ object Network extends NetworkCompanionPlatform { port: Option[Port], options: List[SocketOption] ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - bind(SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options) - .flatMap(b => Resource.eval(b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]])).tupleRight(b.accept)) + bind( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), + options + ) + .flatMap(b => + Resource + .eval(b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]])) + .tupleRight(b.accept) + ) } def apply[F[_]](implicit F: Network[F]): F.type = F } - diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 3e66b67256..705f28560a 100644 --- a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -24,14 +24,14 @@ package io package net /** Represents a bound TCP server socket. - * - * Note some platforms do not support getting and setting socket options on server sockets - * so take care when using `getOption` and `setOption`. - * - * Client sockets can be accepted by pulling on the `accept` stream. A concurrent server - * that is limited to `maxAccept` concurrent clients is accomplished by - * `b.accept.map(handleClientSocket).parJoin(maxAccept)`. - */ + * + * Note some platforms do not support getting and setting socket options on server sockets + * so take care when using `getOption` and `setOption`. + * + * Client sockets can be accepted by pulling on the `accept` stream. A concurrent server + * that is limited to `maxAccept` concurrent clients is accomplished by + * `b.accept.map(handleClientSocket).parJoin(maxAccept)`. + */ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ @@ -39,7 +39,7 @@ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { } object ServerSocket { - + def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { val accept0 = accept new ServerSocket[F] { @@ -51,4 +51,3 @@ object ServerSocket { } } } - diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 8d9b23d3b1..7da30ab1a0 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -30,7 +30,7 @@ trait SocketInfo[F[_]] { /** Asks for the local address of the socket. */ def localAddressGen: F[GenSocketAddress] - def supportedOptions: F[Set[SocketOption.Key[_]]] + def supportedOptions: F[Set[SocketOption.Key[?]]] def getOption[A](key: SocketOption.Key[A]): F[Option[A]] @@ -38,4 +38,3 @@ trait SocketInfo[F[_]] { } object SocketInfo extends SocketInfoCompanionPlatform - diff --git a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala index d9da6ebc7e..f84b8ec0f2 100644 --- a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala @@ -29,13 +29,14 @@ import com.comcast.ip4s.UnixSocketAddress private[net] trait UnixSocketsProvider[F[_]] { def connect( - address: UnixSocketAddress, - options: List[SocketOption] + address: UnixSocketAddress, + options: List[SocketOption] ): Resource[F, Socket[F]] def bind( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]]} + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] +} private[net] object UnixSocketsProvider extends UnixSocketsProviderCompanionPlatform diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 5e29db3511..d1b909b2ef 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -72,10 +72,16 @@ object UnixSockets { deleteIfExists: Boolean, deleteOnClose: Boolean ): Stream[F, Socket[F]] = - Stream.resource( - delegate.bind(Ip4sUnixSocketAddress(address.path), - List(SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), - SocketOption.unixServerSocketDeleteOnClose(deleteOnClose))) - ).flatMap(_.accept) + Stream + .resource( + delegate.bind( + Ip4sUnixSocketAddress(address.path), + List( + SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), + SocketOption.unixServerSocketDeleteOnClose(deleteOnClose) + ) + ) + ) + .flatMap(_.accept) } } diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index e75e3d0345..64a33d1063 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -33,7 +33,8 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { test(s"echoes - $provider") { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") - val server = Stream.resource(sockets.bind(address, Nil)) + val server = Stream + .resource(sockets.bind(address, Nil)) .flatMap(_.accept) .map { client => client.reads.through(client.writes) From 906d8d52b4df37b18943701119384ca547875323 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 17 Apr 2025 09:05:59 -0400 Subject: [PATCH 19/79] Fix compilation errors --- .../scala/fs2/io/net/SocketInfoPlatform.scala | 2 +- .../io/net/UnixSocketsProviderPlatform.scala | 7 +++- .../io/net/IpSocketsProviderPlatform.scala | 32 ++++++++++++++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 2 +- .../io/net/IpSocketsProviderPlatform.scala | 33 +++++++++++++++++++ .../fs2/io/net/unixsocket/UnixSockets.scala | 8 ++--- 6 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala create mode 100644 io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala diff --git a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 9fbc0af142..b3587873c4 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import com.comcast.ip4s.{GenSocketAddress, SocketAddress} +import com.comcast.ip4s.GenSocketAddress import cats.effect.Async import fs2.io.internal.facade diff --git a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 3934b11199..7a2d345a03 100644 --- a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -23,13 +23,18 @@ package fs2 package io package net -import cats.effect.{Async, Resource} +import cats.effect.{Async, LiftIO, Resource} import cats.syntax.all._ import com.comcast.ip4s.UnixSocketAddress import fs2.io.file.{Files, Path} private[net] trait UnixSocketsProviderCompanionPlatform { + private[net] def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { + val _ = LiftIO[F] + forAsync[F] + } + private[net] def forAsync[F[_]: Async]: UnixSocketsProvider[F] = forAsyncAndFiles(implicitly, Files.forAsync[F]) diff --git a/io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala new file mode 100644 index 0000000000..e068553171 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.Async + +private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => + + private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = + AsynchronousChannelGroupIpSocketsProvider.forAsync[F] +} diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index fca4f72622..1dea8c52ca 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -160,7 +160,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = new AsyncNetwork[F] { - private lazy val ipSockets = AsynchronousChannelGroupIpSocketsProvider.forAsync[F] + private lazy val ipSockets = IpSocketsProvider.forAsync[F] private lazy val unixSockets = UnixSocketsProvider.forAsync[F] private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) diff --git a/io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala new file mode 100644 index 0000000000..e4334d35cc --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, LiftIO} +import com.comcast.ip4s.Dns + +private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => + + private[net] def forLiftIO[F[_]: Async: LiftIO]: IpSocketsProvider[F] = + new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) +} diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index d1b909b2ef..3604c23456 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -55,14 +55,12 @@ object UnixSockets { def forIO: UnixSockets[IO] = forLiftIO - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { - val _ = LiftIO[F] + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = new AsyncUnixSockets[F] - } - private class AsyncUnixSockets[F[_]: Async] extends UnixSockets[F] { + private class AsyncUnixSockets[F[_]: Async: LiftIO] extends UnixSockets[F] { - private val delegate = UnixSocketsProvider.forAsync[F] + private val delegate = UnixSocketsProvider.forLiftIO[F] def client(address: UnixSocketAddress): Resource[F, Socket[F]] = delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) From 058e616686cd41579f66c385932d8b860979060f Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 17 Apr 2025 09:31:39 -0400 Subject: [PATCH 20/79] Implement getLocalAddressGen on JVM --- .../fs2/io/net/SocketAddressHelpers.scala | 22 +++++++++++++++++++ .../scala/fs2/io/net/SocketInfoPlatform.scala | 10 ++------- .../scala/fs2/io/net/SocketPlatform.scala | 13 ++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala new file mode 100644 index 0000000000..dcbd7fd90c --- /dev/null +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -0,0 +1,22 @@ +package fs2 +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, SocketAddress, UnixSocketAddress} +import java.net.{InetSocketAddress, UnixDomainSocketAddress} +import jnr.unixsocket.{UnixSocketAddress => JnrUnixSocketAddress} + +private[net] object SocketAddressHelpers { + + def toGenSocketAddress(address: java.net.SocketAddress): GenSocketAddress = { + address match { + case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) + case _ => + if (JdkUnixSocketsProvider.supported && address.isInstanceOf[UnixDomainSocketAddress]) { + UnixSocketAddress(address.asInstanceOf[UnixDomainSocketAddress].getPath.toString) + } else if (JnrUnixSocketsProvider.supported && address.isInstanceOf[JnrUnixSocketAddress]) { + UnixSocketAddress(address.asInstanceOf[JnrUnixSocketAddress].path) + } else throw new IllegalArgumentException("Unsupported address type: " + address) + } + } +} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 082c2e3b79..3d8b549231 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -23,10 +23,9 @@ package fs2 package io package net -import com.comcast.ip4s.{GenSocketAddress, SocketAddress} +import com.comcast.ip4s.GenSocketAddress import cats.effect.Async -import java.net.InetSocketAddress import java.nio.channels.NetworkChannel import scala.jdk.CollectionConverters.* @@ -44,12 +43,7 @@ private[net] trait SocketInfoCompanionPlatform { protected def channel: NetworkChannel override def localAddressGen: F[GenSocketAddress] = - asyncInstance.delay( - channel.getLocalAddress match { - case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) - // TODO handle unix sockets - } - ) + asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress)) override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index 973f8150fb..d35258a7b5 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -124,7 +124,7 @@ private[net] trait SocketCompanionPlatform { F.delay(Some(endOfInput.voidError)) } - def write(bytes: Chunk[Byte]): F[Unit] = { + override def write(bytes: Chunk[Byte]): F[Unit] = { def go(buff: ByteBuffer): F[Unit] = F.async[Int] { cb => ch.write( @@ -150,23 +150,24 @@ private[net] trait SocketCompanionPlatform { ) ) - def remoteAddress: F[SocketAddress[IpAddress]] = + override def remoteAddress: F[SocketAddress[IpAddress]] = F.delay( SocketAddress.fromInetSocketAddress( ch.getRemoteAddress.asInstanceOf[InetSocketAddress] ) ) - override def remoteAddressGen: F[GenSocketAddress] = ??? + override def remoteAddressGen: F[GenSocketAddress] = + F.delay(SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress)) - def isOpen: F[Boolean] = F.delay(ch.isOpen) + override def isOpen: F[Boolean] = F.delay(ch.isOpen) - def endOfOutput: F[Unit] = + override def endOfOutput: F[Unit] = F.delay { ch.shutdownOutput(); () } - def endOfInput: F[Unit] = + override def endOfInput: F[Unit] = F.delay { ch.shutdownInput(); () } From 7648b71983e9457c0122c0366c2e43334662a745 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 07:56:19 -0400 Subject: [PATCH 21/79] Cleanup --- .../scala/fs2/io/net/SocketInfoPlatform.scala | 31 +----------------- .../scala/fs2/io/net/SocketPlatform.scala | 32 +++++++++++-------- .../fs2/io/net/SocketAddressHelpers.scala | 3 +- .../io/net/UnixSocketsProviderPlatform.scala | 3 +- .../scala/fs2/io/net/FdPollingSocket.scala | 10 ++---- .../main/scala/fs2/io/net/SocketInfo.scala | 14 ++++++-- 6 files changed, 36 insertions(+), 57 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index b3587873c4..e106b1c34e 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -23,33 +23,4 @@ package fs2 package io package net -import com.comcast.ip4s.GenSocketAddress -import cats.effect.Async -import fs2.io.internal.facade - -private[net] trait SocketInfoCompanionPlatform { - private[net] def forAsync[F[_]](sock: facade.net.Socket)(implicit F: Async[F]): SocketInfo[F] = { - val sock0 = sock - new AsyncSocketInfo[F] { - def asyncInstance = F - def sock: facade.net.Socket = sock0 - } - } - - private[net] trait AsyncSocketInfo[F[_]] extends SocketInfo[F] { - - implicit protected def asyncInstance: Async[F] - - protected def sock: facade.net.Socket - - override def localAddressGen: F[GenSocketAddress] = ??? - - override def supportedOptions: F[Set[SocketOption.Key[?]]] = ??? - - override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = - key.get(sock) - - override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = - key.set(sock, value) - } -} +private[net] trait SocketInfoCompanionPlatform diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 8fe6390f0f..3201281995 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -83,22 +83,26 @@ private[net] trait SocketCompanionPlatform { override def isOpen: F[Boolean] = F.delay(sock.readyState == "open") override def localAddress: F[SocketAddress[IpAddress]] = - for { - ip <- F.delay(sock.localAddress.toOption.flatMap(IpAddress.fromString).get) - port <- F.delay(sock.localPort.toOption.map(_.toInt).flatMap(Port.fromInt).get) - } yield SocketAddress(ip, port) - - override def localAddressGen: F[GenSocketAddress] = - ??? + SocketInfo.downcastAddress(localAddressGen) + + override def localAddressGen: F[GenSocketAddress] = F.delay { + val address = sock.address() + if (address.port ne null) + SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) + else + UnixSocketAddress(address.path) + } override def remoteAddress: F[SocketAddress[IpAddress]] = - for { - ip <- F.delay(sock.remoteAddress.toOption.flatMap(IpAddress.fromString).get) - port <- F.delay(sock.remotePort.toOption.map(_.toInt).flatMap(Port.fromInt).get) - } yield SocketAddress(ip, port) - - override def remoteAddressGen: F[GenSocketAddress] = - ??? + SocketInfo.downcastAddress(remoteAddressGen) + + override def remoteAddressGen: F[GenSocketAddress] = F.delay { + val address = sock.remoteAddress() + if (address.port ne null) + SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) + else + UnixSocketAddress(address.path) + } override def supportedOptions: F[Set[SocketOption.Key[?]]] = ??? diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index dcbd7fd90c..a8b4c84783 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -8,7 +8,7 @@ import jnr.unixsocket.{UnixSocketAddress => JnrUnixSocketAddress} private[net] object SocketAddressHelpers { - def toGenSocketAddress(address: java.net.SocketAddress): GenSocketAddress = { + def toGenSocketAddress(address: java.net.SocketAddress): GenSocketAddress = address match { case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) case _ => @@ -18,5 +18,4 @@ private[net] object SocketAddressHelpers { UnixSocketAddress(address.asInstanceOf[JnrUnixSocketAddress].path) } else throw new IllegalArgumentException("Unsupported address type: " + address) } - } } diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 5cee78f813..d0c0f0fc00 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -142,7 +142,8 @@ private[net] trait UnixSocketsProviderCompanionPlatform { def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - def remoteAddressGen: F[GenSocketAddress] = ??? // TODO + def remoteAddressGen: F[GenSocketAddress] = + asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress)) private def raiseIpAddressError[A]: F[A] = F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index c4b00363ad..b6942786bc 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -47,14 +47,8 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( )(implicit F: Async[F]) extends Socket[F] { - def localAddress = downcastAddress(localAddressGen) - def remoteAddress = downcastAddress(remoteAddressGen) - - private def downcastAddress(address: F[GenSocketAddress]): F[SocketAddress[IpAddress]] = - address.flatMap { - case a: SocketAddress[IpAddress] @unchecked => F.pure(a) - case _ => F.raiseError(new UnsupportedOperationException("invalid address type")) - } + def localAddress = SocketInfo.downcastAddress(localAddressGen) + def remoteAddress = SocketInfo.downcastAddress(remoteAddressGen) def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 7da30ab1a0..1b80e6ee65 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -23,7 +23,9 @@ package fs2 package io package net -import com.comcast.ip4s.GenSocketAddress +import cats.MonadThrow +import cats.syntax.all._ +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} trait SocketInfo[F[_]] { @@ -37,4 +39,12 @@ trait SocketInfo[F[_]] { def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] } -object SocketInfo extends SocketInfoCompanionPlatform +object SocketInfo extends SocketInfoCompanionPlatform { + private[net] def downcastAddress[F[_]: MonadThrow]( + address: F[GenSocketAddress] + ): F[SocketAddress[IpAddress]] = + address.flatMap { + case a: SocketAddress[IpAddress] @unchecked => MonadThrow[F].pure(a) + case _ => MonadThrow[F].raiseError(new UnsupportedOperationException("invalid address type")) + } +} From 01322d5429a24ff00ca9023d73c35328f040b422 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 15:37:35 -0400 Subject: [PATCH 22/79] Progress on fixing addresses --- .../scala/fs2/io/internal/facade/net.scala | 7 +-- .../fs2/io/net/AsyncSocketsProvider.scala | 47 +++++++++++++++---- .../fs2/io/net/SocketGroupPlatform.scala | 24 ++++++++-- .../scala/fs2/io/net/SocketPlatform.scala | 29 ++++-------- .../fs2/io/net/tls/TLSSocketPlatform.scala | 5 +- .../fs2/io/net/SocketAddressHelpers.scala | 4 +- .../io/net/UnixSocketsProviderPlatform.scala | 27 +++++++---- .../scala/fs2/io/net/FdPollingSocket.scala | 2 +- .../scala/fs2/io/net/NetworkPlatform.scala | 2 - .../fs2/io/net/SocketAddressHelpers.scala | 17 +++++++ .../src/main/scala/fs2/io/net/Socket.scala | 3 +- .../main/scala/fs2/io/net/SocketInfo.scala | 6 +-- .../scala/fs2/io/net/UnixSocketsSuite.scala | 19 ++++++-- .../scala/fs2/io/net/tcp/SocketSuite.scala | 4 ++ 14 files changed, 135 insertions(+), 61 deletions(-) rename io/{jvm-native => jvm}/src/main/scala/fs2/io/net/SocketAddressHelpers.scala (83%) create mode 100644 io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala diff --git a/io/js/src/main/scala/fs2/io/internal/facade/net.scala b/io/js/src/main/scala/fs2/io/internal/facade/net.scala index ac81493d05..e7ef9d342e 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/net.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/net.scala @@ -41,7 +41,7 @@ private[io] object net { @js.native trait Server extends EventEmitter { - def address(): ServerAddress = js.native + def address(): js.UndefOr[BoundAddress] = js.native def listening: Boolean = js.native @@ -56,10 +56,9 @@ private[io] object net { } @js.native - trait ServerAddress extends js.Object { + trait BoundAddress extends js.Object { def address: String = js.native def port: Int = js.native - def path: String = js.native } trait ServerOptions extends js.Object { @@ -101,6 +100,8 @@ private[io] object net { def remotePort: js.UndefOr[Int] = js.native + def remoteFamily: js.UndefOr[String] = js.native + def end(): Socket = js.native def setEncoding(encoding: String): Socket = js.native diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 24b80debf0..b6e0ef42ff 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -27,7 +27,7 @@ import cats.effect.{Async, Resource} import cats.effect.std.Dispatcher import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{IpAddress, GenSocketAddress, Port, SocketAddress, UnixSocketAddress} import fs2.concurrent.Channel import fs2.io.internal.facade @@ -55,7 +55,21 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } ) .evalTap(setSocketOptions(options)) - socket <- Socket.forAsync(sock) + localAddressGen = F.delay { + (to match { + case Left(_) => + SocketAddress(IpAddress.fromString(sock.localAddress.get).get, Port.fromInt(sock.localPort.get).get) + case Right(_) => UnixSocketAddress("") + }): GenSocketAddress + } + remoteAddressGen = F.delay { + (to match { + case Left(_) => + SocketAddress(IpAddress.fromString(sock.remoteAddress.get).get, Port.fromInt(sock.remotePort.get).get) + case Right(addr) => addr + }): GenSocketAddress + } + socket <- Socket.forAsync(sock, localAddressGen, remoteAddressGen) _ <- F .async[Unit] { cb => sock @@ -119,11 +133,12 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { .toResource info = new SocketInfo[F] { def localAddressGen = F.delay { - val address = server.address() - if (address.port ne null) - SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) - else - UnixSocketAddress(address.path) + address match { + case Left(_) => + val addr = server.address().get + SocketAddress(IpAddress.fromString(addr.address).get, Port.fromInt(addr.port).get) + case Right(addr) => addr + } } def getOption[A](key: SocketOption.Key[A]) = @@ -135,6 +150,22 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } sockets = channel.stream .evalTap(setSocketOptions(options)) - .flatMap(sock => Stream.resource(Socket.forAsync(sock))) + .flatMap { sock => + val localAddressGen = F.delay { + (address match { + case Left(_) => + SocketAddress(IpAddress.fromString(sock.localAddress.get).get, Port.fromInt(sock.localPort.get).get) + case Right(addr) => addr + }): GenSocketAddress + } + val remoteAddressGen = F.delay { + (address match { + case Left(_) => + SocketAddress(IpAddress.fromString(sock.remoteAddress.get).get, Port.fromInt(sock.remotePort.get).get) + case Right(_) => UnixSocketAddress("") + }): GenSocketAddress + } + Stream.resource(Socket.forAsync(sock, localAddressGen, remoteAddressGen)) + } } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala index 632595edf9..2d171d7217 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala @@ -28,12 +28,13 @@ import cats.effect.kernel.Resource import cats.effect.std.Dispatcher import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress} +import com.comcast.ip4s.{Host, GenSocketAddress, IpAddress, Port, SocketAddress, UnixSocketAddress} import fs2.concurrent.Channel import fs2.io.internal.facade import scala.scalajs.js +// TODO replace this implementation with delegation to IpSocketsProvider private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => private[net] def forAsync[F[_]: Async]: SocketGroup[F] = new AsyncSocketGroup[F] @@ -61,7 +62,14 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => } ) .evalTap(setSocketOptions(options)) - socket <- Socket.forAsync(sock) + + localAddressGen = F.delay { + UnixSocketAddress("TODO3 - LOCAL"): GenSocketAddress + } + remoteAddressGen = F.delay { + UnixSocketAddress("TODO3 - REMOTE"): GenSocketAddress + } + socket <- Socket.forAsync(sock, localAddressGen, remoteAddressGen) _ <- F .async[Unit] { cb => sock @@ -119,12 +127,20 @@ private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => } .toResource ipAddress <- F.delay { - val info = server.address() + val info = server.address().asInstanceOf[facade.net.BoundAddress] SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port).get) }.toResource sockets = channel.stream .evalTap(setSocketOptions(options)) - .flatMap(sock => Stream.resource(Socket.forAsync(sock))) + .flatMap { sock => + val localAddressGen = F.delay { + UnixSocketAddress("TODO - ACCEPT - LOCAL"): GenSocketAddress + } + val remoteAddressGen = F.delay { + UnixSocketAddress("TODO - ACCEPT - REMOTE"): GenSocketAddress + } + Stream.resource(Socket.forAsync(sock, localAddressGen, remoteAddressGen)) + } } yield (ipAddress, sockets)).adaptError { case IOException(ex) => ex } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 3201281995..90c7a5b723 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -25,21 +25,22 @@ package net import cats.data.{Kleisli, OptionT} import cats.effect.{Async, Resource} -import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, Port, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.internal.{facade, SuspendedStream} private[net] trait SocketCompanionPlatform { private[net] def forAsync[F[_]]( - sock: facade.net.Socket + sock: facade.net.Socket, + localAddressGen: F[GenSocketAddress], + remoteAddressGen: F[GenSocketAddress] )(implicit F: Async[F]): Resource[F, Socket[F]] = suspendReadableAndRead( destroyIfNotEnded = false, destroyIfCanceled = false )(sock.asInstanceOf[Readable]) .flatMap { case (_, stream) => - SuspendedStream(stream).map(new AsyncSocket(sock, _)) + SuspendedStream(stream).map(new AsyncSocket(sock, _, localAddressGen, remoteAddressGen)) } .onFinalize { F.delay { @@ -50,7 +51,9 @@ private[net] trait SocketCompanionPlatform { private[net] class AsyncSocket[F[_]]( sock: facade.net.Socket, - readStream: SuspendedStream[F, Byte] + readStream: SuspendedStream[F, Byte], + localAddressGen0: F[GenSocketAddress], + remoteAddressGen0: F[GenSocketAddress] )(implicit F: Async[F]) extends Socket[F] { @@ -85,24 +88,12 @@ private[net] trait SocketCompanionPlatform { override def localAddress: F[SocketAddress[IpAddress]] = SocketInfo.downcastAddress(localAddressGen) - override def localAddressGen: F[GenSocketAddress] = F.delay { - val address = sock.address() - if (address.port ne null) - SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) - else - UnixSocketAddress(address.path) - } + override def localAddressGen = localAddressGen0 override def remoteAddress: F[SocketAddress[IpAddress]] = SocketInfo.downcastAddress(remoteAddressGen) - override def remoteAddressGen: F[GenSocketAddress] = F.delay { - val address = sock.remoteAddress() - if (address.port ne null) - SocketAddress(IpAddress.fromString(address.address).get, Port.fromInt(address.port).get) - else - UnixSocketAddress(address.path) - } + override def remoteAddressGen = remoteAddressGen0 override def supportedOptions: F[Set[SocketOption.Key[?]]] = ??? diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 5f9e955278..f8d6a44c09 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -28,6 +28,7 @@ import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.effect.syntax.all._ import cats.syntax.all._ +import com.comcast.ip4s.GenSocketAddress import fs2.io.internal.facade import fs2.io.internal.SuspendedStream import scodec.bits.ByteVector @@ -67,8 +68,8 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => readStream: SuspendedStream[F, Byte], underlying: Socket[F], val session: F[SSLSession], - val applicationProtocol: F[String] - ) extends Socket.AsyncSocket[F](sock, readStream) + val applicationProtocol: F[String], + ) extends Socket.AsyncSocket[F](sock, readStream, Async[F].delay(sys.error("unused")), Async[F].delay(sys.error("unused"))) with UnsealedTLSSocket[F] { override def localAddress = underlying.localAddress override def localAddressGen = underlying.localAddressGen diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala similarity index 83% rename from io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala rename to io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index a8b4c84783..7e66e4e18a 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -3,12 +3,12 @@ package io package net import com.comcast.ip4s.{GenSocketAddress, SocketAddress, UnixSocketAddress} -import java.net.{InetSocketAddress, UnixDomainSocketAddress} +import java.net.{SocketAddress => JSocketAddress, InetSocketAddress, UnixDomainSocketAddress} import jnr.unixsocket.{UnixSocketAddress => JnrUnixSocketAddress} private[net] object SocketAddressHelpers { - def toGenSocketAddress(address: java.net.SocketAddress): GenSocketAddress = + def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = address match { case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) case _ => diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index d0c0f0fc00..55497a751a 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -65,7 +65,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = - openChannel(address).evalMap(makeSocket[F](_)) + openChannel(address).evalMap(makeSocket[F](_, UnixSocketAddress(""), address)) def bind( address: UnixSocketAddress, @@ -98,7 +98,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { .resource(accept.attempt) .flatMap { case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(makeSocket(accepted)) + case Right(accepted) => Stream.eval(makeSocket(accepted, address, UnixSocketAddress(""))) } .repeat ServerSocket(info, acceptIncoming) @@ -107,16 +107,20 @@ private[net] trait UnixSocketsProviderCompanionPlatform { } private def makeSocket[F[_]: Async]( - ch: SocketChannel + ch: SocketChannel, + localAddress: UnixSocketAddress, + remoteAddress: UnixSocketAddress ): F[Socket[F]] = (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => - new AsyncSocket[F](ch, readMutex, writeMutex) + new AsyncSocket[F](ch, readMutex, writeMutex, localAddress, remoteAddress) } private final class AsyncSocket[F[_]]( ch: SocketChannel, readMutex: Mutex[F], - writeMutex: Mutex[F] + writeMutex: Mutex[F], + localAddress0: UnixSocketAddress, + remoteAddress0: UnixSocketAddress )(implicit F: Async[F]) extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { @@ -138,15 +142,18 @@ private[net] trait UnixSocketsProviderCompanionPlatform { } } + private def raiseIpAddressError[A]: F[A] = + F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) + override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError + override def localAddressGen: F[GenSocketAddress] = + F.pure(localAddress0) - def remoteAddressGen: F[GenSocketAddress] = - asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress)) + override def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - private def raiseIpAddressError[A]: F[A] = - F.raiseError(new UnsupportedOperationException("Unix sockets do not use IP addressing")) + override def remoteAddressGen: F[GenSocketAddress] = + F.pure(remoteAddress0) def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index b6942786bc..9992a324ae 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -24,7 +24,7 @@ package io.net import cats.effect.{Async, FileDescriptorPollHandle, IO, LiftIO, Resource} import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import com.comcast.ip4s.GenSocketAddress import fs2.io.internal.NativeUtil._ import fs2.io.internal.{ResizableBuffer, SocketHelpers} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index 75c2b3c65e..f601fffe8d 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -32,8 +32,6 @@ import com.comcast.ip4s.{ Dns, GenSocketAddress, Host, - IpAddress, - Port, SocketAddress, UnixSocketAddress } diff --git a/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala new file mode 100644 index 0000000000..989e6197f6 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -0,0 +1,17 @@ +package fs2 +package io +package net + +import com.comcast.ip4s.{GenSocketAddress, SocketAddress} +import java.net.{SocketAddress => JSocketAddress, InetSocketAddress} + +private[net] object SocketAddressHelpers { + + def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = { + address match { + case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) + case _ => throw new IllegalArgumentException("Unsupported address type: " + address) + } + } +} + diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index c6fd098168..33ec78d860 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -55,12 +55,13 @@ trait Socket[F[_]] extends SocketInfo[F] { def isOpen: F[Boolean] + /** Asks for the local address of this socket. */ def localAddress: F[SocketAddress[IpAddress]] /** Asks for the remote address of the peer. */ def remoteAddress: F[SocketAddress[IpAddress]] - // TODO + /** Asks for the remote address of the peer. Like `remoteAddress` but supports unix sockets as well. */ def remoteAddressGen: F[GenSocketAddress] /** Writes `bytes` to the peer. diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 1b80e6ee65..43328e100d 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -29,7 +29,7 @@ import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} trait SocketInfo[F[_]] { - /** Asks for the local address of the socket. */ + /** Asks for the local address of this socket. Like `localAddress` but supports unix sockets as well. */ def localAddressGen: F[GenSocketAddress] def supportedOptions: F[Set[SocketOption.Key[?]]] @@ -40,9 +40,7 @@ trait SocketInfo[F[_]] { } object SocketInfo extends SocketInfoCompanionPlatform { - private[net] def downcastAddress[F[_]: MonadThrow]( - address: F[GenSocketAddress] - ): F[SocketAddress[IpAddress]] = + private[net] def downcastAddress[F[_]: MonadThrow](address: F[GenSocketAddress]): F[SocketAddress[IpAddress]] = address.flatMap { case a: SocketAddress[IpAddress] @unchecked => MonadThrow[F].pure(a) case _ => MonadThrow[F].raiseError(new UnsupportedOperationException("invalid address type")) diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 64a33d1063..6a4e4cdbaa 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -35,14 +35,23 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { val server = Stream .resource(sockets.bind(address, Nil)) - .flatMap(_.accept) - .map { client => - client.reads.through(client.writes) + .flatMap(ss => + Stream.exec(ss.localAddressGen.flatMap(a => IO.println("Server Local: " + a))) ++ + ss.accept + ) + // TODO + // .flatMap(_.accept) + .map { socket => + Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ + Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ + socket.reads.through(socket.writes) } .parJoinUnbounded - def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { server => - server.write(msg) *> server.endOfOutput *> server.reads.compile + def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { socket => + socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a)) *> + socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) *> + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile .to(Chunk) .map(read => assertEquals(read, msg)) } diff --git a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala index e889f92766..df6958796a 100644 --- a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala @@ -54,6 +54,8 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .resource(setup) .flatMap { case (server, clients) => val echoServer = server.map { socket => + Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ + Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ socket.reads .through(socket.writes) .onFinalize(socket.endOfOutput) @@ -62,6 +64,8 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val msgClients = clients .take(clientCount) .map { socket => + Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a))) ++ + Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a))) ++ Stream .chunk(message) .through(socket.writes) From 42773cea6173dc8f54948973cb3593105914c1a5 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 15:44:01 -0400 Subject: [PATCH 23/79] Fixed addresses --- .../fs2/io/net/SocketGroupPlatform.scala | 148 ------------------ .../fs2/io/net/SocketGroupPlatform.scala | 58 ------- .../main/scala/fs2/io/net/SocketGroup.scala | 44 ++++-- 3 files changed, 27 insertions(+), 223 deletions(-) delete mode 100644 io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala delete mode 100644 io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala diff --git a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala deleted file mode 100644 index 2d171d7217..0000000000 --- a/io/js/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.effect.std.Dispatcher -import cats.effect.syntax.all._ -import cats.syntax.all._ -import com.comcast.ip4s.{Host, GenSocketAddress, IpAddress, Port, SocketAddress, UnixSocketAddress} -import fs2.concurrent.Channel -import fs2.io.internal.facade - -import scala.scalajs.js - -// TODO replace this implementation with delegation to IpSocketsProvider -private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - - private[net] def forAsync[F[_]: Async]: SocketGroup[F] = new AsyncSocketGroup[F] - - private[net] final class AsyncSocketGroup[F[_]](implicit F: Async[F]) - extends AbstractAsyncSocketGroup[F] { - - private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = - options.traverse_(option => option.key.set(socket, option.value)) - - override def client( - to: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - (for { - sock <- Resource - .make( - F.delay( - new facade.net.Socket(new facade.net.SocketOptions { allowHalfOpen = true }) - ) - )(sock => - F.delay { - if (!sock.destroyed) - sock.destroy() - } - ) - .evalTap(setSocketOptions(options)) - - localAddressGen = F.delay { - UnixSocketAddress("TODO3 - LOCAL"): GenSocketAddress - } - remoteAddressGen = F.delay { - UnixSocketAddress("TODO3 - REMOTE"): GenSocketAddress - } - socket <- Socket.forAsync(sock, localAddressGen, remoteAddressGen) - _ <- F - .async[Unit] { cb => - sock - .registerOneTimeListener[F, js.Error]("error") { error => - cb(Left(js.JavaScriptException(error))) - } <* F.delay { - sock.connect(to.port.value, to.host.toString, () => cb(Right(()))) - } - } - .toResource - } yield socket).adaptError { case IOException(ex) => ex } - - override def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - (for { - dispatcher <- Dispatcher.sequential[F] - channel <- Channel.unbounded[F, facade.net.Socket].toResource - server <- Resource.make( - F - .delay( - facade.net.createServer( - new facade.net.ServerOptions { - pauseOnConnect = true - allowHalfOpen = true - }, - sock => dispatcher.unsafeRunAndForget(channel.send(sock)) - ) - ) - )(server => - F.async[Unit] { cb => - if (server.listening) - F.delay(server.close(e => cb(e.toLeft(()).leftMap(js.JavaScriptException)))) *> - channel.close.as(None) - else - F.delay(cb(Right(()))).as(None) - } - ) - _ <- F - .async[Unit] { cb => - server.registerOneTimeListener[F, js.Error]("error") { e => - cb(Left(js.JavaScriptException(e))) - } <* F.delay { - address match { - case Some(host) => - server.listen(port.fold(0)(_.value), host.toString, () => cb(Right(()))) - case None => - server.listen(port.fold(0)(_.value), () => cb(Right(()))) - } - - } - - } - .toResource - ipAddress <- F.delay { - val info = server.address().asInstanceOf[facade.net.BoundAddress] - SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port).get) - }.toResource - sockets = channel.stream - .evalTap(setSocketOptions(options)) - .flatMap { sock => - val localAddressGen = F.delay { - UnixSocketAddress("TODO - ACCEPT - LOCAL"): GenSocketAddress - } - val remoteAddressGen = F.delay { - UnixSocketAddress("TODO - ACCEPT - REMOTE"): GenSocketAddress - } - Stream.resource(Socket.forAsync(sock, localAddressGen, remoteAddressGen)) - } - } yield (ipAddress, sockets)).adaptError { case IOException(ex) => ex } - - } - -} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala deleted file mode 100644 index 93c7ecf94c..0000000000 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketGroupPlatform.scala +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import cats.syntax.all._ -import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} - -private[net] trait SocketGroupCompanionPlatform { self: SocketGroup.type => - - def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = - new SocketGroup[F] { - def client(to: SocketAddress[Host], options: List[SocketOption]) = - ipSockets.connect(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream.resource(serverResource(address, port, options)).flatMap(_._2) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets - .bind( - SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), - options - ) - .evalMap(b => - b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept) - ) - } -} diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 016609a129..f8b2cb15e5 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -24,7 +24,8 @@ package io package net import cats.effect.kernel.Resource -import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress} +import cats.syntax.all._ +import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} import cats.effect.kernel.Async /** Supports creation of client and server TCP sockets that all share @@ -71,23 +72,32 @@ trait SocketGroup[F[_]] { ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] } -private[net] object SocketGroup extends SocketGroupCompanionPlatform { +private[net] object SocketGroup { - private[net] abstract class AbstractAsyncSocketGroup[F[_]: Async] extends SocketGroup[F] { - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream - .resource( - serverResource( - address, - port, + def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = + new SocketGroup[F] { + def client(to: SocketAddress[Host], options: List[SocketOption]) = + ipSockets.connect(to, options) + + def server( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Stream[F, Socket[F]] = + Stream.resource(serverResource(address, port, options)).flatMap(_._2) + + def serverResource( + address: Option[Host], + port: Option[Port], + options: List[SocketOption] + ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = + ipSockets + .bind( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options ) - ) - .flatMap { case (_, clients) => clients } - } - + .evalMap(b => + b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept) + ) + } } From 6de7fbe215692ff2711527a813190ce8c4a2f9c3 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 15:44:50 -0400 Subject: [PATCH 24/79] Scalafmt --- .../fs2/io/net/AsyncSocketsProvider.scala | 22 ++++++++++++---- .../fs2/io/net/tls/TLSSocketPlatform.scala | 9 +++++-- .../fs2/io/net/SocketAddressHelpers.scala | 21 ++++++++++++++++ .../io/net/UnixSocketsProviderPlatform.scala | 5 ++-- .../scala/fs2/io/net/NetworkPlatform.scala | 8 +----- .../fs2/io/net/SocketAddressHelpers.scala | 25 ++++++++++++++++--- .../main/scala/fs2/io/net/SocketInfo.scala | 4 ++- .../scala/fs2/io/net/UnixSocketsSuite.scala | 14 +++++------ .../scala/fs2/io/net/tcp/SocketSuite.scala | 20 ++++++++------- 9 files changed, 92 insertions(+), 36 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index b6e0ef42ff..9462d46922 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -58,14 +58,20 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { localAddressGen = F.delay { (to match { case Left(_) => - SocketAddress(IpAddress.fromString(sock.localAddress.get).get, Port.fromInt(sock.localPort.get).get) + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) case Right(_) => UnixSocketAddress("") }): GenSocketAddress } remoteAddressGen = F.delay { (to match { case Left(_) => - SocketAddress(IpAddress.fromString(sock.remoteAddress.get).get, Port.fromInt(sock.remotePort.get).get) + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) case Right(addr) => addr }): GenSocketAddress } @@ -154,14 +160,20 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { val localAddressGen = F.delay { (address match { case Left(_) => - SocketAddress(IpAddress.fromString(sock.localAddress.get).get, Port.fromInt(sock.localPort.get).get) + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) case Right(addr) => addr }): GenSocketAddress } val remoteAddressGen = F.delay { (address match { - case Left(_) => - SocketAddress(IpAddress.fromString(sock.remoteAddress.get).get, Port.fromInt(sock.remotePort.get).get) + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) case Right(_) => UnixSocketAddress("") }): GenSocketAddress } diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index f8d6a44c09..6d59e2f33f 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -68,8 +68,13 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => readStream: SuspendedStream[F, Byte], underlying: Socket[F], val session: F[SSLSession], - val applicationProtocol: F[String], - ) extends Socket.AsyncSocket[F](sock, readStream, Async[F].delay(sys.error("unused")), Async[F].delay(sys.error("unused"))) + val applicationProtocol: F[String] + ) extends Socket.AsyncSocket[F]( + sock, + readStream, + Async[F].delay(sys.error("unused")), + Async[F].delay(sys.error("unused")) + ) with UnsealedTLSSocket[F] { override def localAddress = underlying.localAddress override def localAddressGen = underlying.localAddressGen diff --git a/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index 7e66e4e18a..7a2d45128e 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package fs2 package io package net diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 55497a751a..6d440172fb 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -97,8 +97,9 @@ private[net] trait UnixSocketsProviderCompanionPlatform { Stream .resource(accept.attempt) .flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => Stream.eval(makeSocket(accepted, address, UnixSocketAddress(""))) + case Left(_) => Stream.empty[F] + case Right(accepted) => + Stream.eval(makeSocket(accepted, address, UnixSocketAddress(""))) } .repeat ServerSocket(info, acceptIncoming) diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index f601fffe8d..96dab508e7 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -28,13 +28,7 @@ import cats.effect.IO import cats.effect.LiftIO import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{ - Dns, - GenSocketAddress, - Host, - SocketAddress, - UnixSocketAddress -} +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, SocketAddress, UnixSocketAddress} private[net] trait NetworkPlatform[F[_]] diff --git a/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index 989e6197f6..7453f1a6b7 100644 --- a/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala +++ b/io/native/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package fs2 package io package net @@ -7,11 +28,9 @@ import java.net.{SocketAddress => JSocketAddress, InetSocketAddress} private[net] object SocketAddressHelpers { - def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = { + def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = address match { case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) case _ => throw new IllegalArgumentException("Unsupported address type: " + address) } - } } - diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 43328e100d..2cc2d7c8cb 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -40,7 +40,9 @@ trait SocketInfo[F[_]] { } object SocketInfo extends SocketInfoCompanionPlatform { - private[net] def downcastAddress[F[_]: MonadThrow](address: F[GenSocketAddress]): F[SocketAddress[IpAddress]] = + private[net] def downcastAddress[F[_]: MonadThrow]( + address: F[GenSocketAddress] + ): F[SocketAddress[IpAddress]] = address.flatMap { case a: SocketAddress[IpAddress] @unchecked => MonadThrow[F].pure(a) case _ => MonadThrow[F].raiseError(new UnsupportedOperationException("invalid address type")) diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 6a4e4cdbaa..6faf41c416 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -37,23 +37,23 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { .resource(sockets.bind(address, Nil)) .flatMap(ss => Stream.exec(ss.localAddressGen.flatMap(a => IO.println("Server Local: " + a))) ++ - ss.accept + ss.accept ) // TODO // .flatMap(_.accept) .map { socket => Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ - Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ - socket.reads.through(socket.writes) + Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ + socket.reads.through(socket.writes) } .parJoinUnbounded def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { socket => socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a)) *> - socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) *> - socket.write(msg) *> socket.endOfOutput *> socket.reads.compile - .to(Chunk) - .map(read => assertEquals(read, msg)) + socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) *> + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile + .to(Chunk) + .map(read => assertEquals(read, msg)) } val clients = (0 until 100).map(b => client(Chunk.singleton(b.toByte))) diff --git a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala index df6958796a..9f3465899e 100644 --- a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala @@ -55,21 +55,23 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .flatMap { case (server, clients) => val echoServer = server.map { socket => Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ - Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ - socket.reads - .through(socket.writes) - .onFinalize(socket.endOfOutput) + Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ + socket.reads + .through(socket.writes) + .onFinalize(socket.endOfOutput) }.parJoinUnbounded val msgClients = clients .take(clientCount) .map { socket => Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a))) ++ - Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a))) ++ - Stream - .chunk(message) - .through(socket.writes) - .onFinalize(socket.endOfOutput) ++ + Stream.exec( + socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) + ) ++ + Stream + .chunk(message) + .through(socket.writes) + .onFinalize(socket.endOfOutput) ++ socket.reads.chunks .map(bytes => new String(bytes.toArray)) } From 4399a1a6f5a770451843a0944868c9f0f70676ee Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 15:59:36 -0400 Subject: [PATCH 25/79] Test unix socket addresses --- .../scala/fs2/io/net/UnixSocketsSuite.scala | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 6faf41c416..2bf669c590 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -25,42 +25,72 @@ package io.net import scala.concurrent.duration._ import cats.effect.IO +import cats.syntax.all._ import com.comcast.ip4s.UnixSocketAddress class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { - protected def testProvider(provider: String, sockets: UnixSocketsProvider[IO]) = + protected def testProvider(provider: String, sockets: UnixSocketsProvider[IO]) = { test(s"echoes - $provider") { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") + val server = Stream + .resource(sockets.bind(address, Nil)) + .flatMap(_.accept) + .map { socket => + socket.reads.through(socket.writes) + } + .parJoinUnbounded + + def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { socket => + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile + .to(Chunk) + .map(read => assertEquals(read, msg)) + } + + val clients = (0 until 100).map(b => client(Chunk.singleton(b.toByte))) + + (Stream.sleep_[IO](1.second) ++ Stream.emits(clients).evalMap(identity)) + .concurrently(server) + .compile + .drain + } + + test(s"addresses - $provider") { + val address = UnixSocketAddress("fs2-unix-sockets-test.sock") + val server = Stream .resource(sockets.bind(address, Nil)) .flatMap(ss => - Stream.exec(ss.localAddressGen.flatMap(a => IO.println("Server Local: " + a))) ++ - ss.accept + Stream.exec(ss.localAddressGen.map { local => + assertEquals(local, address) + }) ++ ss.accept ) - // TODO - // .flatMap(_.accept) .map { socket => - Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ - Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ + Stream.exec((socket.localAddressGen, socket.remoteAddressGen).mapN { + case (local, remote) => + assertEquals(local, address) + assertEquals(remote, UnixSocketAddress("")) + }) ++ socket.reads.through(socket.writes) } .parJoinUnbounded - def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { socket => - socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a)) *> - socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) *> + val msg = Chunk.array("Hello, world".getBytes) + val client = Stream.resource(sockets.connect(address, Nil).evalMap { socket => + (socket.localAddressGen, socket.remoteAddressGen).mapN { case (local, remote) => + assertEquals(local, UnixSocketAddress("")) + assertEquals(remote, address) + } *> socket.write(msg) *> socket.endOfOutput *> socket.reads.compile .to(Chunk) .map(read => assertEquals(read, msg)) - } + }) - val clients = (0 until 100).map(b => client(Chunk.singleton(b.toByte))) - - (Stream.sleep_[IO](1.second) ++ Stream.emits(clients).evalMap(identity)) + (Stream.sleep_[IO](1.second) ++ client) .concurrently(server) .compile .drain } + } } From 18eb5d2305f38e3c4054efb2bfa07cf35fb37e4a Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 18:21:27 -0400 Subject: [PATCH 26/79] IP address tests --- .../net/{tcp => }/SocketSuitePlatform.scala | 2 +- .../net/{tcp => }/SocketSuitePlatform.scala | 4 +- .../fs2/io/net/{tcp => }/SocketSuite.scala | 37 ++++++++----------- 3 files changed, 18 insertions(+), 25 deletions(-) rename io/js/src/test/scala/fs2/io/net/{tcp => }/SocketSuitePlatform.scala (98%) rename io/jvm-native/src/test/scala/fs2/io/net/{tcp => }/SocketSuitePlatform.scala (96%) rename io/shared/src/test/scala/fs2/io/net/{tcp => }/SocketSuite.scala (92%) diff --git a/io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala similarity index 98% rename from io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala rename to io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala index fe0607a79a..4e753a5bb6 100644 --- a/io/js/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/SocketSuitePlatform.scala @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.tcp +package fs2.io.net trait SocketSuitePlatform { diff --git a/io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala b/io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala similarity index 96% rename from io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala rename to io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala index e78ecc048f..e7645d86e2 100644 --- a/io/jvm-native/src/test/scala/fs2/io/net/tcp/SocketSuitePlatform.scala +++ b/io/jvm-native/src/test/scala/fs2/io/net/SocketSuitePlatform.scala @@ -19,9 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package fs2.io.net.tcp - -import fs2.io.net.SocketOption +package fs2.io.net trait SocketSuitePlatform { diff --git a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala similarity index 92% rename from io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala rename to io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index 9f3465899e..48115f38a0 100644 --- a/io/shared/src/test/scala/fs2/io/net/tcp/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -22,7 +22,6 @@ package fs2 package io package net -package tcp import cats.effect.IO import com.comcast.ip4s._ @@ -36,14 +35,16 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val timeout = 30.seconds val setup = for { - serverSetup <- Network[IO].serverResource(address = Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup + serverSocket <- Network[IO].bind(address = SocketAddress(ip"127.0.0.1", Port.Wildcard)) clients = Stream - .resource( - Network[IO].client(bindAddress, options = setupOptionsPlatform) - ) + .eval(serverSocket.localAddressGen) + .flatMap { localAddress => + Stream.resource( + Network[IO].connect(localAddress, options = setupOptionsPlatform) + ) + } .repeat - } yield server -> clients + } yield serverSocket.accept -> clients group("tcp") { test("echo requests - each concurrent client gets back what it sent") { @@ -54,24 +55,18 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .resource(setup) .flatMap { case (server, clients) => val echoServer = server.map { socket => - Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Local: " + a))) ++ - Stream.exec(socket.remoteAddressGen.flatMap(a => IO.println("Remote: " + a))) ++ - socket.reads - .through(socket.writes) - .onFinalize(socket.endOfOutput) + socket.reads + .through(socket.writes) + .onFinalize(socket.endOfOutput) }.parJoinUnbounded val msgClients = clients .take(clientCount) .map { socket => - Stream.exec(socket.localAddressGen.flatMap(a => IO.println("Client Local: " + a))) ++ - Stream.exec( - socket.remoteAddressGen.flatMap(a => IO.println("Client Remote: " + a)) - ) ++ - Stream - .chunk(message) - .through(socket.writes) - .onFinalize(socket.endOfOutput) ++ + Stream + .chunk(message) + .through(socket.writes) + .onFinalize(socket.endOfOutput) ++ socket.reads.chunks .map(bytes => new String(bytes.toArray)) } @@ -161,7 +156,6 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { assertEquals(clientRemote, serverLocal) assertEquals(clientLocal, serverRemote) } - } .compile .drain @@ -291,6 +285,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } } } + test("sendFile - sends data from file to socket from the offset") { val content = "Hello, world!" val offset = 7L From 1e9703e2b14757fad5738d8d16f9d948c5ed3f9a Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 19:05:51 -0400 Subject: [PATCH 27/79] Socket option cleanup --- .../fs2/io/net/AsyncSocketsProvider.scala | 17 ++-- .../fs2/io/net/SocketOptionPlatform.scala | 34 +++---- .../scala/fs2/io/net/SocketPlatform.scala | 11 ++- .../fs2/io/net/tls/TLSSocketPlatform.scala | 1 - .../scala/fs2/io/net/SocketInfoPlatform.scala | 3 + .../fs2/io/net/SocketOptionPlatform.scala | 39 ++++---- .../fs2/io/net/JnrUnixSocketsProvider.scala | 10 ++- .../main/scala/fs2/io/net/ServerSocket.scala | 11 +-- .../main/scala/fs2/io/net/SocketInfo.scala | 2 + .../test/scala/fs2/io/net/SocketSuite.scala | 89 ++++++++++--------- 10 files changed, 128 insertions(+), 89 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 9462d46922..4850da27d1 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -146,13 +146,16 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { case Right(addr) => addr } } - - def getOption[A](key: SocketOption.Key[A]) = - F.raiseError(new UnsupportedOperationException) - def setOption[A](key: SocketOption.Key[A], value: A) = - F.raiseError(new UnsupportedOperationException) - def supportedOptions = - F.raiseError(new UnsupportedOperationException) + def localAddress = SocketInfo.downcastAddress(localAddressGen) + private def raiseOptionError[A]: F[A] = + F.raiseError( + new UnsupportedOperationException( + "Node.js server sockets do not support socket options" + ) + ) + def getOption[A](key: SocketOption.Key[A]) = raiseOptionError + def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError + def supportedOptions = raiseOptionError } sockets = channel.stream .evalTap(setSocketOptions(options)) diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 27e32ccd88..12224c4200 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -32,27 +32,32 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[A]] } - private object Encoding extends Key[String] { + private def unsupportedGet[F[_]: Sync, A]: F[A] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support get")) + + object Encoding extends Key[String] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: String): F[Unit] = Sync[F].delay { sock.setEncoding(value) () } override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[String]] = - Sync[F].raiseError(new UnsupportedOperationException) + unsupportedGet } + def encoding(value: String): SocketOption = apply(Encoding, value) - private object KeepAlive extends Key[Boolean] { + object KeepAlive extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setKeepAlive(value) () } override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - Sync[F].raiseError(new UnsupportedOperationException) + unsupportedGet } + def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) - private object NoDelay extends Key[Boolean] { + object NoDelay extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setNoDelay(value) @@ -60,10 +65,11 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => } override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - Sync[F].raiseError(new UnsupportedOperationException) + unsupportedGet } + def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) - private object Timeout extends Key[FiniteDuration] { + object Timeout extends Key[FiniteDuration] { override private[net] def set[F[_]: Sync]( sock: facade.net.Socket, value: FiniteDuration @@ -77,6 +83,7 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => Some(sock.timeout.toLong.millis) } } + def timeout(value: FiniteDuration): SocketOption = apply(Timeout, value) object UnixServerSocketDeleteIfExists extends Key[Boolean] { override private[net] def set[F[_]: Sync]( @@ -84,8 +91,10 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => value: Boolean ): F[Unit] = Sync[F].unit override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - Sync[F].pure(None) + unsupportedGet } + def unixServerSocketDeleteIfExists(value: Boolean): SocketOption = + apply(UnixServerSocketDeleteIfExists, value) object UnixServerSocketDeleteOnClose extends Key[Boolean] { override private[net] def set[F[_]: Sync]( @@ -93,15 +102,8 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => value: Boolean ): F[Unit] = Sync[F].unit override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - Sync[F].pure(None) + unsupportedGet } - - def encoding(value: String): SocketOption = apply(Encoding, value) - def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) - def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) - def timeout(value: FiniteDuration): SocketOption = apply(Timeout, value) - def unixServerSocketDeleteIfExists(value: Boolean): SocketOption = - apply(UnixServerSocketDeleteIfExists, value) def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = apply(UnixServerSocketDeleteOnClose, value) } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 90c7a5b723..1f69dfcb3c 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -96,7 +96,16 @@ private[net] trait SocketCompanionPlatform { override def remoteAddressGen = remoteAddressGen0 override def supportedOptions: F[Set[SocketOption.Key[?]]] = - ??? + F.pure( + Set( + SocketOption.Encoding, + SocketOption.KeepAlive, + SocketOption.NoDelay, + SocketOption.Timeout, + SocketOption.UnixServerSocketDeleteIfExists, + SocketOption.UnixServerSocketDeleteOnClose + ) + ) override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = key.get(sock) diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 6d59e2f33f..7ae46e2a99 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -28,7 +28,6 @@ import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.GenSocketAddress import fs2.io.internal.facade import fs2.io.internal.SuspendedStream import scodec.bits.ByteVector diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 3d8b549231..74ceb867e0 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -45,6 +45,9 @@ private[net] trait SocketInfoCompanionPlatform { override def localAddressGen: F[GenSocketAddress] = asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress)) + override def localAddress = + SocketInfo.downcastAddress(localAddressGen) + override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { channel.supportedOptions.asScala.toSet diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 724f0e12e9..d71951069a 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -36,47 +36,58 @@ private[net] trait SocketOptionCompanionPlatform { def integer(key: JSocketOption[JInt], value: Int): SocketOption = SocketOption[JInt](key, value) + val MulticastInterface = StandardSocketOptions.IP_MULTICAST_IF def multicastInterface(value: NetworkInterface): SocketOption = - SocketOption(StandardSocketOptions.IP_MULTICAST_IF, value) + SocketOption(MulticastInterface, value) + val MulticastLoop = StandardSocketOptions.IP_MULTICAST_LOOP def multicastLoop(value: Boolean): SocketOption = - boolean(StandardSocketOptions.IP_MULTICAST_LOOP, value) + boolean(MulticastLoop, value) + val MulticastTtl = StandardSocketOptions.IP_MULTICAST_TTL def multicastTtl(value: Int): SocketOption = - integer(StandardSocketOptions.IP_MULTICAST_TTL, value) + integer(MulticastTtl, value) + val TypeOfService = StandardSocketOptions.IP_TOS def typeOfService(value: Int): SocketOption = - integer(StandardSocketOptions.IP_TOS, value) + integer(TypeOfService, value) + val Broadcast = StandardSocketOptions.SO_BROADCAST def broadcast(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_BROADCAST, value) + boolean(Broadcast, value) + val KeepAlive = StandardSocketOptions.SO_KEEPALIVE def keepAlive(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_KEEPALIVE, value) + boolean(KeepAlive, value) + val Linger = StandardSocketOptions.SO_LINGER def linger(value: Int): SocketOption = - integer(StandardSocketOptions.SO_LINGER, value) + integer(Linger, value) + val ReceiveBufferSize = StandardSocketOptions.SO_RCVBUF def receiveBufferSize(value: Int): SocketOption = - integer(StandardSocketOptions.SO_RCVBUF, value) + integer(ReceiveBufferSize, value) + val ReuseAddress = StandardSocketOptions.SO_REUSEADDR def reuseAddress(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_REUSEADDR, value) + boolean(ReuseAddress, value) + val ReusePort = StandardSocketOptions.SO_REUSEPORT def reusePort(value: Boolean): SocketOption = - boolean(StandardSocketOptions.SO_REUSEPORT, value) + boolean(ReusePort, value) + val SendBufferSize = StandardSocketOptions.SO_SNDBUF def sendBufferSize(value: Int): SocketOption = - integer(StandardSocketOptions.SO_SNDBUF, value) + integer(SendBufferSize, value) + val NoDelay = StandardSocketOptions.TCP_NODELAY def noDelay(value: Boolean): SocketOption = - boolean(StandardSocketOptions.TCP_NODELAY, value) + boolean(NoDelay, value) val UnixServerSocketDeleteIfExists: Key[JBoolean] = new Key[JBoolean] { def name() = "FS2_UNIX_DELETE_IF_EXISTS" def `type`() = classOf[JBoolean] } - def unixServerSocketDeleteIfExists(value: JBoolean): SocketOption = boolean(UnixServerSocketDeleteIfExists, value) @@ -84,8 +95,6 @@ private[net] trait SocketOptionCompanionPlatform { def name() = "FS2_UNIX_DELETE_ON_CLOSE" def `type`() = classOf[JBoolean] } - def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = boolean(UnixServerSocketDeleteOnClose, value) - } diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index c87d280d9f..7701de889c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -77,10 +77,12 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ ) ) val info: SocketInfo[F] = new SocketInfo[F] { - def supportedOptions = F.pure(Set.empty) - def getOption[A](key: SocketOption.Key[A]) = raiseOptionError - def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError - def localAddressGen = F.pure(address) + override def supportedOptions = F.pure(Set.empty) + override def getOption[A](key: SocketOption.Key[A]) = raiseOptionError + override def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError + override def localAddressGen = F.pure(address) + override def localAddress = + SocketInfo.downcastAddress(localAddressGen) } info -> Resource diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 705f28560a..46a5987f8f 100644 --- a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -43,11 +43,12 @@ object ServerSocket { def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { val accept0 = accept new ServerSocket[F] { - def accept: Stream[F, Socket[F]] = accept0 - def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) - def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) - def supportedOptions = info.supportedOptions - def localAddressGen = info.localAddressGen + override def accept: Stream[F, Socket[F]] = accept0 + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) + override def supportedOptions = info.supportedOptions + override def localAddressGen = info.localAddressGen + override def localAddress = info.localAddress } } } diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 2cc2d7c8cb..3aa8a8748a 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -32,6 +32,8 @@ trait SocketInfo[F[_]] { /** Asks for the local address of this socket. Like `localAddress` but supports unix sockets as well. */ def localAddressGen: F[GenSocketAddress] + def localAddress: F[SocketAddress[IpAddress]] + def supportedOptions: F[Set[SocketOption.Key[?]]] def getOption[A](key: SocketOption.Key[A]): F[Option[A]] diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index 48115f38a0..14347ccaaf 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import cats.effect.IO +import cats.effect.{IO, Resource} import com.comcast.ip4s._ import scala.concurrent.duration._ @@ -163,23 +163,26 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("errors - should be captured in the effect") { val connectionRefused = for { - port <- Network[IO].serverResource(Some(ip"127.0.0.1")).use(s => IO.pure(s._1.port)) + port <- Network[IO] + .bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + .use(s => s.localAddress.map(_.port)) _ <- Network[IO] - .client(SocketAddress(host"localhost", port)) + .connect(SocketAddress(host"localhost", port)) .use_ .interceptMessage[ConnectException]("Connection refused") } yield () val addressAlreadyInUse = - Network[IO].serverResource(Some(ip"127.0.0.1")).map(_._1).use { bindAddress => - Network[IO] - .serverResource(Some(bindAddress.host), Some(bindAddress.port)) - .use_ - .interceptMessage[BindException]("Address already in use") + Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)).evalMap(_.localAddress).use { + bindAddress => + Network[IO] + .bind(bindAddress) + .use_ + .interceptMessage[BindException]("Address already in use") } val unknownHost = Network[IO] - .client(SocketAddress.fromString("not.example.com:80").get) + .connect(SocketAddress.fromString("not.example.com:80").get) .use_ .attempt .map { @@ -199,10 +202,10 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { SocketOption.noDelay(true) ) ++ optionsPlatform val setup = for { - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1"), None, opts) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress, opts) - } yield (server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard), opts) + bindAddress <- Resource.eval(serverSocket.localAddressGen) + client <- Network[IO].connect(bindAddress, opts) + } yield (serverSocket.accept, client) val msg = "hello" @@ -228,10 +231,10 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("read after timed out read") { val setup = for { - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress) - } yield (server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + bindAddress <- Resource.eval(serverSocket.localAddressGen) + client <- Network[IO].connect(bindAddress) + } yield (serverSocket.accept, client) Stream .resource(setup) .flatMap { case (server, client) => @@ -251,37 +254,43 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } test("can shutdown a socket that's pending a read") { - Network[IO].serverResource().use { case (bindAddress, clients) => - Network[IO].client(bindAddress).use { _ => - clients.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { - case _: TimeoutException => () + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + Resource.eval(serverSocket.localAddressGen).use { bindAddress => + Network[IO].connect(bindAddress).use { _ => + serverSocket.accept.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { + case _: TimeoutException => () + } } } } } test("accepted socket closes timely") { - Network[IO].serverResource().use { case (bindAddress, clients) => - clients.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].client(bindAddress).use { client => - client.read(1).assertEquals(None) + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + Resource.eval(serverSocket.localAddressGen).use { bindAddress => + serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { + Network[IO].connect(bindAddress).use { client => + client.read(1).assertEquals(None) + } } } } } test("endOfOutput / endOfInput ignores ENOTCONN") { - Network[IO].serverResource().use { case (bindAddress, clients) => - Network[IO].client(bindAddress).surround(IO.sleep(100.millis)).background.surround { - clients - .take(1) - .foreach { socket => - socket.write(Chunk.array("fs2.rocks".getBytes)) *> - IO.sleep(1.second) *> - socket.endOfOutput *> socket.endOfInput - } - .compile - .drain + Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => + Resource.eval(serverSocket.localAddressGen).use { bindAddress => + Network[IO].connect(bindAddress).surround(IO.sleep(100.millis)).background.surround { + serverSocket.accept + .take(1) + .foreach { socket => + socket.write(Chunk.array("fs2.rocks".getBytes)) *> + IO.sleep(1.second) *> + socket.endOfOutput *> socket.endOfInput + } + .compile + .drain + } } } } @@ -302,10 +311,10 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .compile .drain .toResource - serverSetup <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (bindAddress, server) = serverSetup - client <- Network[IO].client(bindAddress) - } yield (tempFile, server, client) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + bindAddress <- Resource.eval(serverSocket.localAddressGen) + client <- Network[IO].connect(bindAddress) + } yield (tempFile, serverSocket.accept, client) Stream .resource(setup) From f27f191dfe1cdb278ec452da6c0d008c7dd6caee Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 19:14:21 -0400 Subject: [PATCH 28/79] Socket option cleanup --- .../main/scala/fs2/io/net/AsyncSocketsProvider.scala | 2 +- .../main/scala/fs2/io/internal/SocketHelpers.scala | 12 ++++++++++++ .../fs2/io/net/FdPollingIpSocketsProvider.scala | 3 ++- .../src/main/scala/fs2/io/net/FdPollingSocket.scala | 2 +- .../fs2/io/net/FdPollingUnixSocketsProvider.scala | 3 ++- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 4850da27d1..9e38664a4b 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -155,7 +155,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { ) def getOption[A](key: SocketOption.Key[A]) = raiseOptionError def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError - def supportedOptions = raiseOptionError + def supportedOptions = F.pure(Set.empty) } sockets = channel.stream .evalTap(setSocketOptions(options)) diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index 00cb11b6c6..594691b908 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -72,6 +72,18 @@ private[io] object SocketHelpers { (if (LinktimeInfo.isMac) setNoSigPipe(fd) else F.unit) } + // TODO: Support other options (e.g. extended options?) + + def supportedOptions[F[_]: Sync]: F[Set[SocketOption[?]]] = + Sync[F].pure(Set( + StandardSocketOptions.SO_SNDBUF, + StandardSocketOptions.SO_RCVBUF, + StandardSocketOptions.SO_REUSEADDR, + StandardSocketOptions.SO_REUSEPORT, + StandardSocketOptions.SO_KEEPALIVE, + StandardSocketOptions.TCP_NODELAY + )) + def getOption[F[_]: Sync, A](fd: CInt, name: SocketOption[A]): F[Option[A]] = (name match { case StandardSocketOptions.SO_SNDBUF => getOptionInt(fd, SO_SNDBUF) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 708998212b..8d377e0454 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -148,8 +148,9 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As info = new SocketInfo[F] { def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) - def supportedOptions = ??? + def supportedOptions = SocketHelpers.supportedOptions def localAddressGen = SocketHelpers.getLocalAddressGen[F](fd, if (ipv4) AF_INET else AF_INET6) + def localAddress = SocketInfo.downcastAddress(localAddressGen) } } yield ServerSocket(info, sockets) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index 9992a324ae..3131a12e7d 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -123,7 +123,7 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) - def supportedOptions = ??? + def supportedOptions = SocketHelpers.supportedOptions } private object FdPollingSocket { diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 3d6284dd6a..9b814e80e1 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -119,8 +119,9 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) - def supportedOptions = ??? + def supportedOptions = SocketHelpers.supportedOptions def localAddressGen = SocketHelpers.getLocalAddressGen(fd, AF_UNIX) + def localAddress = SocketInfo.downcastAddress(localAddressGen) } clients = Stream From 5285acb6212f8ed004e9e24e8f170f40c0073f31 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 19:23:37 -0400 Subject: [PATCH 29/79] Socket option cleanup --- .../scala/fs2/io/internal/SocketHelpers.scala | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index 594691b908..7646ba5563 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -75,14 +75,16 @@ private[io] object SocketHelpers { // TODO: Support other options (e.g. extended options?) def supportedOptions[F[_]: Sync]: F[Set[SocketOption[?]]] = - Sync[F].pure(Set( - StandardSocketOptions.SO_SNDBUF, - StandardSocketOptions.SO_RCVBUF, - StandardSocketOptions.SO_REUSEADDR, - StandardSocketOptions.SO_REUSEPORT, - StandardSocketOptions.SO_KEEPALIVE, - StandardSocketOptions.TCP_NODELAY - )) + Sync[F].pure( + Set( + StandardSocketOptions.SO_SNDBUF, + StandardSocketOptions.SO_RCVBUF, + StandardSocketOptions.SO_REUSEADDR, + StandardSocketOptions.SO_REUSEPORT, + StandardSocketOptions.SO_KEEPALIVE, + StandardSocketOptions.TCP_NODELAY + ) + ) def getOption[F[_]: Sync, A](fd: CInt, name: SocketOption[A]): F[Option[A]] = (name match { case StandardSocketOptions.SO_SNDBUF => From 78bc8ccdc695e9e6c379c9529a45f85d6b87d9e0 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 18 Apr 2025 19:48:18 -0400 Subject: [PATCH 30/79] Deprecate old socket group methods --- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 58 +++++++++--------- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 26 ++++---- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 60 +++++++++---------- .../main/scala/fs2/io/net/SocketGroup.scala | 3 + 4 files changed, 75 insertions(+), 72 deletions(-) diff --git a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index ca642a1cb3..4c10324c0b 100644 --- a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -45,7 +45,7 @@ class TLSSocketSuite extends TLSSuite { SecureContext(minVersion = protocol.some, maxVersion = protocol.some) ) ) - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -110,10 +110,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) @@ -124,7 +124,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -150,10 +150,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) @@ -164,7 +164,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -191,10 +191,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { serverContext <- Resource.eval(testTlsContext(true)) clientContext <- Resource.eval(testTlsContext(false)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( clientContext .clientBuilder(_) @@ -205,7 +205,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( serverContext .serverBuilder(s) @@ -239,10 +239,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true, Some(protocol))) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) @@ -255,7 +255,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( tlsContext .serverBuilder(s) @@ -295,16 +295,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(Network[IO].tlsContext.insecure) tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap(s => clientContext .clientBuilder(s) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -331,16 +331,16 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(Network[IO].tlsContext.system) tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap(s => clientContext .clientBuilder(s) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -364,10 +364,10 @@ class TLSSocketSuite extends TLSSuite { test("get local and remote address") { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) @@ -378,7 +378,7 @@ class TLSSocketSuite extends TLSSuite { ) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index fd514a7814..d23ee6c9c1 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -43,7 +43,7 @@ class TLSSocketSuite extends TLSSuite { def googleSetup(protocol: String) = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -130,10 +130,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client <- Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) + client <- Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -158,10 +158,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client <- Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) + client <- Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -188,10 +188,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Resource.eval(TLSContext.Builder.forAsync[IO].insecure) tlsContext <- Resource.eval(testTlsContext) - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client <- Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap(s => clientContext .clientBuilder(s) @@ -200,7 +200,7 @@ class TLSSocketSuite extends TLSSuite { ) // makes test fail if using X509TrustManager, passes if using X509ExtendedTrustManager .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) diff --git a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 81d6e594ed..b18f568d25 100644 --- a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -26,7 +26,7 @@ package tls import scala.concurrent.duration._ -import cats.effect.IO +import cats.effect.{IO, Resource} import cats.syntax.all._ import com.comcast.ip4s._ @@ -38,7 +38,7 @@ class TLSSocketSuite extends TLSSuite { def googleSetup(version: String) = for { tlsContext <- Network[IO].tlsContext.systemResource - socket <- Network[IO].client(SocketAddress(host"google.com", port"443")) + socket <- Network[IO].connect(SocketAddress(host"google.com", port"443")) tlsSocket <- tlsContext .clientBuilder(socket) .withParameters( @@ -107,17 +107,17 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -141,17 +141,17 @@ class TLSSocketSuite extends TLSSuite { test("empty write") { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -175,17 +175,17 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Network[IO].tlsContext.systemResource - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -211,17 +211,17 @@ class TLSSocketSuite extends TLSSuite { val setup = for { serverContext <- testTlsContext clientContext <- testClientTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( clientContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( serverContext .serverBuilder(s) @@ -255,17 +255,17 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .client(serverAddress) + .connect(serverAddress) .flatMap( tlsContext .clientBuilder(_) .withParameters(TLSParameters(serverName = Some("Unknown"))) .build ) - } yield server.flatMap(s => + } yield serverSocket.accept.flatMap(s => Stream.resource( tlsContext .serverBuilder(s) @@ -298,10 +298,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { clientContext <- Network[IO].tlsContext.insecureResource tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client = Network[IO].client(serverAddress).flatMap(clientContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) + client = Network[IO].connect(serverAddress).flatMap(clientContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream .resource(setup) @@ -327,10 +327,10 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext - addressAndConnections <- Network[IO].serverResource(Some(ip"127.0.0.1")) - (serverAddress, server) = addressAndConnections - client = Network[IO].client(serverAddress).flatMap(tlsContext.client(_)) - } yield server.flatMap(s => Stream.resource(tlsContext.server(s))) -> client + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + serverAddress <- Resource.eval(serverSocket.localAddressGen) + client = Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client val echo = Stream .resource(setup) diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index f8b2cb15e5..44e4aa7601 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -40,6 +40,7 @@ trait SocketGroup[F[_]] { * @param to address of remote server * @param options socket options to apply to the underlying socket */ + @deprecated("3.13.0", "Use Network[F].connect instead") def client( to: SocketAddress[Host], options: List[SocketOption] = List.empty @@ -55,6 +56,7 @@ trait SocketGroup[F[_]] { * @param port port to bind * @param options socket options to apply to the underlying socket */ + @deprecated("3.13.0", "Use Network[F].bindAndAccept instead") def server( address: Option[Host] = None, port: Option[Port] = None, @@ -65,6 +67,7 @@ trait SocketGroup[F[_]] { * * Make sure to handle errors in the client socket Streams. */ + @deprecated("3.13.0", "Use Network[F].bind instead") def serverResource( address: Option[Host] = None, port: Option[Port] = None, From a0ab96bbc3505badc3bd55509894fd32a97fe1d3 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Sat, 19 Apr 2025 08:11:52 -0400 Subject: [PATCH 31/79] Address cleanup --- build.sbt | 2 +- .../fs2/io/net/AsyncSocketsProvider.scala | 5 +- .../scala/fs2/io/net/SocketPlatform.scala | 5 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 21 +++---- ...hronousChannelGroupIpSocketsProvider.scala | 2 +- .../scala/fs2/io/net/SocketInfoPlatform.scala | 3 +- .../fs2/io/net/JnrUnixSocketsProvider.scala | 4 +- .../io/net/SelectingIpSocketsProvider.scala | 12 ++-- .../io/net/UnixSocketsProviderPlatform.scala | 2 +- .../fs2/io/net/tls/TLSDebugExample.scala | 2 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 9 +-- .../io/net/FdPollingIpSocketsProvider.scala | 6 +- .../scala/fs2/io/net/FdPollingSocket.scala | 4 +- .../io/net/FdPollingUnixSocketsProvider.scala | 5 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 23 +++---- .../main/scala/fs2/io/net/ServerSocket.scala | 21 ++++++- .../main/scala/fs2/io/net/SocketInfo.scala | 12 +--- .../test/scala/fs2/io/net/SocketSuite.scala | 60 ++++++++----------- 18 files changed, 93 insertions(+), 105 deletions(-) diff --git a/build.sbt b/build.sbt index 515ce696a0..adf0a25495 100644 --- a/build.sbt +++ b/build.sbt @@ -353,7 +353,7 @@ lazy val io = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "fs2-io", tlVersionIntroduced ~= { _.updated("3", "3.1.0") }, - libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.6.0-91-51bd018-SNAPSHOT", + libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.6.0-94-3751623-SNAPSHOT", tlJdkRelease := None ) .jvmSettings( diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 9e38664a4b..1dc8949fe5 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -146,7 +146,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { case Right(addr) => addr } } - def localAddress = SocketInfo.downcastAddress(localAddressGen) + def localAddress = localAddressGen.map(_.asIpUnsafe) private def raiseOptionError[A]: F[A] = F.raiseError( new UnsupportedOperationException( @@ -182,5 +182,6 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } Stream.resource(Socket.forAsync(sock, localAddressGen, remoteAddressGen)) } - } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } + serverSocket <- Resource.eval(ServerSocket(info, sockets)) + } yield serverSocket).adaptError { case IOException(ex) => ex } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 1f69dfcb3c..81d9f5c4d1 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -25,6 +25,7 @@ package net import cats.data.{Kleisli, OptionT} import cats.effect.{Async, Resource} +import cats.syntax.all._ import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.internal.{facade, SuspendedStream} @@ -86,12 +87,12 @@ private[net] trait SocketCompanionPlatform { override def isOpen: F[Boolean] = F.delay(sock.readyState == "open") override def localAddress: F[SocketAddress[IpAddress]] = - SocketInfo.downcastAddress(localAddressGen) + localAddressGen.map(_.asIpUnsafe) override def localAddressGen = localAddressGen0 override def remoteAddress: F[SocketAddress[IpAddress]] = - SocketInfo.downcastAddress(remoteAddressGen) + remoteAddressGen.map(_.asIpUnsafe) override def remoteAddressGen = remoteAddressGen0 diff --git a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 4c10324c0b..c5da4e54d5 100644 --- a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -111,9 +111,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -151,9 +150,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -192,9 +190,8 @@ class TLSSocketSuite extends TLSSuite { serverContext <- Resource.eval(testTlsContext(true)) clientContext <- Resource.eval(testTlsContext(false)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( clientContext .clientBuilder(_) @@ -240,9 +237,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true, Some(protocol))) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -296,9 +292,8 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Resource.eval(Network[IO].tlsContext.insecure) tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap(s => clientContext .clientBuilder(s) @@ -332,9 +327,8 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Resource.eval(Network[IO].tlsContext.system) tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap(s => clientContext .clientBuilder(s) @@ -365,9 +359,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 53a8bade05..77bfc06bda 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -144,7 +144,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( } } - setup.map(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) + setup.evalMap(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 74ceb867e0..1eb5dd7a1f 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -25,6 +25,7 @@ package net import com.comcast.ip4s.GenSocketAddress import cats.effect.Async +import cats.syntax.all._ import java.nio.channels.NetworkChannel @@ -46,7 +47,7 @@ private[net] trait SocketInfoCompanionPlatform { asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress)) override def localAddress = - SocketInfo.downcastAddress(localAddressGen) + localAddressGen.map(_.asIpUnsafe) override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index 7701de889c..eef1509778 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -23,6 +23,7 @@ package fs2 package io package net +import cats.syntax.all._ import cats.effect.{Async, Resource} import cats.effect.syntax.all._ @@ -81,8 +82,7 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ override def getOption[A](key: SocketOption.Key[A]) = raiseOptionError override def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError override def localAddressGen = F.pure(address) - override def localAddress = - SocketInfo.downcastAddress(localAddressGen) + override def localAddress = localAddressGen.map(_.asIpUnsafe) } info -> Resource diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index e3b079da9f..e6b2f009a9 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -83,11 +83,11 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici .make(F.delay(selector.provider.openServerSocketChannel())) { ch => F.delay(ch.close()) } - .evalMap { serverCh => + .evalMap { sch => val configure = address.host.resolve.flatMap { addr => F.delay { - serverCh.configureBlocking(false) - serverCh.bind( + sch.configureBlocking(false) + sch.bind( new InetSocketAddress( if (addr.isWildcard) null else addr.toInetAddress, address.port.value @@ -99,8 +99,8 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici def acceptLoop: Stream[F, SocketChannel] = Stream .bracketFull[F, SocketChannel] { poll => def go: F[SocketChannel] = - F.delay(serverCh.accept()).flatMap { - case null => poll(selector.select(serverCh, OP_ACCEPT).to) *> go + F.delay(sch.accept()).flatMap { + case null => poll(selector.select(sch, OP_ACCEPT).to) *> go case ch => F.pure(ch) } go @@ -122,7 +122,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici ) } - configure.as(ServerSocket(SocketInfo.forAsync(serverCh), accept)) + configure *> ServerSocket(SocketInfo.forAsync(sch), accept) } private def remoteAddress(ch: SocketChannel) = diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 6d440172fb..71ab4c41dc 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -92,7 +92,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) } - (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => + (delete *> openServerChannel(address, filteredOptions)).evalMap { case (info, accept) => val acceptIncoming = Stream .resource(accept.attempt) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala index 3068d85912..92a4a886ef 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSDebugExample.scala @@ -37,7 +37,7 @@ object TLSDebug { host: SocketAddress[Hostname] ): F[String] = host.resolve.flatMap { socketAddress => - Network[F].client(socketAddress).use { rawSocket => + Network[F].connect(socketAddress).use { rawSocket => tlsContext .clientBuilder(rawSocket) .withParameters( diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index d23ee6c9c1..dbf79de436 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -131,8 +131,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) - client <- Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + client <- Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -159,8 +158,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) - client <- Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + client <- Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -189,9 +187,8 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Resource.eval(TLSContext.Builder.forAsync[IO].insecure) tlsContext <- Resource.eval(testTlsContext) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client <- Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap(s => clientContext .clientBuilder(s) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 8d377e0454..6499776918 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -150,9 +150,9 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) def supportedOptions = SocketHelpers.supportedOptions def localAddressGen = SocketHelpers.getLocalAddressGen[F](fd, if (ipv4) AF_INET else AF_INET6) - def localAddress = SocketInfo.downcastAddress(localAddressGen) + def localAddress = localAddressGen.map(_.asIpUnsafe) } - - } yield ServerSocket(info, sockets) + serverSocket <- Resource.eval(ServerSocket(info, sockets)) + } yield serverSocket } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index 3131a12e7d..fd59f50853 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -47,8 +47,8 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( )(implicit F: Async[F]) extends Socket[F] { - def localAddress = SocketInfo.downcastAddress(localAddressGen) - def remoteAddress = SocketInfo.downcastAddress(remoteAddressGen) + def localAddress = localAddressGen.map(_.asIpUnsafe) + def remoteAddress = remoteAddressGen.map(_.asIpUnsafe) def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 9b814e80e1..2dfb47493f 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -121,7 +121,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F SocketHelpers.setOption(fd, key, value) def supportedOptions = SocketHelpers.supportedOptions def localAddressGen = SocketHelpers.getLocalAddressGen(fd, AF_UNIX) - def localAddress = SocketInfo.downcastAddress(localAddressGen) + def localAddress = localAddressGen.map(_.asIpUnsafe) } clients = Stream @@ -169,7 +169,8 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F .repeat .unNone - } yield ServerSocket(info, clients) + serverSocket <- Resource.eval(ServerSocket(info, clients)) + } yield serverSocket } private def toSockaddrUn[A](path: String)(f: Ptr[sockaddr] => A): A = { diff --git a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index b18f568d25..0272aafe8a 100644 --- a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -26,7 +26,7 @@ package tls import scala.concurrent.duration._ -import cats.effect.{IO, Resource} +import cats.effect.IO import cats.syntax.all._ import com.comcast.ip4s._ @@ -108,9 +108,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -142,9 +141,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -176,9 +174,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Network[IO].tlsContext.systemResource serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -212,9 +209,8 @@ class TLSSocketSuite extends TLSSuite { serverContext <- testTlsContext clientContext <- testClientTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( clientContext .clientBuilder(_) @@ -256,9 +252,8 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) client = Network[IO] - .connect(serverAddress) + .connect(serverSocket.boundAddress) .flatMap( tlsContext .clientBuilder(_) @@ -299,8 +294,7 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Network[IO].tlsContext.insecureResource tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) - client = Network[IO].connect(serverAddress).flatMap(clientContext.client(_)) + client = Network[IO].connect(serverSocket.boundAddress).flatMap(clientContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -328,8 +322,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - serverAddress <- Resource.eval(serverSocket.localAddressGen) - client = Network[IO].connect(serverAddress).flatMap(tlsContext.client(_)) + client = Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client val echo = Stream diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 46a5987f8f..07b862f35a 100644 --- a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -23,6 +23,10 @@ package fs2 package io package net +import cats.Functor +import cats.syntax.all._ +import com.comcast.ip4s.GenSocketAddress + /** Represents a bound TCP server socket. * * Note some platforms do not support getting and setting socket options on server sockets @@ -34,15 +38,24 @@ package net */ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { + /** Address this socket is bound to. */ + def boundAddress: GenSocketAddress + /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ def accept: Stream[F, Socket[F]] } object ServerSocket { - def apply[F[_]](info: SocketInfo[F], accept: Stream[F, Socket[F]]): ServerSocket[F] = { + private[net] def apply[F[_]]( + boundAddress: GenSocketAddress, + info: SocketInfo[F], + accept: Stream[F, Socket[F]] + ): ServerSocket[F] = { + val boundAddress0 = boundAddress val accept0 = accept new ServerSocket[F] { + override def boundAddress = boundAddress0 override def accept: Stream[F, Socket[F]] = accept0 override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) override def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) @@ -51,4 +64,10 @@ object ServerSocket { override def localAddress = info.localAddress } } + + private[net] def apply[F[_]: Functor]( + info: SocketInfo[F], + accept: Stream[F, Socket[F]] + ): F[ServerSocket[F]] = + info.localAddressGen.map(boundAddress => apply(boundAddress, info, accept)) } diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index 3aa8a8748a..ff78b1c5d5 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -23,8 +23,6 @@ package fs2 package io package net -import cats.MonadThrow -import cats.syntax.all._ import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} trait SocketInfo[F[_]] { @@ -41,12 +39,4 @@ trait SocketInfo[F[_]] { def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] } -object SocketInfo extends SocketInfoCompanionPlatform { - private[net] def downcastAddress[F[_]: MonadThrow]( - address: F[GenSocketAddress] - ): F[SocketAddress[IpAddress]] = - address.flatMap { - case a: SocketAddress[IpAddress] @unchecked => MonadThrow[F].pure(a) - case _ => MonadThrow[F].raiseError(new UnsupportedOperationException("invalid address type")) - } -} +object SocketInfo extends SocketInfoCompanionPlatform diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index 14347ccaaf..0495f57e71 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -23,7 +23,7 @@ package fs2 package io package net -import cats.effect.{IO, Resource} +import cats.effect.IO import com.comcast.ip4s._ import scala.concurrent.duration._ @@ -36,14 +36,12 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val setup = for { serverSocket <- Network[IO].bind(address = SocketAddress(ip"127.0.0.1", Port.Wildcard)) - clients = Stream - .eval(serverSocket.localAddressGen) - .flatMap { localAddress => - Stream.resource( - Network[IO].connect(localAddress, options = setupOptionsPlatform) + clients = + Stream + .resource( + Network[IO].connect(serverSocket.boundAddress, options = setupOptionsPlatform) ) - } - .repeat + .repeat } yield serverSocket.accept -> clients group("tcp") { @@ -165,7 +163,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val connectionRefused = for { port <- Network[IO] .bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - .use(s => s.localAddress.map(_.port)) + .use(serverSocket => IO.pure(serverSocket.boundAddress.asIpUnsafe.port)) _ <- Network[IO] .connect(SocketAddress(host"localhost", port)) .use_ @@ -173,12 +171,11 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { } yield () val addressAlreadyInUse = - Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)).evalMap(_.localAddress).use { - bindAddress => - Network[IO] - .bind(bindAddress) - .use_ - .interceptMessage[BindException]("Address already in use") + Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)).use { serverSocket => + Network[IO] + .bind(serverSocket.boundAddress) + .use_ + .interceptMessage[BindException]("Address already in use") } val unknownHost = Network[IO] @@ -203,8 +200,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { ) ++ optionsPlatform val setup = for { serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard), opts) - bindAddress <- Resource.eval(serverSocket.localAddressGen) - client <- Network[IO].connect(bindAddress, opts) + client <- Network[IO].connect(serverSocket.boundAddress, opts) } yield (serverSocket.accept, client) val msg = "hello" @@ -232,8 +228,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("read after timed out read") { val setup = for { serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - bindAddress <- Resource.eval(serverSocket.localAddressGen) - client <- Network[IO].connect(bindAddress) + client <- Network[IO].connect(serverSocket.boundAddress) } yield (serverSocket.accept, client) Stream .resource(setup) @@ -255,11 +250,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("can shutdown a socket that's pending a read") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => - Resource.eval(serverSocket.localAddressGen).use { bindAddress => - Network[IO].connect(bindAddress).use { _ => - serverSocket.accept.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { - case _: TimeoutException => () - } + Network[IO].connect(serverSocket.boundAddress).use { _ => + serverSocket.accept.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { + case _: TimeoutException => () } } } @@ -267,11 +260,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("accepted socket closes timely") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => - Resource.eval(serverSocket.localAddressGen).use { bindAddress => - serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].connect(bindAddress).use { client => - client.read(1).assertEquals(None) - } + serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { + Network[IO].connect(serverSocket.boundAddress).use { client => + client.read(1).assertEquals(None) } } } @@ -279,8 +270,11 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("endOfOutput / endOfInput ignores ENOTCONN") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => - Resource.eval(serverSocket.localAddressGen).use { bindAddress => - Network[IO].connect(bindAddress).surround(IO.sleep(100.millis)).background.surround { + Network[IO] + .connect(serverSocket.boundAddress) + .surround(IO.sleep(100.millis)) + .background + .surround { serverSocket.accept .take(1) .foreach { socket => @@ -291,7 +285,6 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .compile .drain } - } } } @@ -312,8 +305,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .drain .toResource serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - bindAddress <- Resource.eval(serverSocket.localAddressGen) - client <- Network[IO].connect(bindAddress) + client <- Network[IO].connect(serverSocket.boundAddress) } yield (tempFile, serverSocket.accept, client) Stream From 8c28c696f865f3d89b4b8322dbb0f6e335923f68 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Sun, 20 Apr 2025 13:51:04 -0400 Subject: [PATCH 32/79] Add address and peerAddress, deprecate localAddress and remoteAddress --- .../fs2/io/net/AsyncSocketsProvider.scala | 96 +++++++++---------- .../scala/fs2/io/net/SocketPlatform.scala | 19 ++-- .../fs2/io/net/tls/TLSSocketPlatform.scala | 8 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 22 ++--- ...hronousChannelGroupIpSocketsProvider.scala | 2 +- .../scala/fs2/io/net/SocketInfoPlatform.scala | 8 +- .../scala/fs2/io/net/SocketPlatform.scala | 26 ++--- .../fs2/io/net/JnrUnixSocketsProvider.scala | 5 +- .../io/net/SelectingIpSocketsProvider.scala | 37 ++++--- .../scala/fs2/io/net/SelectingSocket.scala | 20 ++-- .../io/net/UnixSocketsProviderPlatform.scala | 14 +-- .../fs2/io/net/tls/TLSSocketPlatform.scala | 12 ++- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 6 +- .../scala/fs2/io/internal/SocketHelpers.scala | 28 ++---- .../io/net/FdPollingIpSocketsProvider.scala | 14 ++- .../scala/fs2/io/net/FdPollingSocket.scala | 14 +-- .../io/net/FdPollingUnixSocketsProvider.scala | 15 ++- .../fs2/io/net/tls/TLSSocketPlatform.scala | 12 ++- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 14 +-- .../src/main/scala/fs2/io/net/Network.scala | 7 +- .../main/scala/fs2/io/net/ServerSocket.scala | 17 +--- .../src/main/scala/fs2/io/net/Socket.scala | 14 ++- .../main/scala/fs2/io/net/SocketGroup.scala | 5 +- .../main/scala/fs2/io/net/SocketInfo.scala | 12 ++- .../test/scala/fs2/io/net/SocketSuite.scala | 22 ++--- .../scala/fs2/io/net/UnixSocketsSuite.scala | 31 +++--- 26 files changed, 212 insertions(+), 268 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 1dc8949fe5..f9255cf820 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -27,7 +27,7 @@ import cats.effect.{Async, Resource} import cats.effect.std.Dispatcher import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, GenSocketAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{IpAddress, Port, SocketAddress, UnixSocketAddress} import fs2.concurrent.Channel import fs2.io.internal.facade @@ -55,27 +55,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } ) .evalTap(setSocketOptions(options)) - localAddressGen = F.delay { - (to match { - case Left(_) => - SocketAddress( - IpAddress.fromString(sock.localAddress.get).get, - Port.fromInt(sock.localPort.get).get - ) - case Right(_) => UnixSocketAddress("") - }): GenSocketAddress - } - remoteAddressGen = F.delay { - (to match { - case Left(_) => - SocketAddress( - IpAddress.fromString(sock.remoteAddress.get).get, - Port.fromInt(sock.remotePort.get).get - ) - case Right(addr) => addr - }): GenSocketAddress - } - socket <- Socket.forAsync(sock, localAddressGen, remoteAddressGen) + _ <- F .async[Unit] { cb => sock @@ -91,6 +71,24 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } } .toResource + + address = to match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) + case Right(_) => UnixSocketAddress("") + } + peerAddress = to match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) + case Right(addr) => addr + } + socket <- Socket.forAsync(sock, address, peerAddress) } yield socket).adaptError { case IOException(ex) => ex } protected def bindIpOrUnix( @@ -137,16 +135,14 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } } .toResource + serverSocketAddress = address match { + case Left(_) => + val addr = server.address().get + SocketAddress(IpAddress.fromString(addr.address).get, Port.fromInt(addr.port).get) + case Right(addr) => addr + } info = new SocketInfo[F] { - def localAddressGen = F.delay { - address match { - case Left(_) => - val addr = server.address().get - SocketAddress(IpAddress.fromString(addr.address).get, Port.fromInt(addr.port).get) - case Right(addr) => addr - } - } - def localAddress = localAddressGen.map(_.asIpUnsafe) + val address = serverSocketAddress private def raiseOptionError[A]: F[A] = F.raiseError( new UnsupportedOperationException( @@ -157,31 +153,27 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError def supportedOptions = F.pure(Set.empty) } + address0 = address sockets = channel.stream .evalTap(setSocketOptions(options)) .flatMap { sock => - val localAddressGen = F.delay { - (address match { - case Left(_) => - SocketAddress( - IpAddress.fromString(sock.localAddress.get).get, - Port.fromInt(sock.localPort.get).get - ) - case Right(addr) => addr - }): GenSocketAddress + val address = address0 match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.localAddress.get).get, + Port.fromInt(sock.localPort.get).get + ) + case Right(addr) => addr } - val remoteAddressGen = F.delay { - (address match { - case Left(_) => - SocketAddress( - IpAddress.fromString(sock.remoteAddress.get).get, - Port.fromInt(sock.remotePort.get).get - ) - case Right(_) => UnixSocketAddress("") - }): GenSocketAddress + val peerAddress = address0 match { + case Left(_) => + SocketAddress( + IpAddress.fromString(sock.remoteAddress.get).get, + Port.fromInt(sock.remotePort.get).get + ) + case Right(_) => UnixSocketAddress("") } - Stream.resource(Socket.forAsync(sock, localAddressGen, remoteAddressGen)) + Stream.resource(Socket.forAsync(sock, address, peerAddress)) } - serverSocket <- Resource.eval(ServerSocket(info, sockets)) - } yield serverSocket).adaptError { case IOException(ex) => ex } + } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 81d9f5c4d1..2241983888 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -25,7 +25,6 @@ package net import cats.data.{Kleisli, OptionT} import cats.effect.{Async, Resource} -import cats.syntax.all._ import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} import fs2.io.internal.{facade, SuspendedStream} @@ -33,15 +32,15 @@ private[net] trait SocketCompanionPlatform { private[net] def forAsync[F[_]]( sock: facade.net.Socket, - localAddressGen: F[GenSocketAddress], - remoteAddressGen: F[GenSocketAddress] + address: GenSocketAddress, + peerAddress: GenSocketAddress )(implicit F: Async[F]): Resource[F, Socket[F]] = suspendReadableAndRead( destroyIfNotEnded = false, destroyIfCanceled = false )(sock.asInstanceOf[Readable]) .flatMap { case (_, stream) => - SuspendedStream(stream).map(new AsyncSocket(sock, _, localAddressGen, remoteAddressGen)) + SuspendedStream(stream).map(new AsyncSocket(sock, _, address, peerAddress)) } .onFinalize { F.delay { @@ -53,8 +52,8 @@ private[net] trait SocketCompanionPlatform { private[net] class AsyncSocket[F[_]]( sock: facade.net.Socket, readStream: SuspendedStream[F, Byte], - localAddressGen0: F[GenSocketAddress], - remoteAddressGen0: F[GenSocketAddress] + val address: GenSocketAddress, + val peerAddress: GenSocketAddress )(implicit F: Async[F]) extends Socket[F] { @@ -87,14 +86,10 @@ private[net] trait SocketCompanionPlatform { override def isOpen: F[Boolean] = F.delay(sock.readyState == "open") override def localAddress: F[SocketAddress[IpAddress]] = - localAddressGen.map(_.asIpUnsafe) - - override def localAddressGen = localAddressGen0 + F.delay(address.asIpUnsafe) override def remoteAddress: F[SocketAddress[IpAddress]] = - remoteAddressGen.map(_.asIpUnsafe) - - override def remoteAddressGen = remoteAddressGen0 + F.delay(peerAddress.asIpUnsafe) override def supportedOptions: F[Set[SocketOption.Key[?]]] = F.pure( diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 7ae46e2a99..031a43a921 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -71,14 +71,14 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => ) extends Socket.AsyncSocket[F]( sock, readStream, - Async[F].delay(sys.error("unused")), - Async[F].delay(sys.error("unused")) + underlying.address, + underlying.peerAddress ) with UnsealedTLSSocket[F] { + @deprecated("3.13.0", "Use address instead") override def localAddress = underlying.localAddress - override def localAddressGen = underlying.localAddressGen + @deprecated("3.13.0", "Use peerAddress instead") override def remoteAddress = underlying.remoteAddress - override def remoteAddressGen = underlying.remoteAddressGen override def getOption[A](key: SocketOption.Key[A]) = underlying.getOption(key) override def setOption[A](key: SocketOption.Key[A], value: A) = underlying.setOption(key, value) override def supportedOptions = underlying.supportedOptions diff --git a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index c5da4e54d5..cfc2241f42 100644 --- a/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/js/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -112,7 +112,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -151,7 +151,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(Network[IO].tlsContext.system) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -191,7 +191,7 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Resource.eval(testTlsContext(false)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( clientContext .clientBuilder(_) @@ -238,7 +238,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext(true, Some(protocol))) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -293,7 +293,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) @@ -328,7 +328,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) @@ -360,7 +360,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext(true)) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -376,13 +376,13 @@ class TLSSocketSuite extends TLSSuite { Stream .resource(setup) .flatMap { case (server, clientSocket) => - val serverSocketAddresses = server.evalMap { socket => - socket.localAddress.product(socket.remoteAddress) + val serverSocketAddresses = server.map { socket => + socket.address -> socket.peerAddress } val clientSocketAddresses = - Stream.resource(clientSocket).evalMap { socket => - socket.localAddress.product(socket.remoteAddress) + Stream.resource(clientSocket).map { socket => + socket.address -> socket.peerAddress } serverSocketAddresses.parZip(clientSocketAddresses).map { diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 77bfc06bda..53a8bade05 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -144,7 +144,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( } } - setup.evalMap(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) + setup.map(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 1eb5dd7a1f..a85340fa82 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -25,7 +25,6 @@ package net import com.comcast.ip4s.GenSocketAddress import cats.effect.Async -import cats.syntax.all._ import java.nio.channels.NetworkChannel @@ -43,11 +42,8 @@ private[net] trait SocketInfoCompanionPlatform { implicit protected def asyncInstance: Async[F] protected def channel: NetworkChannel - override def localAddressGen: F[GenSocketAddress] = - asyncInstance.delay(SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress)) - - override def localAddress = - localAddressGen.map(_.asIpUnsafe) + override val address: GenSocketAddress = + SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress) override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala index d35258a7b5..f42cfd7a2d 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -28,7 +28,6 @@ import cats.effect.Async import cats.effect.std.Mutex import cats.syntax.all._ -import java.net.InetSocketAddress import java.nio.channels.{AsynchronousSocketChannel, CompletionHandler} import java.nio.{Buffer, ByteBuffer} @@ -37,7 +36,12 @@ private[net] trait SocketCompanionPlatform { ch: AsynchronousSocketChannel ): F[Socket[F]] = (Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => - new AsyncSocket[F](ch, readMutex, writeMutex) + new AsyncSocket[F]( + ch, + readMutex, + writeMutex, + SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress) + ) } private[net] abstract class BufferedReads[F[_]]( @@ -106,7 +110,8 @@ private[net] trait SocketCompanionPlatform { private final class AsyncSocket[F[_]]( ch: AsynchronousSocketChannel, readMutex: Mutex[F], - writeMutex: Mutex[F] + writeMutex: Mutex[F], + val peerAddress: GenSocketAddress )(implicit F: Async[F]) extends BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { @@ -144,21 +149,10 @@ private[net] trait SocketCompanionPlatform { } override def localAddress: F[SocketAddress[IpAddress]] = - asyncInstance.delay( - SocketAddress.fromInetSocketAddress( - channel.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - ) + asyncInstance.pure(address.asIpUnsafe) override def remoteAddress: F[SocketAddress[IpAddress]] = - F.delay( - SocketAddress.fromInetSocketAddress( - ch.getRemoteAddress.asInstanceOf[InetSocketAddress] - ) - ) - - override def remoteAddressGen: F[GenSocketAddress] = - F.delay(SocketAddressHelpers.toGenSocketAddress(ch.getRemoteAddress)) + asyncInstance.pure(peerAddress.asIpUnsafe) override def isOpen: F[Boolean] = F.delay(ch.isOpen) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index eef1509778..d620fe7ddc 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -23,7 +23,6 @@ package fs2 package io package net -import cats.syntax.all._ import cats.effect.{Async, Resource} import cats.effect.syntax.all._ @@ -77,12 +76,12 @@ private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[ "JNR unix server sockets do not support socket options" ) ) + val address0 = address val info: SocketInfo[F] = new SocketInfo[F] { + override def address = address0 override def supportedOptions = F.pure(Set.empty) override def getOption[A](key: SocketOption.Key[A]) = raiseOptionError override def setOption[A](key: SocketOption.Key[A], value: A) = raiseOptionError - override def localAddressGen = F.pure(address) - override def localAddress = localAddressGen.map(_.asIpUnsafe) } info -> Resource diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index e6b2f009a9..43db465cee 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -27,7 +27,7 @@ import cats.effect.Selector import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.syntax.all._ -import com.comcast.ip4s.{Dns, Host, SocketAddress} +import com.comcast.ip4s.{Dns, Host, IpAddress, SocketAddress} import java.net.InetSocketAddress import java.nio.channels.AsynchronousCloseException @@ -66,11 +66,18 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici } } - val make = SelectingSocket[F]( - selector, - ch, - remoteAddress(ch) - ) + val make = F + .delay { + localAddress(ch) -> remoteAddress(ch) + } + .flatMap { case (addr, peerAddr) => + SelectingSocket[F]( + selector, + ch, + addr, + peerAddr + ) + } configure *> connect *> make } @@ -118,18 +125,22 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici } *> SelectingSocket[F]( selector, ch, + localAddress(ch), remoteAddress(ch) ) } - configure *> ServerSocket(SocketInfo.forAsync(sch), accept) + configure *> F.delay(ServerSocket(SocketInfo.forAsync(sch), accept)) } - private def remoteAddress(ch: SocketChannel) = - F.delay { - SocketAddress.fromInetSocketAddress( - ch.getRemoteAddress.asInstanceOf[InetSocketAddress] - ) - } + private def localAddress(ch: SocketChannel): SocketAddress[IpAddress] = + SocketAddress.fromInetSocketAddress( + ch.getLocalAddress.asInstanceOf[InetSocketAddress] + ) + + private def remoteAddress(ch: SocketChannel): SocketAddress[IpAddress] = + SocketAddress.fromInetSocketAddress( + ch.getRemoteAddress.asInstanceOf[InetSocketAddress] + ) } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala index 36740523b4..2b7cf81031 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingSocket.scala @@ -29,9 +29,8 @@ import cats.effect.Selector import cats.effect.kernel.Async import cats.effect.std.Mutex import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import com.comcast.ip4s.{IpAddress, SocketAddress} -import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SelectionKey.OP_READ import java.nio.channels.SelectionKey.OP_WRITE @@ -42,7 +41,8 @@ private final class SelectingSocket[F[_]: LiftIO] private ( ch: SocketChannel, readMutex: Mutex[F], writeMutex: Mutex[F], - val remoteAddress: F[SocketAddress[IpAddress]] + override val address: SocketAddress[IpAddress], + val peerAddress: SocketAddress[IpAddress] )(implicit F: Async[F]) extends Socket.BufferedReads(readMutex) with SocketInfo.AsyncSocketInfo[F] { @@ -51,14 +51,10 @@ private final class SelectingSocket[F[_]: LiftIO] private ( protected def channel = ch override def localAddress: F[SocketAddress[IpAddress]] = - asyncInstance.delay( - SocketAddress.fromInetSocketAddress( - ch.getLocalAddress.asInstanceOf[InetSocketAddress] - ) - ) + asyncInstance.pure(address) - def remoteAddressGen: F[GenSocketAddress] = - remoteAddress.map(a => a: GenSocketAddress) + override def remoteAddress: F[SocketAddress[IpAddress]] = + asyncInstance.pure(peerAddress) protected def readChunk(buf: ByteBuffer): F[Int] = F.delay(ch.read(buf)).flatMap { readed => @@ -125,7 +121,8 @@ private object SelectingSocket { def apply[F[_]: LiftIO]( selector: Selector, ch: SocketChannel, - remoteAddress: F[SocketAddress[IpAddress]] + address: SocketAddress[IpAddress], + remoteAddress: SocketAddress[IpAddress] )(implicit F: Async[F]): F[Socket[F]] = (Mutex[F], Mutex[F]).flatMapN { (readMutex, writeMutex) => F.delay { @@ -134,6 +131,7 @@ private object SelectingSocket { ch, readMutex, writeMutex, + address, remoteAddress ) } diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index 71ab4c41dc..d8fb254e20 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -28,7 +28,7 @@ import cats.effect.std.Mutex import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{IpAddress, SocketAddress, UnixSocketAddress} import fs2.io.file.{Files, FileHandle, Path, SyncFileHandle} @@ -92,7 +92,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) } - (delete *> openServerChannel(address, filteredOptions)).evalMap { case (info, accept) => + (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => val acceptIncoming = Stream .resource(accept.attempt) @@ -120,8 +120,8 @@ private[net] trait UnixSocketsProviderCompanionPlatform { ch: SocketChannel, readMutex: Mutex[F], writeMutex: Mutex[F], - localAddress0: UnixSocketAddress, - remoteAddress0: UnixSocketAddress + override val address: UnixSocketAddress, + val peerAddress: UnixSocketAddress )(implicit F: Async[F]) extends Socket.BufferedReads[F](readMutex) with SocketInfo.AsyncSocketInfo[F] { @@ -148,14 +148,8 @@ private[net] trait UnixSocketsProviderCompanionPlatform { override def localAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - override def localAddressGen: F[GenSocketAddress] = - F.pure(localAddress0) - override def remoteAddress: F[SocketAddress[IpAddress]] = raiseIpAddressError - override def remoteAddressGen: F[GenSocketAddress] = - F.pure(remoteAddress0) - def isOpen: F[Boolean] = evalOnVirtualThreadIfAvailable(F.blocking(ch.isOpen())) def close: F[Unit] = evalOnVirtualThreadIfAvailable(F.blocking(ch.close())) def endOfOutput: F[Unit] = diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 899005b562..ae97497e53 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -88,17 +88,19 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def endOfInput: F[Unit] = socket.endOfInput + @deprecated("3.13.0", "Use address instead") def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress - def localAddressGen: F[GenSocketAddress] = - socket.localAddressGen - + @deprecated("3.13.0", "Use peerAddress instead") def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress - def remoteAddressGen: F[GenSocketAddress] = - socket.remoteAddressGen + def address: GenSocketAddress = + socket.address + + def peerAddress: GenSocketAddress = + socket.peerAddress def supportedOptions: F[Set[SocketOption.Key[?]]] = socket.supportedOptions diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index dbf79de436..66af8da877 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -131,7 +131,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(testTlsContext) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client <- Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) + client <- Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -158,7 +158,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- Resource.eval(Network[IO].tlsContext.system) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client <- Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) + client <- Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -188,7 +188,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Resource.eval(testTlsContext) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client <- Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap(s => clientContext .clientBuilder(s) diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index 7646ba5563..8e7aaea265 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -225,30 +225,14 @@ private[io] object SocketHelpers { throw errnoToThrowable(!optval) } - def getLocalAddress[F[_]](fd: Int, ipv4: Boolean)(implicit - F: Sync[F] - ): F[SocketAddress[IpAddress]] = - getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6).map { - case a: SocketAddress[IpAddress] @unchecked => a - case _ => throw new IllegalArgumentException + def getAddress(fd: Int, domain: CInt): GenSocketAddress = + SocketHelpers.toSocketAddress(domain) { (addr, len) => + guard_(getsockname(fd, addr, len)) } - def getLocalAddressGen[F[_]](fd: Int, domain: CInt)(implicit - F: Sync[F] - ): F[GenSocketAddress] = - F.delay { - SocketHelpers.toSocketAddress(domain) { (addr, len) => - guard_(getsockname(fd, addr, len)) - } - } - - def getRemoteAddressGen[F[_]](fd: Int, domain: CInt)(implicit - F: Sync[F] - ): F[GenSocketAddress] = - F.delay { - SocketHelpers.toSocketAddress(domain) { (addr, len) => - guard_(getpeername(fd, addr, len)) - } + def getPeerAddress(fd: Int, domain: CInt): GenSocketAddress = + SocketHelpers.toSocketAddress(domain) { (addr, len) => + guard_(getpeername(fd, addr, len)) } def toSockaddr[A]( diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index 6499776918..a38b166392 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -72,8 +72,8 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6), - SocketHelpers.getRemoteAddressGen(fd, if (ipv4) AF_INET else AF_INET6) + SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getPeerAddress(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket @@ -135,8 +135,8 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddressGen(fd, if (ipv4) AF_INET else AF_INET6), - SocketHelpers.getRemoteAddressGen(fd, if (ipv4) AF_INET else AF_INET6) + SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6), + SocketHelpers.getPeerAddress(fd, if (ipv4) AF_INET else AF_INET6) ) } yield socket @@ -149,10 +149,8 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) def supportedOptions = SocketHelpers.supportedOptions - def localAddressGen = SocketHelpers.getLocalAddressGen[F](fd, if (ipv4) AF_INET else AF_INET6) - def localAddress = localAddressGen.map(_.asIpUnsafe) + val address = SocketHelpers.getAddress(fd, if (ipv4) AF_INET else AF_INET6) } - serverSocket <- Resource.eval(ServerSocket(info, sockets)) - } yield serverSocket + } yield ServerSocket(info, sockets) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala index fd59f50853..f0f9c4bf57 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingSocket.scala @@ -42,13 +42,13 @@ private final class FdPollingSocket[F[_]: LiftIO] private ( handle: FileDescriptorPollHandle, readBuffer: ResizableBuffer[F], val isOpen: F[Boolean], - val localAddressGen: F[GenSocketAddress], - val remoteAddressGen: F[GenSocketAddress] + val address: GenSocketAddress, + val peerAddress: GenSocketAddress )(implicit F: Async[F]) extends Socket[F] { - def localAddress = localAddressGen.map(_.asIpUnsafe) - def remoteAddress = remoteAddressGen.map(_.asIpUnsafe) + def localAddress = F.pure(address.asIpUnsafe) + def remoteAddress = F.pure(peerAddress.asIpUnsafe) def endOfInput: F[Unit] = shutdownF(0) def endOfOutput: F[Unit] = shutdownF(1) @@ -132,10 +132,10 @@ private object FdPollingSocket { def apply[F[_]: LiftIO]( fd: Int, handle: FileDescriptorPollHandle, - localAddressGen: F[GenSocketAddress], - remoteAddressGen: F[GenSocketAddress] + address: GenSocketAddress, + peerAddress: GenSocketAddress )(implicit F: Async[F]): Resource[F, Socket[F]] = for { buffer <- ResizableBuffer(DefaultReadSize) isOpen <- Resource.make(F.ref(true))(_.set(false)) - } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, localAddressGen, remoteAddressGen) + } yield new FdPollingSocket(fd, handle, buffer, isOpen.get, address, peerAddress) } diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 2dfb47493f..934bf4347c 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -70,8 +70,8 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddressGen(fd, AF_UNIX), - SocketHelpers.getRemoteAddressGen(fd, AF_UNIX) + UnixSocketAddress(""), + address ) } yield socket @@ -115,13 +115,13 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F } *> F.delay(guard_(listen(fd, 0))) } + address0 = address info = new SocketInfo[F] { def getOption[A](key: SocketOption.Key[A]) = SocketHelpers.getOption(fd, key) def setOption[A](key: SocketOption.Key[A], value: A) = SocketHelpers.setOption(fd, key, value) def supportedOptions = SocketHelpers.supportedOptions - def localAddressGen = SocketHelpers.getLocalAddressGen(fd, AF_UNIX) - def localAddress = localAddressGen.map(_.asIpUnsafe) + val address = address0 } clients = Stream @@ -159,8 +159,8 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F socket <- FdPollingSocket[F]( fd, handle, - SocketHelpers.getLocalAddressGen(fd, AF_UNIX), - SocketHelpers.getRemoteAddressGen(fd, AF_UNIX) + address, + UnixSocketAddress("") ) } yield socket @@ -169,8 +169,7 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F .repeat .unNone - serverSocket <- Resource.eval(ServerSocket(info, clients)) - } yield serverSocket + } yield ServerSocket(info, clients) } private def toSockaddrUn[A](path: String)(f: Ptr[sockaddr] => A): A = { diff --git a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 511f15e216..3b89004971 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -85,17 +85,19 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def endOfInput: F[Unit] = socket.endOfInput + @deprecated("3.13.0", "Use address instead") def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress - def localAddressGen: F[GenSocketAddress] = - socket.localAddressGen - + @deprecated("3.13.0", "Use peerAddress instead") def remoteAddress: F[SocketAddress[IpAddress]] = socket.remoteAddress - def remoteAddressGen: F[GenSocketAddress] = - socket.remoteAddressGen + def address: GenSocketAddress = + socket.address + + def peerAddress: GenSocketAddress = + socket.peerAddress def supportedOptions: F[Set[SocketOption.Key[?]]] = socket.supportedOptions diff --git a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 0272aafe8a..07883b8404 100644 --- a/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/native/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -109,7 +109,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -142,7 +142,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -175,7 +175,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- Network[IO].tlsContext.systemResource serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -210,7 +210,7 @@ class TLSSocketSuite extends TLSSuite { clientContext <- testClientTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( clientContext .clientBuilder(_) @@ -253,7 +253,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) client = Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .flatMap( tlsContext .clientBuilder(_) @@ -294,7 +294,7 @@ class TLSSocketSuite extends TLSSuite { clientContext <- Network[IO].tlsContext.insecureResource tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client = Network[IO].connect(serverSocket.boundAddress).flatMap(clientContext.client(_)) + client = Network[IO].connect(serverSocket.address).flatMap(clientContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client Stream @@ -322,7 +322,7 @@ class TLSSocketSuite extends TLSSuite { val setup = for { tlsContext <- testTlsContext serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client = Network[IO].connect(serverSocket.boundAddress).flatMap(tlsContext.client(_)) + client = Network[IO].connect(serverSocket.address).flatMap(tlsContext.client(_)) } yield serverSocket.accept.flatMap(s => Stream.resource(tlsContext.server(s))) -> client val echo = Stream diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index e0868cb118..39e805a48f 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -24,7 +24,6 @@ package io package net import cats.effect.{Async, Resource} -import cats.syntax.all.* import com.comcast.ip4s.{GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress} import fs2.io.net.tls.TLSContext @@ -118,11 +117,7 @@ object Network extends NetworkCompanionPlatform { SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options ) - .flatMap(b => - Resource - .eval(b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]])) - .tupleRight(b.accept) - ) + .map(ss => ss.address.asIpUnsafe -> ss.accept) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala index 07b862f35a..9ea66c223d 100644 --- a/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/ServerSocket.scala @@ -23,8 +23,6 @@ package fs2 package io package net -import cats.Functor -import cats.syntax.all._ import com.comcast.ip4s.GenSocketAddress /** Represents a bound TCP server socket. @@ -38,8 +36,7 @@ import com.comcast.ip4s.GenSocketAddress */ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { - /** Address this socket is bound to. */ - def boundAddress: GenSocketAddress + def address: GenSocketAddress /** Stream of client sockets; typically processed concurrently to allow concurrent clients. */ def accept: Stream[F, Socket[F]] @@ -48,26 +45,16 @@ sealed trait ServerSocket[F[_]] extends SocketInfo[F] { object ServerSocket { private[net] def apply[F[_]]( - boundAddress: GenSocketAddress, info: SocketInfo[F], accept: Stream[F, Socket[F]] ): ServerSocket[F] = { - val boundAddress0 = boundAddress val accept0 = accept new ServerSocket[F] { - override def boundAddress = boundAddress0 override def accept: Stream[F, Socket[F]] = accept0 + override def address = info.address override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = info.getOption(key) override def setOption[A](key: SocketOption.Key[A], value: A) = info.setOption(key, value) override def supportedOptions = info.supportedOptions - override def localAddressGen = info.localAddressGen - override def localAddress = info.localAddress } } - - private[net] def apply[F[_]: Functor]( - info: SocketInfo[F], - accept: Stream[F, Socket[F]] - ): F[ServerSocket[F]] = - info.localAddressGen.map(boundAddress => apply(boundAddress, info, accept)) } diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index 33ec78d860..73bca58be1 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -55,14 +55,20 @@ trait Socket[F[_]] extends SocketInfo[F] { def isOpen: F[Boolean] - /** Asks for the local address of this socket. */ + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) def localAddress: F[SocketAddress[IpAddress]] - /** Asks for the remote address of the peer. */ + @deprecated( + "3.13.0", + "Use peerAddress instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) def remoteAddress: F[SocketAddress[IpAddress]] - /** Asks for the remote address of the peer. Like `remoteAddress` but supports unix sockets as well. */ - def remoteAddressGen: F[GenSocketAddress] + /** Gets the remote address of this socket. */ + def peerAddress: GenSocketAddress /** Writes `bytes` to the peer. * diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 44e4aa7601..66ce7998e2 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -24,7 +24,6 @@ package io package net import cats.effect.kernel.Resource -import cats.syntax.all._ import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} import cats.effect.kernel.Async @@ -99,8 +98,6 @@ private[net] object SocketGroup { SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), options ) - .evalMap(b => - b.localAddressGen.map(_.asInstanceOf[SocketAddress[IpAddress]]).tupleRight(b.accept) - ) + .map(ss => ss.address.asIpUnsafe -> ss.accept) } } diff --git a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala index ff78b1c5d5..2e6621b1cd 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketInfo.scala @@ -23,19 +23,21 @@ package fs2 package io package net -import com.comcast.ip4s.{GenSocketAddress, IpAddress, SocketAddress} +import com.comcast.ip4s.GenSocketAddress +/** Information about a connected socket. Super trait of both [[ServerSocket]] and [[Socket]]. */ trait SocketInfo[F[_]] { - /** Asks for the local address of this socket. Like `localAddress` but supports unix sockets as well. */ - def localAddressGen: F[GenSocketAddress] - - def localAddress: F[SocketAddress[IpAddress]] + /** Local address of this socket. */ + def address: GenSocketAddress + /** Gets the set of options that may be used with `setOption`. Note some options may not support `getOption`. */ def supportedOptions: F[Set[SocketOption.Key[?]]] + /** Gets the value of the specified option, if defined. */ def getOption[A](key: SocketOption.Key[A]): F[Option[A]] + /** Sets the specified option to the supplied value. */ def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] } diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index 0495f57e71..ed51df2de8 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -39,7 +39,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { clients = Stream .resource( - Network[IO].connect(serverSocket.boundAddress, options = setupOptionsPlatform) + Network[IO].connect(serverSocket.address, options = setupOptionsPlatform) ) .repeat } yield serverSocket.accept -> clients @@ -139,14 +139,14 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .resource(setup) .flatMap { case (server, clients) => val serverSocketAddresses = server.evalMap { socket => - socket.endOfOutput *> socket.localAddress.product(socket.remoteAddress) + socket.endOfOutput.as(socket.address -> socket.peerAddress) } val clientSocketAddresses = clients .take(1) .evalMap { socket => - socket.endOfOutput *> socket.localAddress.product(socket.remoteAddress) + socket.endOfOutput.as(socket.address -> socket.peerAddress) } serverSocketAddresses.parZip(clientSocketAddresses).map { @@ -163,7 +163,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val connectionRefused = for { port <- Network[IO] .bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - .use(serverSocket => IO.pure(serverSocket.boundAddress.asIpUnsafe.port)) + .use(serverSocket => IO.pure(serverSocket.address.asIpUnsafe.port)) _ <- Network[IO] .connect(SocketAddress(host"localhost", port)) .use_ @@ -173,7 +173,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { val addressAlreadyInUse = Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)).use { serverSocket => Network[IO] - .bind(serverSocket.boundAddress) + .bind(serverSocket.address) .use_ .interceptMessage[BindException]("Address already in use") } @@ -200,7 +200,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { ) ++ optionsPlatform val setup = for { serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard), opts) - client <- Network[IO].connect(serverSocket.boundAddress, opts) + client <- Network[IO].connect(serverSocket.address, opts) } yield (serverSocket.accept, client) val msg = "hello" @@ -228,7 +228,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("read after timed out read") { val setup = for { serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client <- Network[IO].connect(serverSocket.boundAddress) + client <- Network[IO].connect(serverSocket.address) } yield (serverSocket.accept, client) Stream .resource(setup) @@ -250,7 +250,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("can shutdown a socket that's pending a read") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => - Network[IO].connect(serverSocket.boundAddress).use { _ => + Network[IO].connect(serverSocket.address).use { _ => serverSocket.accept.head.flatMap(_.reads).compile.drain.timeout(2.seconds).recover { case _: TimeoutException => () } @@ -261,7 +261,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("accepted socket closes timely") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].connect(serverSocket.boundAddress).use { client => + Network[IO].connect(serverSocket.address).use { client => client.read(1).assertEquals(None) } } @@ -271,7 +271,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("endOfOutput / endOfInput ignores ENOTCONN") { Network[IO].bind(SocketAddress.Wildcard).use { serverSocket => Network[IO] - .connect(serverSocket.boundAddress) + .connect(serverSocket.address) .surround(IO.sleep(100.millis)) .background .surround { @@ -305,7 +305,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .drain .toResource serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) - client <- Network[IO].connect(serverSocket.boundAddress) + client <- Network[IO].connect(serverSocket.address) } yield (tempFile, serverSocket.accept, client) Stream diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 2bf669c590..93bf4e832b 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -25,7 +25,6 @@ package io.net import scala.concurrent.duration._ import cats.effect.IO -import cats.syntax.all._ import com.comcast.ip4s.UnixSocketAddress class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { @@ -61,30 +60,24 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { val server = Stream .resource(sockets.bind(address, Nil)) - .flatMap(ss => - Stream.exec(ss.localAddressGen.map { local => - assertEquals(local, address) - }) ++ ss.accept - ) + .flatMap { ss => + assertEquals(ss.address, address) + ss.accept + } .map { socket => - Stream.exec((socket.localAddressGen, socket.remoteAddressGen).mapN { - case (local, remote) => - assertEquals(local, address) - assertEquals(remote, UnixSocketAddress("")) - }) ++ - socket.reads.through(socket.writes) + assertEquals(socket.address, address) + assertEquals(socket.peerAddress, UnixSocketAddress("")) + socket.reads.through(socket.writes) } .parJoinUnbounded val msg = Chunk.array("Hello, world".getBytes) val client = Stream.resource(sockets.connect(address, Nil).evalMap { socket => - (socket.localAddressGen, socket.remoteAddressGen).mapN { case (local, remote) => - assertEquals(local, UnixSocketAddress("")) - assertEquals(remote, address) - } *> - socket.write(msg) *> socket.endOfOutput *> socket.reads.compile - .to(Chunk) - .map(read => assertEquals(read, msg)) + assertEquals(socket.address, UnixSocketAddress("")) + assertEquals(socket.peerAddress, address) + socket.write(msg) *> socket.endOfOutput *> socket.reads.compile + .to(Chunk) + .map(read => assertEquals(read, msg)) }) (Stream.sleep_[IO](1.second) ++ client) From 65333189c6f3782ac54db3a141a9e117ae1f0af0 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Sun, 20 Apr 2025 20:26:02 -0400 Subject: [PATCH 33/79] Deprecate Socket#isOpen --- .../fs2/io/net/tls/TLSSocketPlatform.scala | 1 + .../fs2/io/net/tls/TLSSocketPlatform.scala | 1 + .../src/main/scala/fs2/io/net/Socket.scala | 39 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index ae97497e53..ae31cce0db 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -120,6 +120,7 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def applicationProtocol: F[String] = engine.applicationProtocol + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") def isOpen: F[Boolean] = socket.isOpen } } diff --git a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 3b89004971..43ddc7daee 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -112,6 +112,7 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => def applicationProtocol: F[String] = connection.applicationProtocol + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") def isOpen: F[Boolean] = socket.isOpen } } diff --git a/io/shared/src/main/scala/fs2/io/net/Socket.scala b/io/shared/src/main/scala/fs2/io/net/Socket.scala index 73bca58be1..50b540f7b4 100644 --- a/io/shared/src/main/scala/fs2/io/net/Socket.scala +++ b/io/shared/src/main/scala/fs2/io/net/Socket.scala @@ -30,6 +30,9 @@ import fs2.io.file.FileHandle */ trait Socket[F[_]] extends SocketInfo[F] { + /** Gets the remote address of this socket. */ + def peerAddress: GenSocketAddress + /** Reads up to `maxBytes` from the peer. * * Returns `None` if the "end of stream" is reached, indicating there will be no more bytes sent. @@ -50,26 +53,9 @@ trait Socket[F[_]] extends SocketInfo[F] { */ def endOfInput: F[Unit] - /** Indicates to peer, we are done writing. * */ + /** Indicates to peer that we are done writing. * */ def endOfOutput: F[Unit] - def isOpen: F[Boolean] - - @deprecated( - "3.13.0", - "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" - ) - def localAddress: F[SocketAddress[IpAddress]] - - @deprecated( - "3.13.0", - "Use peerAddress instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" - ) - def remoteAddress: F[SocketAddress[IpAddress]] - - /** Gets the remote address of this socket. */ - def peerAddress: GenSocketAddress - /** Writes `bytes` to the peer. * * Completes when the bytes are written to the socket. @@ -107,6 +93,23 @@ trait Socket[F[_]] extends SocketInfo[F] { go(offset, count).through(writes) } + + // Deprecated members + + @deprecated("3.13.0", "No replacement; sockets are open until they are finalized") + def isOpen: F[Boolean] + + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + def localAddress: F[SocketAddress[IpAddress]] + + @deprecated( + "3.13.0", + "Use peerAddress instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + def remoteAddress: F[SocketAddress[IpAddress]] } object Socket extends SocketCompanionPlatform From e7ca92093f92bce2c0da525eb346db949a065ba1 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 07:34:56 -0400 Subject: [PATCH 34/79] Remove some unnecesssary changes from net facade --- io/js/src/main/scala/fs2/io/internal/facade/net.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/internal/facade/net.scala b/io/js/src/main/scala/fs2/io/internal/facade/net.scala index e7ef9d342e..cbb3ce3724 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/net.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/net.scala @@ -41,7 +41,7 @@ private[io] object net { @js.native trait Server extends EventEmitter { - def address(): js.UndefOr[BoundAddress] = js.native + def address(): js.UndefOr[ServerAddress] = js.native def listening: Boolean = js.native @@ -56,7 +56,7 @@ private[io] object net { } @js.native - trait BoundAddress extends js.Object { + trait ServerAddress extends js.Object { def address: String = js.native def port: Int = js.native } @@ -100,8 +100,6 @@ private[io] object net { def remotePort: js.UndefOr[Int] = js.native - def remoteFamily: js.UndefOr[String] = js.native - def end(): Socket = js.native def setEncoding(encoding: String): Socket = js.native @@ -113,7 +111,6 @@ private[io] object net { def setTimeout(timeout: Double): Socket = js.native def timeout: Double = js.native - } } From f4bc9c8f56f392a6ef4e39d8688e71eff53ada93 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 07:48:26 -0400 Subject: [PATCH 35/79] Cleanup in selecting ip sockets provider --- .../io/net/SelectingIpSocketsProvider.scala | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index 43db465cee..528247c079 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -22,19 +22,17 @@ package fs2 package io.net -import cats.effect.LiftIO -import cats.effect.Selector -import cats.effect.kernel.Async -import cats.effect.kernel.Resource +import cats.effect.{Async, LiftIO, Resource, Selector} import cats.syntax.all._ import com.comcast.ip4s.{Dns, Host, IpAddress, SocketAddress} import java.net.InetSocketAddress -import java.nio.channels.AsynchronousCloseException -import java.nio.channels.ClosedChannelException -import java.nio.channels.SelectionKey.OP_ACCEPT -import java.nio.channels.SelectionKey.OP_CONNECT -import java.nio.channels.SocketChannel +import java.nio.channels.{ + AsynchronousCloseException, + ClosedChannelException, + SelectionKey, + SocketChannel +} private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implicit F: Async[F], @@ -59,27 +57,19 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici val connect = to.resolve.flatMap { ip => F.delay(ch.connect(ip.toInetSocketAddress)).flatMap { connected => selector - .select(ch, OP_CONNECT) + .select(ch, SelectionKey.OP_CONNECT) .to .untilM_(F.delay(ch.finishConnect())) - .unlessA(connected) - } - } - - val make = F - .delay { - localAddress(ch) -> remoteAddress(ch) - } - .flatMap { case (addr, peerAddr) => - SelectingSocket[F]( + .unlessA(connected) *> SelectingSocket[F]( selector, ch, - addr, - peerAddr + localAddress(ch), + remoteAddress(ch) ) } + } - configure *> connect *> make + configure *> connect } def bind( @@ -107,7 +97,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici .bracketFull[F, SocketChannel] { poll => def go: F[SocketChannel] = F.delay(sch.accept()).flatMap { - case null => poll(selector.select(sch, OP_ACCEPT).to) *> go + case null => poll(selector.select(sch, SelectionKey.OP_ACCEPT).to) *> go case ch => F.pure(ch) } go @@ -122,12 +112,15 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici F.delay { ch.configureBlocking(false) options.foreach(opt => ch.setOption(opt.key, opt.value)) - } *> SelectingSocket[F]( - selector, - ch, - localAddress(ch), - remoteAddress(ch) - ) + localAddress(ch) -> remoteAddress(ch) + }.flatMap { case (addr, peerAddr) => + SelectingSocket[F]( + selector, + ch, + addr, + peerAddr + ) + } } configure *> F.delay(ServerSocket(SocketInfo.forAsync(sch), accept)) From 896738a8ec5f73837eef0ee2cec89443d8b3b834 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 09:03:09 -0400 Subject: [PATCH 36/79] Unify Network implementations --- .../scala/fs2/io/net/NetworkPlatform.scala | 61 ++-------------- .../scala/fs2/io/net/NetworkPlatform.scala | 55 ++------------- .../scala/fs2/io/internal/SocketHelpers.scala | 2 +- ...cala => DatagramSocketGroupPlatform.scala} | 4 +- .../fs2/io/net/DatagramSocketPlatform.scala | 41 +++++++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 51 ++------------ .../src/main/scala/fs2/io/net/Datagram.scala | 0 .../scala/fs2/io/net/DatagramSocket.scala | 0 .../fs2/io/net/DatagramSocketGroup.scala | 0 .../src/main/scala/fs2/io/net/Network.scala | 69 ++++++++++++++++++- 10 files changed, 128 insertions(+), 155 deletions(-) rename io/native/src/main/scala/fs2/io/net/{DatagramSocketGroup.scala => DatagramSocketGroupPlatform.scala} (91%) create mode 100644 io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala rename io/{js-jvm => shared}/src/main/scala/fs2/io/net/Datagram.scala (100%) rename io/{js-jvm => shared}/src/main/scala/fs2/io/net/DatagramSocket.scala (100%) rename io/{js-jvm => shared}/src/main/scala/fs2/io/net/DatagramSocketGroup.scala (100%) diff --git a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala index 1e51aed08a..edcccb2ffd 100644 --- a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,72 +23,21 @@ package fs2 package io package net -import cats.effect.{Async, IO, LiftIO, Resource} -import com.comcast.ip4s.{GenSocketAddress, Host, Port, SocketAddress, UnixSocketAddress} -import fs2.io.net.tls.TLSContext +import cats.effect.{Async, LiftIO} private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - def forIO: Network[IO] = forLiftIO implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = { val _ = LiftIO[F] forAsync } - // TODO pull up - import cats.ApplicativeThrow - private def matchAddress[F[_]: ApplicativeThrow, A]( - address: GenSocketAddress, - ifIp: SocketAddress[Host] => F[A], - ifUnix: UnixSocketAddress => F[A] - ): F[A] = - address match { - case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => - ApplicativeThrow[F].raiseError( - new UnsupportedOperationException(s"Unsupported address type: $other") - ) - } - def forAsync[F[_]](implicit F: Async[F]): Network[F] = - new AsyncNetwork[F] { - - private lazy val ipSockets = IpSocketsProvider.forAsync[F] - private lazy val unixSockets = UnixSocketsProvider.forAsync[F] - private lazy val datagramSocketGroup = DatagramSocketGroup.forAsync[F] - - override def connect( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, Socket[F]] = - matchAddress( - address, - sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options) - ) - - override def bind( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = - matchAddress( - address, - sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options) - ) - - override def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[DatagramSocketOption], - protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - datagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) - - override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync - + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = IpSocketsProvider.forAsync[F] + protected def mkUnixSocketsProvider = UnixSocketsProvider.forAsync[F] + protected def mkDatagramSocketGroup = DatagramSocketGroup.forAsync[F] } } diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 1dea8c52ca..6d02702bf0 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,13 +23,12 @@ package fs2 package io package net -import cats.ApplicativeThrow import cats.effect.IO import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{Dns, GenSocketAddress, Host, Port} import fs2.internal.ThreadFactories @@ -76,22 +75,6 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private lazy val globalAdsg = AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) - private def matchAddress[F[_]: ApplicativeThrow, A]( - address: GenSocketAddress, - ifIp: SocketAddress[Host] => F[A], - ifUnix: UnixSocketAddress => F[A] - ): F[A] = - address match { - case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => - ApplicativeThrow[F].raiseError( - new UnsupportedOperationException(s"Unsupported address type: $other") - ) - } - - def forIO: Network[IO] = forLiftIO - implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = new AsyncNetwork[F] { private lazy val fallback = forAsync[F] @@ -159,38 +142,10 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N forAsyncAndDns(F, Dns.forAsync(F)) def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = - new AsyncNetwork[F] { - private lazy val ipSockets = IpSocketsProvider.forAsync[F] - private lazy val unixSockets = UnixSocketsProvider.forAsync[F] - private lazy val globalDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) - - def connect( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, Socket[F]] = - matchAddress( - address, - sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options) - ) - - def bind( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = - matchAddress( - address, - sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options) - ) - - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - globalDatagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = IpSocketsProvider.forAsync[F] + protected def mkUnixSocketsProvider = UnixSocketsProvider.forAsync[F] + protected def mkDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = Resource diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index 8e7aaea265..da377bc7a0 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -323,7 +323,7 @@ private[io] object SocketHelpers { def allocateSockaddr[A](domain: CInt)( f: (Ptr[sockaddr], Ptr[socklen_t]) => A ): A = { - // FIXME: Scala Native 0.4 doesn't support getsconame for unix sockets; after upgrading to 0.5, + // FIXME: Scala Native 0.4 doesn't support getsockname for unix sockets; after upgrading to 0.5, // this can be simplified to just allocate a sockaddr_un val (addr, lenValue) = // allocate enough for unix socket address if (domain == AF_UNIX) diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala similarity index 91% rename from io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala rename to io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala index 22e2c3687c..71ed8d5291 100644 --- a/io/native/src/main/scala/fs2/io/net/DatagramSocketGroup.scala +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala @@ -23,4 +23,6 @@ package fs2 package io package net -trait DatagramSocketGroup[F[_]] {} +private[net] trait DatagramSocketGroupCompanionPlatform { + type ProtocolFamily = java.net.ProtocolFamily +} diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala new file mode 100644 index 0000000000..a25fc1ea0b --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import com.comcast.ip4s.IpAddress + +private[net] trait DatagramSocketPlatform[F[_]] { + private[net] trait GroupMembershipPlatform { + + /** Blocks datagrams from the specified source address. */ + def block(source: IpAddress): F[Unit] + + /** Unblocks datagrams from the specified source address. */ + def unblock(source: IpAddress): F[Unit] + } +} + +private[net] trait DatagramSocketCompanionPlatform { + type NetworkInterface = java.net.NetworkInterface +} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index 96dab508e7..c74301edc5 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -23,58 +23,19 @@ package fs2 package io package net -import cats.ApplicativeThrow -import cats.effect.IO -import cats.effect.LiftIO -import cats.effect.kernel.{Async, Resource} +import cats.effect.{Async, LiftIO} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.Dns private[net] trait NetworkPlatform[F[_]] private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - private def matchAddress[F[_]: ApplicativeThrow, A]( - address: GenSocketAddress, - ifIp: SocketAddress[Host] => F[A], - ifUnix: UnixSocketAddress => F[A] - ): F[A] = - address match { - case sa: SocketAddress[Host] => ifIp(sa) - case ua: UnixSocketAddress => ifUnix(ua) - case other => - ApplicativeThrow[F].raiseError( - new UnsupportedOperationException(s"Unsupported address type: $other") - ) - } - - def forIO: Network[IO] = forLiftIO - implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = - new AsyncNetwork[F] { - private lazy val ipSockets = + new AsyncProviderBasedNetwork[F] { + protected def mkIpSocketsProvider = new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) - private lazy val unixSockets = new FdPollingUnixSocketsProvider[F] - - def connect( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, Socket[F]] = - matchAddress( - address, - sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options) - ) - - def bind( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = - matchAddress( - address, - sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options) - ) - + protected def mkUnixSocketsProvider = new FdPollingUnixSocketsProvider[F] + protected def mkDatagramSocketGroup = throw new UnsupportedOperationException } } diff --git a/io/js-jvm/src/main/scala/fs2/io/net/Datagram.scala b/io/shared/src/main/scala/fs2/io/net/Datagram.scala similarity index 100% rename from io/js-jvm/src/main/scala/fs2/io/net/Datagram.scala rename to io/shared/src/main/scala/fs2/io/net/Datagram.scala diff --git a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocket.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala similarity index 100% rename from io/js-jvm/src/main/scala/fs2/io/net/DatagramSocket.scala rename to io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala diff --git a/io/js-jvm/src/main/scala/fs2/io/net/DatagramSocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala similarity index 100% rename from io/js-jvm/src/main/scala/fs2/io/net/DatagramSocketGroup.scala rename to io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 39e805a48f..1c8e0378d9 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -23,8 +23,17 @@ package fs2 package io package net -import cats.effect.{Async, Resource} -import com.comcast.ip4s.{GenSocketAddress, Host, IpAddress, Ipv4Address, Port, SocketAddress} +import cats.ApplicativeThrow +import cats.effect.{Async, IO, Resource} +import com.comcast.ip4s.{ + GenSocketAddress, + Host, + IpAddress, + Ipv4Address, + Port, + SocketAddress, + UnixSocketAddress +} import fs2.io.net.tls.TLSContext /** Provides the ability to work with TCP, UDP, and TLS. @@ -73,6 +82,8 @@ sealed trait Network[F[_]] } object Network extends NetworkCompanionPlatform { + def forIO: Network[IO] = forLiftIO + private[fs2] trait UnsealedNetwork[F[_]] extends Network[F] private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { @@ -95,6 +106,20 @@ object Network extends NetworkCompanionPlatform { override def tlsContext: TLSContext.Builder[F] = TLSContext.Builder.forAsync[F] + protected def matchAddress[G[_]: ApplicativeThrow, A]( + address: GenSocketAddress, + ifIp: SocketAddress[Host] => G[A], + ifUnix: UnixSocketAddress => G[A] + ): G[A] = + address match { + case sa: SocketAddress[Host] => ifIp(sa) + case ua: UnixSocketAddress => ifUnix(ua) + case other => + ApplicativeThrow[G].raiseError( + new UnsupportedOperationException(s"Unsupported address type: $other") + ) + } + // Implementations of deprecated operations override def client( @@ -120,5 +145,45 @@ object Network extends NetworkCompanionPlatform { .map(ss => ss.address.asIpUnsafe -> ss.accept) } + private[fs2] abstract class AsyncProviderBasedNetwork[F[_]](implicit F: Async[F]) + extends AsyncNetwork[F] { + protected def mkIpSocketsProvider: IpSocketsProvider[F] + protected def mkUnixSocketsProvider: UnixSocketsProvider[F] + protected def mkDatagramSocketGroup: DatagramSocketGroup[F] + + protected lazy val ipSockets: IpSocketsProvider[F] = mkIpSocketsProvider + protected lazy val unixSockets: UnixSocketsProvider[F] = mkUnixSocketsProvider + protected lazy val datagramSocketGroup: DatagramSocketGroup[F] = mkDatagramSocketGroup + + override def connect( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + matchAddress( + address, + sa => ipSockets.connect(sa, options), + ua => unixSockets.connect(ua, options) + ) + + override def bind( + address: GenSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + matchAddress( + address, + sa => ipSockets.bind(sa, options), + ua => unixSockets.bind(ua, options) + ) + + override def openDatagramSocket( + address: Option[Host], + port: Option[Port], + options: List[DatagramSocketOption], + protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] + ): Resource[F, DatagramSocket[F]] = + datagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) + + } + def apply[F[_]](implicit F: Network[F]): F.type = F } From e47817c5a4ac48fb2dbc264ac88dbc33b746fb72 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 09:08:35 -0400 Subject: [PATCH 37/79] Fix 2.12 compilation --- .../src/main/scala/fs2/io/net/SocketInfoPlatform.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index a85340fa82..4037034d5e 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -28,7 +28,7 @@ import cats.effect.Async import java.nio.channels.NetworkChannel -import scala.jdk.CollectionConverters.* +import CollectionCompat.* private[net] trait SocketInfoCompanionPlatform { private[net] def forAsync[F[_]](ch: NetworkChannel)(implicit F: Async[F]): SocketInfo[F] = From f3c5f703d0a9b85a8e04895a1bc6d265e6a57eaa Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 10:54:23 -0400 Subject: [PATCH 38/79] Update to ip4s 3.7.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index adf0a25495..3f121880b7 100644 --- a/build.sbt +++ b/build.sbt @@ -353,7 +353,7 @@ lazy val io = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "fs2-io", tlVersionIntroduced ~= { _.updated("3", "3.1.0") }, - libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.6.0-94-3751623-SNAPSHOT", + libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.7.0", tlJdkRelease := None ) .jvmSettings( From 0ee5c57c17bd1ae09558c28ba9b4fe7f8c32b9a5 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 11:05:07 -0400 Subject: [PATCH 39/79] Fix JS 2.12 compilation --- io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala | 4 ---- io/shared/src/test/scala/fs2/io/net/SocketSuite.scala | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala index 031a43a921..9fb45cf2f0 100644 --- a/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/tls/TLSSocketPlatform.scala @@ -75,10 +75,6 @@ private[tls] trait TLSSocketCompanionPlatform { self: TLSSocket.type => underlying.peerAddress ) with UnsealedTLSSocket[F] { - @deprecated("3.13.0", "Use address instead") - override def localAddress = underlying.localAddress - @deprecated("3.13.0", "Use peerAddress instead") - override def remoteAddress = underlying.remoteAddress override def getOption[A](key: SocketOption.Key[A]) = underlying.getOption(key) override def setOption[A](key: SocketOption.Key[A], value: A) = underlying.setOption(key, value) override def supportedOptions = underlying.supportedOptions diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index ed51df2de8..e24d556d21 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -24,7 +24,7 @@ package io package net import cats.effect.IO -import com.comcast.ip4s._ +import com.comcast.ip4s.{UnknownHostException => Ip4sUnknownHostException, _} import scala.concurrent.duration._ import scala.concurrent.TimeoutException @@ -183,7 +183,7 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { .use_ .attempt .map { - case Left(ex: UnknownHostException) => + case Left(ex: Ip4sUnknownHostException) => assert( ex.getMessage == "not.example.com: Name or service not known" || ex.getMessage == "not.example.com: nodename nor servname provided, or not known" ) From 0807afba63136eac700b21e74d6c2d05f6418c23 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 12:53:10 -0400 Subject: [PATCH 40/79] Mima fixes --- build.sbt | 50 ++++++++++++++++++- .../net/unixsocket/UnixSocketsPlatform.scala | 45 +++++++++++++++++ .../net/unixsocket/UnixSocketsPlatform.scala | 45 +++++++++++++++++ .../scala/fs2/io/net/NetworkPlatform.scala | 6 +++ .../net/unixsocket/UnixSocketsPlatform.scala | 32 ++++++++++++ .../fs2/io/net/unixsocket/UnixSockets.scala | 14 ++---- 6 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala create mode 100644 io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala create mode 100644 io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala diff --git a/build.sbt b/build.sbt index 3f121880b7..4053f3f319 100644 --- a/build.sbt +++ b/build.sbt @@ -272,7 +272,55 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ), ProblemFilters.exclude[MissingTypesProblem]( "fs2.interop.flow.StreamSubscriber$State$WaitingOnUpstream$" - ) + ), + // Network refactor: #3563 + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.connect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bind"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bindAndAccept"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Socket.address"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Socket.peerAddress"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.address"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.supportedOptions"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.getOption"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.Socket.setOption"), + ProblemFilters.exclude[DirectMissingMethodProblem]( + "fs2.io.net.SocketCompanionPlatform#AsyncSocket.this" + ), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$AbstractAsyncSocketGroup"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroupCompanionPlatform"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.SocketGroupCompanionPlatform$AsyncSocketGroup" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.address"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSSocket.supportedOptions" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.getOption"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]("fs2.io.net.tls.TLSSocket.setOption"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSockets"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSockets$"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JdkUnixSocketsImpl"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSockets"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSockets$"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.JnrUnixSocketsImpl"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.unixsocket.UnixSocketsCompanionPlatform$AsyncSocket" + ), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.unixsocket.UnixSocketsCompanionPlatform$AsyncUnixSockets" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.SelectingSocket.apply"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SelectingSocketGroup"), + ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.io.net.Socket.forAsync"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.SocketOptionCompanionPlatform#Key.get" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.Network.openDatagramSocket" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.FdPollingSocket.apply"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.FdPollingSocketGroup"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.FdPollingUnixSockets") ) lazy val root = tlCrossRootProject diff --git a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala new file mode 100644 index 0000000000..0801afc99a --- /dev/null +++ b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net +package unixsocket + +import cats.effect.{Async, IO, LiftIO} +import fs2.io.file.Files + +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + def forIO: UnixSockets[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = + forAsyncAndFiles(Files.forAsync(F), F) + + def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = { + val _ = Files[F] + new AsyncUnixSockets(UnixSocketsProvider.forAsync) + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala new file mode 100644 index 0000000000..220008f4da --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net +package unixsocket + +import cats.effect.{Async, IO, LiftIO} +import fs2.io.file.Files + +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + def forIO: UnixSockets[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = { + val _ = Files[F] + new AsyncUnixSockets(UnixSocketsProvider.forAsync) + } + + def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = + forAsyncAndFiles(F, Files.forAsync(F)) +} diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index c74301edc5..a5e79e13e3 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -38,4 +38,10 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N protected def mkUnixSocketsProvider = new FdPollingUnixSocketsProvider[F] protected def mkDatagramSocketGroup = throw new UnsupportedOperationException } + + def forAsync[F[_]](implicit F: Async[F]): Network[F] = + forAsyncAndDns(F, Dns.forAsync(F)) + + def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = + throw new UnsupportedOperationException("must use forLiftIO instead of forAsync/forAsyncAndDns") } diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala new file mode 100644 index 0000000000..8d7768d427 --- /dev/null +++ b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net +package unixsocket + +import cats.effect.{Async, LiftIO} + +private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = + new AsyncUnixSockets(new FdPollingUnixSocketsProvider[F]) +} diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 3604c23456..4aa6dce6f9 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -21,7 +21,7 @@ package fs2.io.net.unixsocket -import cats.effect.{Async, IO, LiftIO, Resource} +import cats.effect.{Async, Resource} import com.comcast.ip4s.{UnixSocketAddress => Ip4sUnixSocketAddress} @@ -50,17 +50,11 @@ trait UnixSockets[F[_]] { ): Stream[F, Socket[F]] } -object UnixSockets { +object UnixSockets extends UnixSocketsCompanionPlatform { def apply[F[_]](implicit F: UnixSockets[F]): UnixSockets[F] = F - def forIO: UnixSockets[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = - new AsyncUnixSockets[F] - - private class AsyncUnixSockets[F[_]: Async: LiftIO] extends UnixSockets[F] { - - private val delegate = UnixSocketsProvider.forLiftIO[F] + protected class AsyncUnixSockets[F[_]: Async](delegate: UnixSocketsProvider[F]) + extends UnixSockets[F] { def client(address: UnixSocketAddress): Resource[F, Socket[F]] = delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) From 91c25f66815b4b6244b76f2974bf53059d418226 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 13:16:57 -0400 Subject: [PATCH 41/79] Fix native 2.12 warnings --- io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index a5e79e13e3..fc04a94c2f 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -42,6 +42,8 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N def forAsync[F[_]](implicit F: Async[F]): Network[F] = forAsyncAndDns(F, Dns.forAsync(F)) - def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = + def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = { + val _ = (F, dns) throw new UnsupportedOperationException("must use forLiftIO instead of forAsync/forAsyncAndDns") + } } From 861f7eaf26fa383a7a3d35e568432dc3cafe9c0b Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 13:25:22 -0400 Subject: [PATCH 42/79] Fix selecting socket address NPE --- .../io/net/SelectingIpSocketsProvider.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index 528247c079..685177ec50 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -60,12 +60,18 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici .select(ch, SelectionKey.OP_CONNECT) .to .untilM_(F.delay(ch.finishConnect())) - .unlessA(connected) *> SelectingSocket[F]( - selector, - ch, - localAddress(ch), - remoteAddress(ch) - ) + .unlessA(connected) *> F + .delay { + localAddress(ch) -> remoteAddress(ch) + } + .flatMap { case (addr, peerAddr) => + SelectingSocket[F]( + selector, + ch, + addr, + peerAddr + ) + } } } From 7395a775c575ade45ad85a787629e1b306ef32cd Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 13:58:34 -0400 Subject: [PATCH 43/79] Fix JVM unix sockets test --- .../src/main/scala/fs2/io/net/SocketInfoPlatform.scala | 9 +++++---- .../scala/fs2/io/net/UnixSocketsProviderPlatform.scala | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala index 4037034d5e..7535c52d31 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketInfoPlatform.scala @@ -37,14 +37,11 @@ private[net] trait SocketInfoCompanionPlatform { def channel = ch } - private[net] trait AsyncSocketInfo[F[_]] extends SocketInfo[F] { + private[net] trait OptionsSupport[F[_]] extends SocketInfo[F] { implicit protected def asyncInstance: Async[F] protected def channel: NetworkChannel - override val address: GenSocketAddress = - SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress) - override def supportedOptions: F[Set[SocketOption.Key[?]]] = asyncInstance.delay { channel.supportedOptions.asScala.toSet @@ -66,4 +63,8 @@ private[net] trait SocketInfoCompanionPlatform { } } + private[net] trait AsyncSocketInfo[F[_]] extends OptionsSupport[F] { + override val address: GenSocketAddress = + SocketAddressHelpers.toGenSocketAddress(channel.getLocalAddress) + } } diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index d8fb254e20..f6ef65072b 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -124,7 +124,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { val peerAddress: UnixSocketAddress )(implicit F: Async[F]) extends Socket.BufferedReads[F](readMutex) - with SocketInfo.AsyncSocketInfo[F] { + with SocketInfo.OptionsSupport[F] { protected def asyncInstance = F protected def channel = ch From 8a0df13161202b62a2e21f00c5dcbaee37bc7309 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 18:33:07 -0400 Subject: [PATCH 44/79] Fix site docs --- .../src/main/scala/fs2/io/net/Network.scala | 6 +- site/concurrency-primitives.md | 1 - site/io.md | 66 ++++++++++--------- site/timeseries.md | 2 +- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 1c8e0378d9..21430fb372 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -51,11 +51,7 @@ import fs2.io.net.tls.TLSContext * In this example, the `F[_]` parameter to `send` requires the `Network` constraint instead * of requiring the much more powerful `Async` constraint. * - * The `Network` instance has a set of global resources used for managing sockets. Alternatively, - * use the `socketGroup` and `datagramSocketGroup` operations to manage the lifecycle of underlying - * resources. - * - * An instance of `Network` is available for any effect `F` which has an `Async[F]` instance. + * An instance of `Network` is available for any effect `F` which has a `LiftIO[F]` instance. */ sealed trait Network[F[_]] extends NetworkPlatform[F] diff --git a/site/concurrency-primitives.md b/site/concurrency-primitives.md index 9efc080e72..09146e6325 100644 --- a/site/concurrency-primitives.md +++ b/site/concurrency-primitives.md @@ -147,7 +147,6 @@ The program ends after 15 seconds when the signal interrupts the publishing of m ```scala mdoc:silent import scala.concurrent.duration._ -import scala.language.higherKinds import cats.effect.std.Console import cats.effect.{Clock, IO, IOApp, Temporal} import cats.syntax.all._ diff --git a/site/io.md b/site/io.md index 7b04b5ed51..90f2c4af90 100644 --- a/site/io.md +++ b/site/io.md @@ -33,7 +33,7 @@ import cats.syntax.all._ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]: F[Unit] = - Network[F].client(SocketAddress(host"localhost", port"5555")).use { socket => + Network[F].connect(SocketAddress(host"localhost", port"5555")).use { socket => socket.write(Chunk.array("Hello, world!".getBytes)) >> socket.read(8192).flatMap { response => Console[F].println(s"Response: $response") @@ -41,9 +41,9 @@ def client[F[_]: MonadCancelThrow: Console: Network]: F[Unit] = } ``` -To open a socket that's connected to `localhost:5555`, we use the `client` method on the `Network` capability. The `Network` capability provides the runtime environment for the sockets it creates. +To open a socket that's connected to `localhost:5555`, we use the `connect` method on the `Network` capability. The `Network` capability provides the runtime environment for the sockets it creates. -The `Network[F].client` method returns a `Resource[F, Socket[F]]` which automatically closes the socket after the resource has been used. To write data to the socket, we call `socket.write`, which takes a `Chunk[Byte]` and returns an `F[Unit]`. Once the write completes, we do a single read from the socket via `socket.read`, passing the maximum amount of bytes we want to read. The returns an `F[Option[Chunk[Byte]]]` -- `None` if the socket reaches end of input and `Some` if the read produced a chunk. Finally, we print the response to the console. +The `Network[F].connect` method returns a `Resource[F, Socket[F]]` which automatically closes the socket after the resource has been used. To write data to the socket, we call `socket.write`, which takes a `Chunk[Byte]` and returns an `F[Unit]`. Once the write completes, we do a single read from the socket via `socket.read`, passing the maximum amount of bytes we want to read. The returns an `F[Option[Chunk[Byte]]]` -- `None` if the socket reaches end of input and `Some` if the read produced a chunk. Finally, we print the response to the console. Note we aren't doing any binary message framing or packetization in this example. Hence, it's very possible for the single read to only receive a portion of the original message -- perhaps just the bytes for `"Hello, w"`. We can use FS2 streams to simplify this. The `Socket` trait defines stream operations -- `writes` and `reads`. We could rewrite this example using the stream operations like so: @@ -56,7 +56,7 @@ import cats.syntax.all._ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]: Stream[F, Unit] = - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { socket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { socket => Stream("Hello, world!") .through(text.utf8.encode) .through(socket.writes) ++ @@ -76,7 +76,7 @@ This program won't end until the server side closes the socket or indicates ther ```scala mdoc:nest def client[F[_]: MonadCancelThrow: Console: Network]: Stream[F, Unit] = - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { socket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) .through(text.utf8.encode) @@ -95,23 +95,23 @@ To update the write side, we added `.interleave(Stream.constant("\n"))` before d #### Handling Connection Errors -If a TCP connection cannot be established, `socketGroup.client` fails with a `java.net.ConnectException`. To automatically attempt a reconnection, we can handle the `ConnectException` and try connecting again. +If a TCP connection cannot be established, `connect` fails with a `fs2.io.net.ConnectException`. To automatically attempt a reconnection, we can handle the `ConnectException` and try connecting again. ```scala mdoc:nest import scala.concurrent.duration._ import cats.effect.Temporal -import fs2.io.net.Socket -import java.net.ConnectException +import fs2.io.net.{ConnectException, Socket} -def connect[F[_]: Temporal: Network](address: SocketAddress[Host]): Stream[F, Socket[F]] = - Stream.resource(Network[F].client(address)) +def retryingConnect[F[_]: Temporal: Network](address: SocketAddress[Host]): Stream[F, Socket[F]] = + Stream.resource(Network[F].connect(address)) .handleErrorWith { case _: ConnectException => - connect(address).delayBy(5.seconds) + retryingConnect(address).delayBy(5.seconds) + case other => Stream.raiseError(other) } def client[F[_]: Temporal: Console: Network]: Stream[F, Unit] = - connect(SocketAddress(host"localhost", port"5555")).flatMap { socket => + retryingConnect(SocketAddress(host"localhost", port"5555")).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) .through(text.utf8.encode) @@ -126,7 +126,7 @@ def client[F[_]: Temporal: Console: Network]: Stream[F, Unit] = } ``` -We've extracted the `Network[IO].client` call in to a new method called `connect`. The connect method attempts to create a client and handles the `ConnectException`. Upon encountering the exception, we call `connect` recursively after a 5 second delay. Because we are using `delayBy`, we needed to add a `Temporal` constraint to `F`. This same pattern could be used for more advanced retry strategies -- e.g., exponential delays and failing after a fixed number of attempts. Streams that call methods on `Socket` can fail with exceptions due to loss of the underlying TCP connection. Such exceptions can be handled in a similar manner. +We've extracted the `Network[IO].connect` call in to a new method called `retryingConnect`. The `retryingConnect` method attempts to create a client and handles the `ConnectException`. Upon encountering the exception, we call `retryingConnect` recursively after a 5 second delay. Because we are using `delayBy`, we needed to add a `Temporal` constraint to `F`. This same pattern could be used for more advanced retry strategies -- e.g., exponential delays and failing after a fixed number of attempts. Streams that call methods on `Socket` can fail with exceptions due to loss of the underlying TCP connection. Such exceptions can be handled in a similar manner. ### Servers @@ -136,18 +136,20 @@ Now let's implement a server application that communicates with the client app w import cats.effect.Concurrent def echoServer[F[_]: Concurrent: Network]: F[Unit] = - Network[F].server(port = Some(port"5555")).map { client => - client.reads - .through(text.utf8.decode) - .through(text.lines) - .interleave(Stream.constant("\n")) - .through(text.utf8.encode) - .through(client.writes) - .handleErrorWith(_ => Stream.empty) // handle errors of client sockets + Stream.resource(Network[F].bind(SocketAddress.port(port"5555"))).map { serverSocket => + serverSocket.accept.map { clientSocket => + clientSocket.reads + .through(text.utf8.decode) + .through(text.lines) + .interleave(Stream.constant("\n")) + .through(text.utf8.encode) + .through(clientSocket.writes) + .handleErrorWith(_ => Stream.empty) // handle errors of client sockets + } }.parJoin(100).compile.drain ``` -We start with a call to `Network[IO].server` which returns a value of an interesting type -- `Stream[F, Socket[F]]`. This is an infinite stream of client sockets -- each time a client connects to the server, a `Socket[F]` is emitted, allowing interaction with that client. The lifetime of the client socket is managed by the overall stream -- e.g. flat mapping over a socket will keep that socket open until the returned inner stream completes, at which point, the client socket is closed and any underlying resources are returned to the runtime environment. +We start with a call to `Network[IO].bind` which returns a value of type `Resource[F, ServerSocket[F]]`. The `ServerSocket` type defines an `accept` method of type `Stream[F, Socket[F]]`. This is an infinite stream of client sockets -- each time a client connects to the server, a `Socket[F]` is emitted, allowing interaction with that client. The lifetime of the client socket is managed by the overall stream -- e.g. flat mapping over a socket will keep that socket open until the returned inner stream completes, at which point, the client socket is closed and any underlying resources are returned to the runtime environment. We map over this infinite stream of clients and provide the logic for handling an individual client. In this case, we read from the client socket, UTF-8 decode the received bytes, extract individual lines, and then write each line back to the client. This logic is implemented as a single `Stream[F, Unit]`. @@ -156,7 +158,7 @@ Since we mapped over the infinite client stream, we end up with a `Stream[F, Str In joining all these streams together, be prudent to handle errors in the client streams. -The pattern of `Network[F].server(address).map(handleClient).parJoin(maxConcurrentClients)` is very common when working with server sockets. +The pattern of `Network[F].bind(address).map(ss => handleClient(ss.accept)).parJoin(maxConcurrentClients)` is very common when working with server sockets. A simpler echo server could be implemented with this core logic: @@ -234,7 +236,7 @@ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]( tlsContext: TLSContext[F]): Stream[F, Unit] = { - Stream.resource(Network[F].client(SocketAddress(host"localhost", port"5555"))).flatMap { underlyingSocket => + Stream.resource(Network[F].connect(SocketAddress(host"localhost", port"5555"))).flatMap { underlyingSocket => Stream.resource(tlsContext.client(underlyingSocket)).flatMap { socket => Stream("Hello, world!") .interleave(Stream.constant("\n")) @@ -263,10 +265,10 @@ import fs2.io.net.tls.{TLSParameters, TLSSocket} import cats.effect.Resource import javax.net.ssl.SNIHostName -def tlsClientWithSni[F[_]: MonadCancelThrow: Network]( +def tlsClientWithSni[F[_]: Network]( tlsContext: TLSContext[F], address: SocketAddress[Host]): Resource[F, TLSSocket[F]] = - Network[F].client(address).flatMap { underlyingSocket => + Network[F].connect(address).flatMap { underlyingSocket => tlsContext.clientBuilder( underlyingSocket ).withParameters( @@ -291,7 +293,7 @@ def debug[F[_]: MonadCancelThrow: Network]( tlsContext: TLSContext[F], address: SocketAddress[Host] ): F[String] = - Network[F].client(address).use { underlyingSocket => + Network[F].connect(address).use { underlyingSocket => tlsContext .clientBuilder(underlyingSocket) .withParameters( @@ -316,13 +318,15 @@ The `fs2.io.file` package provides support for working with files. The README ex ```scala mdoc import cats.effect.Concurrent -import fs2.{hash, text} +import fs2.text +import fs2.hashing.{Hashing, HashAlgorithm} import fs2.io.file.{Files, Path} -def writeDigest[F[_]: Files: Concurrent](path: Path): F[Path] = { +def writeDigest[F[_]: Files: Hashing: Concurrent](path: Path): F[Path] = { val target = Path(path.toString + ".sha256") Files[F].readAll(path) - .through(hash.sha256) + .through(Hashing[F].hash(HashAlgorithm.SHA256)) + .flatMap(h => Stream.chunk(h.bytes)) .through(text.hex.encode) .through(text.utf8.encode) .through(Files[F].writeAll(target)) @@ -436,4 +440,4 @@ The `fs2.io.writeOutputStream` method provides a pipe that writes the bytes emit The `fs2.io.readOutputStream` method creates a `Stream[F, Byte]` from a function which writes to an `OutputStream`. [s2n-tls]: https://github.com/aws/s2n-tls -[`node:tls` module]: https://nodejs.org/api/tls.html \ No newline at end of file +[`node:tls` module]: https://nodejs.org/api/tls.html diff --git a/site/timeseries.md b/site/timeseries.md index ffe6cb119e..2ac60c6761 100644 --- a/site/timeseries.md +++ b/site/timeseries.md @@ -25,7 +25,7 @@ Our `withBitrate` combinator requires a `Stream[F, TimeStamped[ByteVector]]` arg ```scala mdoc def withReceivedBitrate[F[_]](input: Stream[F, Byte]): Stream[F, TimeStamped[Either[Long, ByteVector]]] = - input.chunks.map(c => TimeStamped.unsafeNow(c.toByteVector)).through(withBitrate) + input.chunks.map(c => TimeStamped.unsafeMonotonic(c.toByteVector)).through(withBitrate) ``` Each emitted sample is the sum of bits received during each one second period. Let's compute an average of that value over the last 10 seconds. We can do this via `mapAccumulate` along with a `scala.collection.immutable.Queue`: From 47815b2c2f557f25e52e47e3cb1aa68b9e979813 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 19:32:33 -0400 Subject: [PATCH 45/79] Deprecate old UnixSockets --- .../scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala | 4 ++++ .../scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala | 4 ++++ .../scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala | 1 + .../src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index 0801afc99a..a88a093331 100644 --- a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -28,16 +28,20 @@ import cats.effect.{Async, IO, LiftIO} import fs2.io.file.Files private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") def forIO: UnixSockets[IO] = forLiftIO + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { val _ = LiftIO[F] forAsyncAndFiles } + @deprecated("Use Network instead", "3.13.0") def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = forAsyncAndFiles(Files.forAsync(F), F) + @deprecated("Use Network instead", "3.13.0") def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = { val _ = Files[F] new AsyncUnixSockets(UnixSocketsProvider.forAsync) diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index 220008f4da..8771bf5856 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -28,18 +28,22 @@ import cats.effect.{Async, IO, LiftIO} import fs2.io.file.Files private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") def forIO: UnixSockets[IO] = forLiftIO + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = { val _ = LiftIO[F] forAsyncAndFiles } + @deprecated("Use Network instead", "3.13.0") def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = { val _ = Files[F] new AsyncUnixSockets(UnixSocketsProvider.forAsync) } + @deprecated("Use Network instead", "3.13.0") def forAsync[F[_]](implicit F: Async[F]): UnixSockets[F] = forAsyncAndFiles(F, Files.forAsync(F)) } diff --git a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index 8d7768d427..ea75751cff 100644 --- a/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -27,6 +27,7 @@ package unixsocket import cats.effect.{Async, LiftIO} private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type => + @deprecated("Use Network instead", "3.13.0") implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSockets[F] = new AsyncUnixSockets(new FdPollingUnixSocketsProvider[F]) } diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 4aa6dce6f9..7f071583fc 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -29,10 +29,12 @@ import fs2.Stream import fs2.io.net.{Socket, SocketOption, UnixSocketsProvider} /** Capability of working with AF_UNIX sockets. */ +@deprecated("Use client, bind, bindAndAccept on Network[F] instead", "3.13.0") trait UnixSockets[F[_]] { /** Returns a resource which opens a unix socket to the specified path. */ + @deprecated("Use Network[F].connect(address) instead", "3.13.0") def client(address: UnixSocketAddress): Resource[F, Socket[F]] /** Listens to the specified path for connections and emits a `Socket` for each connection. @@ -43,6 +45,7 @@ trait UnixSockets[F[_]] { * * By default, the path is deleted when the server closes. To override this, pass `deleteOnClose = false`. */ + @deprecated("Use Network[F].bind(address) instead", "3.13.0") def server( address: UnixSocketAddress, deleteIfExists: Boolean = false, @@ -51,8 +54,10 @@ trait UnixSockets[F[_]] { } object UnixSockets extends UnixSocketsCompanionPlatform { + @deprecated("Use Network[F] instead", "3.13.0") def apply[F[_]](implicit F: UnixSockets[F]): UnixSockets[F] = F + @deprecated("Use Network[F] instead", "3.13.0") protected class AsyncUnixSockets[F[_]: Async](delegate: UnixSocketsProvider[F]) extends UnixSockets[F] { From 5af859e9cca301df4e44368d2cbd1110d3d176bf Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 21 Apr 2025 20:22:12 -0400 Subject: [PATCH 46/79] Set client socket options on JVM Unix --- .../main/scala/fs2/io/net/SocketOptionPlatform.scala | 2 ++ .../main/scala/fs2/io/net/JdkUnixSocketsProvider.scala | 6 ++++-- .../main/scala/fs2/io/net/JnrUnixSocketsProvider.scala | 10 ++++++---- .../scala/fs2/io/net/UnixSocketsProviderPlatform.scala | 7 +++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index d71951069a..38edc8a905 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -97,4 +97,6 @@ private[net] trait SocketOptionCompanionPlatform { } def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = boolean(UnixServerSocketDeleteOnClose, value) + + // TODO SO_PEERCRED } diff --git a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala index f59baabc81..0c7598844e 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala @@ -23,6 +23,7 @@ package fs2 package io package net +import cats.syntax.all._ import cats.effect.{Async, Resource} import cats.effect.syntax.all._ @@ -46,7 +47,7 @@ private[net] object JdkUnixSocketsProvider { private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { - protected def openChannel(address: UnixSocketAddress) = + protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = evalOnVirtualThreadIfAvailable( Resource .make( @@ -54,7 +55,8 @@ private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) )(ch => F.blocking(ch.close())) .evalTap { ch => F.blocking(ch.connect(UnixDomainSocketAddress.of(address.path))) - .cancelable(F.blocking(ch.close())) + .cancelable(F.blocking(ch.close())) *> + F.delay(options.foreach(o => ch.setOption(o.key, o.value))) } ) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index d620fe7ddc..bf5ec77d45 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -57,10 +57,12 @@ private[net] object JnrUnixSocketsProvider { private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { - protected def openChannel(address: UnixSocketAddress) = - Resource.make(F.blocking(UnixSocketChannel.open(new JnrUnixSocketAddress(address.path))))(ch => - F.blocking(ch.close()) - ) + protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = + Resource + .make(F.blocking(UnixSocketChannel.open(new JnrUnixSocketAddress(address.path))))(ch => + F.blocking(ch.close()) + ) + .evalTap(ch => F.delay(options.foreach(o => ch.setOption(o.key, o.value)))) protected def openServerChannel(address: UnixSocketAddress, options: List[SocketOption]) = Resource diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala index f6ef65072b..cab6794a07 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala @@ -57,7 +57,10 @@ private[net] trait UnixSocketsProviderCompanionPlatform { abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) extends UnixSocketsProvider[F] { - protected def openChannel(address: UnixSocketAddress): Resource[F, SocketChannel] + protected def openChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, SocketChannel] protected def openServerChannel( address: UnixSocketAddress, @@ -65,7 +68,7 @@ private[net] trait UnixSocketsProviderCompanionPlatform { ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = - openChannel(address).evalMap(makeSocket[F](_, UnixSocketAddress(""), address)) + openChannel(address, options).evalMap(makeSocket[F](_, UnixSocketAddress(""), address)) def bind( address: UnixSocketAddress, From e09f42586aeb16d207a431eb719928b66f46bdcf Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 23 Apr 2025 12:45:50 -0400 Subject: [PATCH 47/79] Remove explicit DNS lookups from JS IP socket connect & bind --- .../scala/fs2/io/net/AsyncSocketsProvider.scala | 8 ++++---- .../fs2/io/net/IpSocketsProviderPlatform.scala | 13 +++---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index f9255cf820..7e81484a29 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -27,7 +27,7 @@ import cats.effect.{Async, Resource} import cats.effect.std.Dispatcher import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{IpAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress, UnixSocketAddress} import fs2.concurrent.Channel import fs2.io.internal.facade @@ -39,7 +39,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { options.traverse_(option => option.key.set(socket, option.value)) protected def connectIpOrUnix( - to: Either[SocketAddress[IpAddress], UnixSocketAddress], + to: Either[SocketAddress[Host], UnixSocketAddress], options: List[SocketOption] ): Resource[F, Socket[F]] = (for { @@ -92,7 +92,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } yield socket).adaptError { case IOException(ex) => ex } protected def bindIpOrUnix( - address: Either[SocketAddress[IpAddress], UnixSocketAddress], + address: Either[SocketAddress[Host], UnixSocketAddress], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = (for { @@ -125,7 +125,7 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } <* F.delay { address match { case Left(addr) => - if (addr.host.isWildcard) + if (addr.host.isInstanceOf[IpAddress] && addr.host.asInstanceOf[IpAddress].isWildcard) server.listen(addr.port.value, () => cb(Right(()))) else server.listen(addr.port.value, addr.host.toString, () => cb(Right(()))) diff --git a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala index 4d07adebf1..02ac0c1702 100644 --- a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala @@ -24,30 +24,23 @@ package io package net import cats.effect.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, SocketAddress} +import com.comcast.ip4s.{Host, SocketAddress} private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = - forAsyncAndDns[F](implicitly, Dns.forAsync) - - private def forAsyncAndDns[F[_]: Async: Dns]: IpSocketsProvider[F] = new AsyncSocketsProvider[F] with IpSocketsProvider[F] { override def connect( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] = - Resource.eval(address.host.resolve[F]).flatMap { ip => - connectIpOrUnix(Left(SocketAddress(ip, address.port)), options) - } + connectIpOrUnix(Left(address), options) override def bind( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = - Resource.eval(address.host.resolve[F]).flatMap { ip => - bindIpOrUnix(Left(SocketAddress(ip, address.port)), options) - } + bindIpOrUnix(Left(address), options) } } From c063029229f336cbf5301590b0b86c4dcae07d05 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 23 Apr 2025 13:59:36 -0400 Subject: [PATCH 48/79] Scalafmt --- io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 7e81484a29..000ff52e39 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -125,7 +125,9 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { } <* F.delay { address match { case Left(addr) => - if (addr.host.isInstanceOf[IpAddress] && addr.host.asInstanceOf[IpAddress].isWildcard) + if ( + addr.host.isInstanceOf[IpAddress] && addr.host.asInstanceOf[IpAddress].isWildcard + ) server.listen(addr.port.value, () => cb(Right(()))) else server.listen(addr.port.value, addr.host.toString, () => cb(Right(()))) From 6c7f330b2550d8b0d8967045567be96ad158cb4b Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 5 May 2025 11:18:58 -0400 Subject: [PATCH 49/79] Make SO_REUSEPORT lazy loaded --- .../src/main/scala/fs2/io/net/SocketOptionPlatform.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 38edc8a905..444fec9a53 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -72,7 +72,8 @@ private[net] trait SocketOptionCompanionPlatform { def reuseAddress(value: Boolean): SocketOption = boolean(ReuseAddress, value) - val ReusePort = StandardSocketOptions.SO_REUSEPORT + // Note: this option was added in Java 9 so lazily load it to avoid failure on Java 8 + lazy val ReusePort = StandardSocketOptions.SO_REUSEPORT def reusePort(value: Boolean): SocketOption = boolean(ReusePort, value) @@ -97,6 +98,4 @@ private[net] trait SocketOptionCompanionPlatform { } def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = boolean(UnixServerSocketDeleteOnClose, value) - - // TODO SO_PEERCRED } From 3df2f3d4336d4a8a881e0b9ce0fa619b19f86330 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 08:07:20 -0400 Subject: [PATCH 50/79] Revamped datagram support --- .../test/scala/fs2/io/net/udp/UdpSuite.scala | 101 +++++----- .../scala/fs2/io/internal/facade/dgram.scala | 12 ++ .../scala/fs2/io/internal/facade/os.scala | 1 + .../fs2/io/net/AsyncSocketsProvider.scala | 93 ++++++++- .../fs2/io/net/DatagramSocketOption.scala | 25 ++- .../fs2/io/net/DatagramSocketPlatform.scala | 65 ++++++- .../scala/fs2/io/net/NetworkPlatform.scala | 11 +- .../fs2/io/net/SocketOptionPlatform.scala | 98 ++++++++-- .../scala/fs2/io/net/SocketPlatform.scala | 4 +- .../io/net/UnixSocketsProviderPlatform.scala | 77 -------- .../net/unixsocket/UnixSocketsPlatform.scala | 2 +- .../fs2/io/net/udp/UdpSuitePlatform.scala | 3 +- ...hronousChannelGroupIpSocketsProvider.scala | 12 +- .../fs2/io/net/SocketOptionPlatform.scala | 12 +- .../net/AsyncIpDatagramSocketsProvider.scala | 177 ++++++++++++++++++ ...m.scala => AsyncUnixSocketsProvider.scala} | 119 +++++------- .../net/AsynchronousDatagramSocketGroup.scala | 20 +- ...etectingUnixDatagramSocketsProvider.scala} | 32 ++-- .../AutoDetectingUnixSocketsProvider.scala} | 25 ++- .../io/net/DatagramSocketGroupPlatform.scala | 105 ----------- .../fs2/io/net/DatagramSocketPlatform.scala | 1 + .../fs2/io/net/JdkUnixSocketsProvider.scala | 2 +- .../net/JnrUnixDatagramSocketsProvider.scala | 172 +++++++++++++++++ .../fs2/io/net/JnrUnixSocketsProvider.scala | 7 +- .../scala/fs2/io/net/NetworkPlatform.scala | 77 ++++---- .../io/net/SelectingIpSocketsProvider.scala | 5 +- .../scala/fs2/io/net/tls/DTLSSocket.scala | 50 ++++- .../fs2/io/net/tls/TLSContextPlatform.scala | 2 +- .../net/unixsocket/UnixSocketsPlatform.scala | 2 +- .../scala/fs2/io/net/UnixDatagramSuite.scala | 101 ++++++++++ .../fs2/io/net/UnixSocketsSuitePlatform.scala | 6 +- .../fs2/io/net/tls/DTLSSocketSuite.scala | 12 +- .../io/net/FdPollingIpSocketsProvider.scala | 7 +- .../io/net/FdPollingUnixSocketsProvider.scala | 30 +-- .../scala/fs2/io/net/NetworkPlatform.scala | 9 +- .../src/main/scala/fs2/io/net/Datagram.scala | 10 +- .../scala/fs2/io/net/DatagramSocket.scala | 31 ++- .../fs2/io/net/DatagramSocketGroup.scala | 1 + .../io/net/IpDatagramSocketsProvider.scala} | 12 +- .../scala/fs2/io/net/IpSocketsProvider.scala | 6 +- .../src/main/scala/fs2/io/net/Network.scala | 55 ++++-- .../scala/fs2/io/net/PeerCredentials.scala | 1 + .../main/scala/fs2/io/net/SocketGroup.scala | 31 +-- .../main/scala/fs2/io/net/SocketOption.scala | 40 +++- .../io/net/UnixDatagramSocketsProvider.scala} | 11 +- .../fs2/io/net/UnixSocketsProvider.scala | 6 +- .../fs2/io/net/unixsocket/UnixSockets.scala | 8 +- .../scala/fs2/io/net/UnixSocketsSuite.scala | 8 +- 48 files changed, 1143 insertions(+), 554 deletions(-) delete mode 100644 io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala create mode 100644 io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala rename io/jvm/src/main/scala/fs2/io/net/{UnixSocketsProviderPlatform.scala => AsyncUnixSocketsProvider.scala} (59%) rename io/{js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala => jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala} (59%) rename io/{native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala => jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala} (58%) create mode 100644 io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala create mode 100644 io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala rename io/{native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala => shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala} (81%) create mode 100644 io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala rename io/{jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala => shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala} (82%) diff --git a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala index 19fd4effc2..e40915f0a2 100644 --- a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala +++ b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala @@ -32,24 +32,28 @@ import com.comcast.ip4s._ import scala.concurrent.duration._ class UdpSuite extends Fs2Suite with UdpSuitePlatform { - def sendAndReceive(socket: DatagramSocket[IO], toSend: Datagram): IO[Datagram] = + private def sendAndReceive(socket: DatagramSocket[IO], toSend: Datagram): IO[Datagram] = socket .write(toSend) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceive(socket, toSend))) + private def sendAndReceiveBytes(socket: DatagramSocket[IO], bytes: Chunk[Byte]): IO[Datagram] = + socket + .write(bytes) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceiveBytes(socket, bytes))) + group("udp") { test("echo one") { val msg = Chunk.array("Hello, world!".getBytes) Stream - .resource(Network[IO].openDatagramSocket()) + .resource(Network[IO].bindDatagramSocket(SocketAddress.Wildcard)) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val serverAddress = SocketAddress(ip"127.0.0.1", serverPort) - val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).evalMap { clientSocket => - sendAndReceive(clientSocket, Datagram(serverAddress, msg)) + val serverAddress = SocketAddress(ip"127.0.0.1", serverSocket.address.asIpUnsafe.port) + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = + Stream.resource(Network[IO].bindDatagramSocket(SocketAddress.Wildcard)).evalMap { + clientSocket => + sendAndReceive(clientSocket, Datagram(serverAddress, msg)) } - client.concurrently(server) - } + client.concurrently(server) } .compile .lastOrError @@ -67,22 +71,20 @@ class UdpSuite extends Fs2Suite with UdpSuitePlatform { .sorted Stream - .resource(Network[IO].openDatagramSocket()) + .resource(Network[IO].bindDatagramSocket()) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val serverAddress = SocketAddress(ip"127.0.0.1", serverPort) - val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).flatMap { clientSocket => - Stream - .emits(msgs.map(msg => Datagram(serverAddress, msg))) - .evalMap(msg => sendAndReceive(clientSocket, msg)) - } - val clients = Stream - .constant(client) - .take(numClients.toLong) - .parJoin(numParallelClients) - clients.concurrently(server) + val serverAddress = SocketAddress(ip"127.0.0.1", serverSocket.address.asIpUnsafe.port) + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => + Stream + .emits(msgs.map(msg => Datagram(serverAddress, msg))) + .evalMap(msg => sendAndReceive(clientSocket, msg)) } + val clients = Stream + .constant(client) + .take(numClients.toLong) + .parJoin(numParallelClients) + clients.concurrently(server) } .compile .toVector @@ -90,32 +92,47 @@ class UdpSuite extends Fs2Suite with UdpSuitePlatform { .assertEquals(expected) } - test("multicast".ignore) { - // Fails often based on routing table of host machine - val group = mip"232.10.10.10" + test("echo connected") { + val msg = Chunk.array("Hello, world!".getBytes) + Stream + .resource(Network[IO].bindDatagramSocket()) + .flatMap { serverSocket => + val serverAddress = serverSocket.address.asIpUnsafe + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = Stream.resource(Network[IO].bindDatagramSocket()).evalMap { clientSocket => + clientSocket.connect(serverAddress) >> sendAndReceiveBytes(clientSocket, msg) + } + client.concurrently(server) + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } + + test("multicast") { + val group = mip"239.10.10.10" val groupJoin = MulticastJoin.asm(group) val msg = Chunk.array("Hello, world!".getBytes) Stream .resource( - Network[IO].openDatagramSocket( - options = List(DatagramSocketOption.multicastTtl(1)), - protocolFamily = Some(v4ProtocolFamily) - ) + Network[IO] + .bindDatagramSocket( + options = List(SocketOption.multicastTtl(1)) + ) + .evalMap { serverSocket => + v4Interfaces + .traverse_(interface => serverSocket.join(groupJoin, interface)) + .as(serverSocket) + } ) .flatMap { serverSocket => - Stream.eval(serverSocket.localAddress).map(_.port).flatMap { serverPort => - val server = Stream - .exec( - v4Interfaces.traverse_(interface => serverSocket.join(groupJoin, interface)) - ) ++ - serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].openDatagramSocket()).flatMap { clientSocket => - Stream(Datagram(SocketAddress(group.address, serverPort), msg)) - .through(clientSocket.writes) - .drain ++ Stream.eval(clientSocket.read) - } - client.concurrently(server) + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => + val to = SocketAddress(group.address, serverSocket.address.asIpUnsafe.port) + Stream.eval(clientSocket.write(msg, to) >> clientSocket.read) } + client.concurrently(server) } .compile .lastOrError diff --git a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala index 8b5875b123..8bfb0cd0d7 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala @@ -45,6 +45,11 @@ private[io] object dgram { def bind(options: BindOptions, cb: js.Function0[Unit]): Unit = js.native + def connect(port: Int, address: String, cb: js.Function1[js.UndefOr[js.Error], Unit]): Unit = + js.native + + def disconnect(): Unit = js.native + def addMembership(multicastAddress: String, multicastInterface: String): Unit = js.native def dropMembership(multicastAddress: String, multicastInterface: String): Unit = js.native @@ -63,6 +68,9 @@ private[io] object dgram { def close(cb: js.Function0[Unit]): Unit = js.native + def send(msg: Uint8Array, cb: js.Function1[js.Error, Unit]): Unit = + js.native + def send(msg: Uint8Array, port: Int, address: String, cb: js.Function1[js.Error, Unit]): Unit = js.native @@ -74,8 +82,12 @@ private[io] object dgram { def setMulticastTTL(ttl: Int): Unit = js.native + def getRecvBufferSize: Int = js.native + def setRecvBufferSize(size: Int): Unit = js.native + def getSendBufferSize: Int = js.native + def setSendBufferSize(size: Int): Unit = js.native def setTTL(ttl: Int): Unit = js.native diff --git a/io/js/src/main/scala/fs2/io/internal/facade/os.scala b/io/js/src/main/scala/fs2/io/internal/facade/os.scala index 63c5bd0176..4dd9fb4afa 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/os.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/os.scala @@ -49,6 +49,7 @@ private[io] object os { @js.native trait NetworkInterfaceInfo extends js.Object { def family: String = js.native + def address: String = js.native } } diff --git a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala index 000ff52e39..4dba59153b 100644 --- a/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala +++ b/io/js/src/main/scala/fs2/io/net/AsyncSocketsProvider.scala @@ -27,13 +27,61 @@ import cats.effect.{Async, Resource} import cats.effect.std.Dispatcher import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress, UnixSocketAddress} +import com.comcast.ip4s.{Dns, Host, IpAddress, Port, SocketAddress, UnixSocketAddress} import fs2.concurrent.Channel +import fs2.io.file.Files import fs2.io.internal.facade import scala.scalajs.js -private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { +private[net] final class AsyncSocketsProvider[F[_]](implicit F: Async[F]) + extends IpSocketsProvider[F] + with UnixSocketsProvider[F] + with IpDatagramSocketsProvider[F] + with UnixDatagramSocketsProvider[F] { + + implicit private val dnsInstance: Dns[F] = Dns.forAsync[F] + implicit protected val filesInstance: Files[F] = Files.forAsync[F] + + override def connectIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = + connectIpOrUnix(Left(address), options) + + override def bindIp( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = + bindIpOrUnix(Left(address), options) + + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + connectIpOrUnix(Right(address), options) + + override def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + delete *> bindIpOrUnix(Right(address), filteredOptions) + } + + override def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + bindDatagramSocketIpOrUnix(Left(address), options) + + override def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + delete *> bindDatagramSocketIpOrUnix(Right(address), filteredOptions) + } private def setSocketOptions(options: List[SocketOption])(socket: facade.net.Socket): F[Unit] = options.traverse_(option => option.key.set(socket, option.value)) @@ -178,4 +226,45 @@ private[net] abstract class AsyncSocketsProvider[F[_]](implicit F: Async[F]) { Stream.resource(Socket.forAsync(sock, address, peerAddress)) } } yield ServerSocket(info, sockets)).adaptError { case IOException(ex) => ex } + + protected def bindDatagramSocketIpOrUnix( + address: Either[SocketAddress[Host], UnixSocketAddress], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + address match { + case Right(_) => + Resource.eval( + new UnsupportedOperationException( + "Node.js does not support unix datagram sockets" + ).raiseError + ) + case Left(sa) => + for { + ip <- Resource.eval(sa.host.resolve) + protocolFamily = ip.fold(_ => "udp4", _ => "udp6") + sock <- F + .delay(facade.dgram.createSocket(protocolFamily)) + .toResource + _ <- F + .async_[Unit] { cb => + val errorListener: js.Function1[js.Error, Unit] = { error => + cb(Left(js.JavaScriptException(error))) + } + sock.once[js.Error]("error", errorListener) + val options = new facade.dgram.BindOptions {} + if (!ip.isWildcard) options.address = ip.toString + options.port = sa.port.value + sock.bind( + options, + { () => + sock.removeListener("error", errorListener) + cb(Right(())) + } + ) + } + .toResource + _ <- Resource.eval(options.traverse_(option => option.key.set(sock, option.value))) + socket <- DatagramSocket.forAsync[F](sock) + } yield socket + } } diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index 176ee90c65..9bbfaef4cc 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -40,6 +40,11 @@ sealed trait DatagramSocketOption { object DatagramSocketOption { sealed trait Key[A] { + private[net] def get[F[_]: Sync](sock: facade.dgram.Socket): F[Option[A]] = { + val _ = sock + Sync[F].raiseError(new UnsupportedOperationException("option does not support get")) + } + private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: A): F[Unit] } @@ -49,17 +54,17 @@ object DatagramSocketOption { val value = value0 } - private object Broadcast extends Key[Boolean] { + object Broadcast extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay(sock.setBroadcast(value)) } - private object MulticastInterface extends Key[String] { + object MulticastInterface extends Key[String] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = Sync[F].delay(sock.setMulticastInterface(value)) } - private object MulticastLoopback extends Key[Boolean] { + object MulticastLoop extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setMulticastLoopback(value) @@ -67,7 +72,7 @@ object DatagramSocketOption { } } - private object MulticastTtl extends Key[Int] { + object MulticastTtl extends Key[Int] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay { sock.setMulticastTTL(value) @@ -75,17 +80,21 @@ object DatagramSocketOption { } } - private object ReceiveBufferSize extends Key[Int] { + object ReceiveBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getRecvBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setRecvBufferSize(value)) } - private object SendBufferSize extends Key[Int] { + object SendBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getSendBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setSendBufferSize(value)) } - private object Ttl extends Key[Int] { + object Ttl extends Key[Int] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay { sock.setTTL(value) @@ -95,7 +104,7 @@ object DatagramSocketOption { def broadcast(value: Boolean): DatagramSocketOption = apply(Broadcast, value) def multicastInterface(value: String): DatagramSocketOption = apply(MulticastInterface, value) - def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoopback, value) + def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoop, value) def multicastTtl(value: Int): DatagramSocketOption = apply(MulticastTtl, value) def receiveBufferSize(value: Int): DatagramSocketOption = apply(ReceiveBufferSize, value) def sendBufferSize(value: Int): DatagramSocketOption = apply(SendBufferSize, value) diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index c4ebb702bf..ff38881d99 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -32,6 +32,7 @@ import cats.effect.std.Queue import cats.effect.syntax.all._ import cats.syntax.all._ import com.comcast.ip4s.AnySourceMulticastJoin +import com.comcast.ip4s.GenSocketAddress import com.comcast.ip4s.IpAddress import com.comcast.ip4s.MulticastJoin import com.comcast.ip4s.Port @@ -73,7 +74,7 @@ private[net] trait DatagramSocketCompanionPlatform { _ <- sock.registerListener[F, js.Error]("error", dispatcher) { e => error.complete(js.JavaScriptException(e)).void } - socket <- Resource.make(F.pure(new AsyncDatagramSocket(sock, queue, error)))(_ => + socket <- Resource.make(F.delay(new AsyncDatagramSocket(sock, queue, error)))(_ => F.async_[Unit](cb => sock.close(() => cb(Right(())))) ) } yield socket @@ -86,30 +87,58 @@ private[net] trait DatagramSocketCompanionPlatform { F: Async[F] ) extends DatagramSocket[F] { + override def connect(address: GenSocketAddress) = + F.async_[Unit] { cb => + val addr = address.asIpUnsafe + sock.connect( + addr.port.value, + addr.host.toString, + err => err.toOption.fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) + ) + } + + override def disconnect = + F.delay(sock.disconnect()) + override def read: F[Datagram] = EitherT( queue.take.race(error.get.flatMap(F.raiseError[Datagram])) ).merge + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + override def reads: Stream[F, Datagram] = Stream .fromQueueUnterminated(queue) .concurrently(Stream.eval(error.get.flatMap(F.raiseError[Datagram]))) - override def write(datagram: Datagram): F[Unit] = F.async_ { cb => + override def write(bytes: Chunk[Byte]) = F.async_ { cb => + sock.send( + bytes.toUint8Array, + err => Option(err).fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) + ) + } + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = F.async_ { cb => + val addr = address.asIpUnsafe sock.send( - datagram.bytes.toUint8Array, - datagram.remote.port.value, - datagram.remote.host.toString, + bytes.toUint8Array, + addr.port.value, + addr.host.toString, err => Option(err).fold(cb(Right(())))(err => cb(Left(js.JavaScriptException(err)))) ) } + override def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + override def writes: Pipe[F, Datagram, Nothing] = _.foreach(write) + override val address: GenSocketAddress = { + val info = sock.address() + SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port.toInt).get) + } + override def localAddress: F[SocketAddress[IpAddress]] = - F.delay { - val info = sock.address() - SocketAddress(IpAddress.fromString(info.address).get, Port.fromInt(info.port.toInt).get) - } + F.pure(address.asIpUnsafe) override def join( join: MulticastJoin[IpAddress], @@ -135,5 +164,23 @@ private[net] trait DatagramSocketCompanionPlatform { } }) + override def getOption[A](key: SocketOption.Key[A]) = + key.get(sock) + + override def setOption[A](key: SocketOption.Key[A], value: A) = + key.set(sock, value) + + override def supportedOptions = + F.pure( + Set( + SocketOption.MulticastInterface, + SocketOption.MulticastLoop, + SocketOption.MulticastTtl, + SocketOption.Broadcast, + SocketOption.ReceiveBufferSize, + SocketOption.SendBufferSize, + SocketOption.Ttl + ) + ) } } diff --git a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala index edcccb2ffd..7966552a78 100644 --- a/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -34,10 +34,13 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N forAsync } - def forAsync[F[_]](implicit F: Async[F]): Network[F] = + def forAsync[F[_]](implicit F: Async[F]): Network[F] = { + val omni = new AsyncSocketsProvider[F] new AsyncProviderBasedNetwork[F] { - protected def mkIpSocketsProvider = IpSocketsProvider.forAsync[F] - protected def mkUnixSocketsProvider = UnixSocketsProvider.forAsync[F] - protected def mkDatagramSocketGroup = DatagramSocketGroup.forAsync[F] + protected def mkIpSocketsProvider = omni + protected def mkUnixSocketsProvider = omni + protected def mkIpDatagramSocketsProvider = omni + protected def mkUnixDatagramSocketsProvider = omni } + } } diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 12224c4200..c187344c7c 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -24,12 +24,26 @@ package fs2.io.net import cats.effect.kernel.Sync import fs2.io.internal.facade +import scala.annotation.nowarn import scala.concurrent.duration._ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sealed trait Key[A] { - private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: A): F[Unit] - private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[A]] + @nowarn + private[net] def set[F[_]: Sync](sock: facade.net.Socket, value: A): F[Unit] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support TCP")) + + @nowarn + private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[A]] = + unsupportedGet + + @nowarn + private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: A): F[Unit] = + Sync[F].raiseError(new UnsupportedOperationException("option does not support UDP")) + + @nowarn + private[net] def get[F[_]: Sync](sock: facade.dgram.Socket): F[Option[A]] = + unsupportedGet } private def unsupportedGet[F[_]: Sync, A]: F[A] = @@ -41,8 +55,6 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setEncoding(value) () } - override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[String]] = - unsupportedGet } def encoding(value: String): SocketOption = apply(Encoding, value) @@ -52,8 +64,6 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setKeepAlive(value) () } - override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - unsupportedGet } def keepAlive(value: Boolean): SocketOption = apply(KeepAlive, value) @@ -63,9 +73,6 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => sock.setNoDelay(value) () } - - override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - unsupportedGet } def noDelay(value: Boolean): SocketOption = apply(NoDelay, value) @@ -85,25 +92,78 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => } def timeout(value: FiniteDuration): SocketOption = apply(Timeout, value) - object UnixServerSocketDeleteIfExists extends Key[Boolean] { + object UnixSocketDeleteIfExists extends Key[Boolean] { override private[net] def set[F[_]: Sync]( sock: facade.net.Socket, value: Boolean ): F[Unit] = Sync[F].unit - override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - unsupportedGet } - def unixServerSocketDeleteIfExists(value: Boolean): SocketOption = - apply(UnixServerSocketDeleteIfExists, value) + def unixSocketDeleteIfExists(value: Boolean): SocketOption = + apply(UnixSocketDeleteIfExists, value) - object UnixServerSocketDeleteOnClose extends Key[Boolean] { + object UnixSocketDeleteOnClose extends Key[Boolean] { override private[net] def set[F[_]: Sync]( sock: facade.net.Socket, value: Boolean ): F[Unit] = Sync[F].unit - override private[net] def get[F[_]: Sync](sock: facade.net.Socket): F[Option[Boolean]] = - unsupportedGet } - def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = - apply(UnixServerSocketDeleteOnClose, value) + def unixSocketDeleteOnClose(value: Boolean): SocketOption = + apply(UnixSocketDeleteOnClose, value) + + // Datagram options + + object Broadcast extends Key[Boolean] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = + Sync[F].delay(sock.setBroadcast(value)) + } + def broadcast(value: Boolean): SocketOption = apply(Broadcast, value) + + object MulticastInterface extends Key[String] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = + Sync[F].delay(sock.setMulticastInterface(value)) + } + def multicastInterface(value: String): SocketOption = apply(MulticastInterface, value) + + object MulticastLoop extends Key[Boolean] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = + Sync[F].delay { + sock.setMulticastLoopback(value) + () + } + } + def multicastLoop(value: Boolean): SocketOption = apply(MulticastLoop, value) + + object MulticastTtl extends Key[Int] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay { + sock.setMulticastTTL(value) + () + } + } + def multicastTtl(value: Int): SocketOption = apply(MulticastTtl, value) + + object ReceiveBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getRecvBufferSize)) + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay(sock.setRecvBufferSize(value)) + } + def receiveBufferSize(value: Int): SocketOption = apply(ReceiveBufferSize, value) + + object SendBufferSize extends Key[Int] { + override private[net] def get[F[_]: Sync](sock: facade.dgram.Socket) = + Sync[F].delay(Some(sock.getSendBufferSize)) + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay(sock.setSendBufferSize(value)) + } + def sendBufferSize(value: Int): SocketOption = apply(SendBufferSize, value) + + object Ttl extends Key[Int] { + override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = + Sync[F].delay { + sock.setTTL(value) + () + } + } + def ttl(value: Int): SocketOption = apply(Ttl, value) } diff --git a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala index 2241983888..52d7424d18 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketPlatform.scala @@ -98,8 +98,8 @@ private[net] trait SocketCompanionPlatform { SocketOption.KeepAlive, SocketOption.NoDelay, SocketOption.Timeout, - SocketOption.UnixServerSocketDeleteIfExists, - SocketOption.UnixServerSocketDeleteOnClose + SocketOption.UnixSocketDeleteIfExists, + SocketOption.UnixSocketDeleteOnClose ) ) diff --git a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala deleted file mode 100644 index 7a2d345a03..0000000000 --- a/io/js/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net - -import cats.effect.{Async, LiftIO, Resource} -import cats.syntax.all._ -import com.comcast.ip4s.UnixSocketAddress -import fs2.io.file.{Files, Path} - -private[net] trait UnixSocketsProviderCompanionPlatform { - - private[net] def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { - val _ = LiftIO[F] - forAsync[F] - } - - private[net] def forAsync[F[_]: Async]: UnixSocketsProvider[F] = - forAsyncAndFiles(implicitly, Files.forAsync[F]) - - private def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = - new AsyncSocketsProvider[F] with UnixSocketsProvider[F] { - - override def connect( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, Socket[F]] = - connectIpOrUnix(Right(address), options) - - override def bind( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = { - - var deleteIfExists: Boolean = false - var deleteOnClose: Boolean = true - - val filteredOptions = options.filter { opt => - if (opt.key == SocketOption.UnixServerSocketDeleteIfExists) { - deleteIfExists = opt.value.asInstanceOf[Boolean] - false - } else if (opt.key == SocketOption.UnixServerSocketDeleteOnClose) { - deleteOnClose = opt.value.asInstanceOf[Boolean] - false - } else true - } - - val delete = Resource.make( - if (deleteIfExists) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit - )(_ => - if (deleteOnClose) Files[F].deleteIfExists(Path(address.path)).void else Async[F].unit - ) - - delete *> bindIpOrUnix(Right(address), filteredOptions) - } - } -} diff --git a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index a88a093331..2dfad89616 100644 --- a/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -44,6 +44,6 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type @deprecated("Use Network instead", "3.13.0") def forAsyncAndFiles[F[_]: Files](implicit F: Async[F]): UnixSockets[F] = { val _ = Files[F] - new AsyncUnixSockets(UnixSocketsProvider.forAsync) + new AsyncUnixSockets(new AsyncSocketsProvider) } } diff --git a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala index 0d92a8ea8e..bc2cde011e 100644 --- a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala @@ -32,7 +32,8 @@ trait UdpSuitePlatform extends Fs2Suite { .networkInterfaces() .toMap .collect { - case (k, v) if v.exists(_.family == "IPv4") => k + case (_, addresses) if addresses.exists(_.family == "IPv4") => + addresses.collectFirst { case a if a.family == "IPv4" => a.address }.get } .toList diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index 53a8bade05..c9126d14d6 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -41,7 +41,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( )(implicit F: Async[F], F2: Dns[F]) extends IpSocketsProvider[F] { - override def connect( + override def connectIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] = { @@ -76,7 +76,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( setup.evalMap(ch => connect(ch) *> Socket.forAsync(ch)) } - override def bind( + override def bindIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = { @@ -146,13 +146,13 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( setup.map(sch => ServerSocket(SocketInfo.forAsync(sch), acceptIncoming(sch))) } + } private[net] object AsynchronousChannelGroupIpSocketsProvider { - def forAsyncAndDns[F[_]: Async: Dns]: AsynchronousChannelGroupIpSocketsProvider[F] = + def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = { + implicit val dnsInstance = Dns.forAsync[F] new AsynchronousChannelGroupIpSocketsProvider[F](null) - - def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = - forAsyncAndDns(Async[F], Dns.forAsync[F]) + } } diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index 444fec9a53..bb5379c428 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -85,17 +85,17 @@ private[net] trait SocketOptionCompanionPlatform { def noDelay(value: Boolean): SocketOption = boolean(NoDelay, value) - val UnixServerSocketDeleteIfExists: Key[JBoolean] = new Key[JBoolean] { + val UnixSocketDeleteIfExists: Key[JBoolean] = new Key[JBoolean] { def name() = "FS2_UNIX_DELETE_IF_EXISTS" def `type`() = classOf[JBoolean] } - def unixServerSocketDeleteIfExists(value: JBoolean): SocketOption = - boolean(UnixServerSocketDeleteIfExists, value) + def unixSocketDeleteIfExists(value: JBoolean): SocketOption = + boolean(UnixSocketDeleteIfExists, value) - val UnixServerSocketDeleteOnClose: Key[JBoolean] = new Key[JBoolean] { + val UnixSocketDeleteOnClose: Key[JBoolean] = new Key[JBoolean] { def name() = "FS2_UNIX_DELETE_ON_CLOSE" def `type`() = classOf[JBoolean] } - def unixServerSocketDeleteOnClose(value: Boolean): SocketOption = - boolean(UnixServerSocketDeleteOnClose, value) + def unixSocketDeleteOnClose(value: Boolean): SocketOption = + boolean(UnixSocketDeleteOnClose, value) } diff --git a/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala new file mode 100644 index 0000000000..e98fb238f6 --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import java.net.InetSocketAddress + +import cats.syntax.all._ +import cats.effect.{Async, Resource} +import com.comcast.ip4s.{Dns, Host, SocketAddress} + +import fs2.internal.ThreadFactories +import java.net.StandardProtocolFamily +import java.nio.channels.DatagramChannel +import com.comcast.ip4s.* +import java.net.NetworkInterface +import CollectionCompat.* + +private[net] object AsyncIpDatagramSocketsProvider { + + private[net] def forAsync[F[_]: Async]: IpDatagramSocketsProvider[F] = + new IpDatagramSocketsProvider[F] { + + private implicit val dnsInstance: Dns[F] = Dns.forAsync[F] + + private lazy val globalAdsg = + AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) + + override def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = + Resource.eval(address.resolve[F]).flatMap { addr => + val mkChannel = Async[F].delay { + val pf = + addr.host.fold(_ => StandardProtocolFamily.INET, _ => StandardProtocolFamily.INET6) + val channel = DatagramChannel.open(pf) + options.foreach(o => channel.setOption[o.Value](o.key, o.value)) + channel.bind( + new InetSocketAddress( + if (addr.host.isWildcard) null else addr.host.toInetAddress, + addr.port.value + ) + ) + channel + } + Resource + .make(mkChannel)(c => Async[F].delay(c.close())) + .flatMap(mkDatagramSocket(_, globalAdsg)) + } + + private def mkDatagramSocket( + channel: DatagramChannel, + adsg: AsynchronousDatagramSocketGroup + ): Resource[F, DatagramSocket[F]] = + Resource(Async[F].delay { + val ctx0 = adsg.register(channel) + + val socket = new DatagramSocket[F] { + private val ctx = ctx0 + + override val address: GenSocketAddress = + SocketAddress.fromInetSocketAddress( + channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress] + ) + + override def localAddress: F[SocketAddress[IpAddress]] = + Async[F].pure(address.asIpUnsafe) + + override def connect(address: GenSocketAddress) = + Async[F].blocking(channel.connect(address.asIpUnsafe.toInetSocketAddress)).void + + override def disconnect = + Async[F].blocking(channel.disconnect()).void + + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + + override def read: F[Datagram] = + Async[F].async[Datagram] { cb => + Async[F].delay { + val cancel = adsg.read(ctx, result => cb(result)) + Some(Async[F].delay(cancel())) + } + } + + override def reads: Stream[F, Datagram] = + Stream.repeatEval(read) + + override def write(bytes: Chunk[Byte]) = + write(bytes, None) + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = + write(bytes, Some(address.asIpUnsafe.toInetSocketAddress)) + + private def write(bytes: Chunk[Byte], address: Option[InetSocketAddress]) = + Async[F].async[Unit] { cb => + Async[F].delay { + val cancel = adsg.write(ctx, bytes, address, t => cb(t.toLeft(()))) + Some(Async[F].delay(cancel())) + } + } + + override def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + override def writes: Pipe[F, Datagram, Nothing] = + _.foreach(write) + + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] = + Async[F].delay { + val membership = join.fold( + j => channel.join(j.group.address.toInetAddress, interface), + j => + channel.join(j.group.address.toInetAddress, interface, j.source.toInetAddress) + ) + new GroupMembership { + def drop = Async[F].delay(membership.drop) + def block(source: IpAddress) = + Async[F].delay { membership.block(source.toInetAddress); () } + def unblock(source: IpAddress) = + Async[F].delay { membership.unblock(source.toInetAddress); () } + override def toString = "GroupMembership" + } + } + + override def supportedOptions: F[Set[SocketOption.Key[?]]] = + Async[F].delay { + channel.supportedOptions.asScala.toSet + } + + override def getOption[A](key: SocketOption.Key[A]): F[Option[A]] = + Async[F].delay { + try + Some(channel.getOption(key)) + catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] = + Async[F].delay { + channel.setOption(key, value) + () + } + + override def toString = + s"DatagramSocket($address)" + } + + (socket, Async[F].delay(adsg.close(ctx0))) + }) + + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala similarity index 59% rename from io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala rename to io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala index cab6794a07..073bc7fa2d 100644 --- a/io/jvm/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala @@ -23,92 +23,63 @@ package fs2 package io package net -import cats.effect.{Async, IO, LiftIO, Resource} +import cats.effect.{Async, Resource} import cats.effect.std.Mutex import cats.effect.syntax.all._ import cats.syntax.all._ import com.comcast.ip4s.{IpAddress, SocketAddress, UnixSocketAddress} -import fs2.io.file.{Files, FileHandle, Path, SyncFileHandle} +import fs2.io.file.{Files, FileHandle, SyncFileHandle} import java.nio.ByteBuffer import java.nio.channels.SocketChannel -private[net] trait UnixSocketsProviderCompanionPlatform { - def forIO: UnixSocketsProvider[IO] = forLiftIO - - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { - val _ = LiftIO[F] - forAsyncAndFiles - } - - def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = - if (JdkUnixSocketsProvider.supported) JdkUnixSocketsProvider.forAsyncAndFiles - else if (JnrUnixSocketsProvider.supported) JnrUnixSocketsProvider.forAsyncAndFiles - else - throw new UnsupportedOperationException( - """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" - ) - - def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = - forAsyncAndFiles(F, Files.forAsync(F)) - - abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) - extends UnixSocketsProvider[F] { - - protected def openChannel( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, SocketChannel] - - protected def openServerChannel( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] - - def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = - openChannel(address, options).evalMap(makeSocket[F](_, UnixSocketAddress(""), address)) - - def bind( - address: UnixSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = { - var deleteIfExists: Boolean = false - var deleteOnClose: Boolean = true - - val filteredOptions = options.filter { opt => - opt.key match { - case SocketOption.UnixServerSocketDeleteIfExists => - deleteIfExists = opt.value.asInstanceOf[java.lang.Boolean] - false - case SocketOption.UnixServerSocketDeleteOnClose => - deleteOnClose = opt.value.asInstanceOf[java.lang.Boolean] - false - case _ => true - } - } - - val delete = Resource.make { - Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) - } { _ => - Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) - } - - (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => - val acceptIncoming = - Stream - .resource(accept.attempt) - .flatMap { - case Left(_) => Stream.empty[F] - case Right(accepted) => - Stream.eval(makeSocket(accepted, address, UnixSocketAddress(""))) - } - .repeat - ServerSocket(info, acceptIncoming) - } +private[net] abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) + extends UnixSocketsProvider[F] { + + protected def openChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, SocketChannel] + + protected def openServerChannel( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, (SocketInfo[F], Resource[F, SocketChannel])] + + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = + openChannel(address, options).evalMap( + AsyncUnixSocketsProvider.makeSocket[F](_, UnixSocketAddress(""), address) + ) + + override def bindUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, ServerSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + + (delete *> openServerChannel(address, filteredOptions)).map { case (info, accept) => + val acceptIncoming = + Stream + .resource(accept.attempt) + .flatMap { + case Left(_) => Stream.empty[F] + case Right(accepted) => + Stream.eval( + AsyncUnixSocketsProvider.makeSocket(accepted, address, UnixSocketAddress("")) + ) + } + .repeat + ServerSocket(info, acceptIncoming) } } +} + +private[net] object AsyncUnixSocketsProvider { private def makeSocket[F[_]: Async]( ch: SocketChannel, diff --git a/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala b/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala index 8ccfa506d1..18c3d73e3c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala @@ -48,7 +48,8 @@ private[net] trait AsynchronousDatagramSocketGroup { ): () => Unit def write( ctx: Context, - datagram: Datagram, + bytes: Chunk[Byte], + address: Option[InetSocketAddress], cb: Option[Throwable] => Unit ): () => Unit def close(ctx: Context): Unit @@ -60,7 +61,7 @@ private[net] object AsynchronousDatagramSocketGroup { * Used to avoid copying between Chunk[Byte] and ByteBuffer during writes within the selector thread, * as it can be expensive depending on particular implementation of Chunk. */ - private class WriterDatagram(val remote: InetSocketAddress, val bytes: ByteBuffer) + private class WriterDatagram(val remote: Option[InetSocketAddress], val bytes: ByteBuffer) def unsafe(threadFactory: ThreadFactory): AsynchronousDatagramSocketGroup = new AsynchronousDatagramSocketGroup { @@ -218,13 +219,14 @@ private[net] object AsynchronousDatagramSocketGroup { override def write( key: SelectionKey, - datagram: Datagram, + bytes: Chunk[Byte], + remote: Option[InetSocketAddress], cb: Option[Throwable] => Unit ): () => Unit = { val writerId = ids.getAndIncrement() val writerDatagram = { - val bytes = { - val srcBytes = datagram.bytes.toArraySlice + val bytes0 = { + val srcBytes = bytes.toArraySlice if (srcBytes.size == srcBytes.values.size) srcBytes.values else { val destBytes = new Array[Byte](srcBytes.size) @@ -232,7 +234,7 @@ private[net] object AsynchronousDatagramSocketGroup { destBytes } } - new WriterDatagram(datagram.remote.toInetSocketAddress, ByteBuffer.wrap(bytes)) + new WriterDatagram(remote, ByteBuffer.wrap(bytes0)) } val attachment = key.attachment.asInstanceOf[Attachment] onSelectorThread { @@ -259,7 +261,11 @@ private[net] object AsynchronousDatagramSocketGroup { cb: Option[Throwable] => Unit ): Boolean = try { - val sent = channel.send(datagram.bytes, datagram.remote) + val remote = datagram.remote + val sent = remote match { + case Some(addr) => channel.send(datagram.bytes, addr) + case None => channel.write(datagram.bytes) + } if (sent > 0) { cb(None) true diff --git a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala similarity index 59% rename from io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala rename to io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala index 02ac0c1702..db7e0d4df7 100644 --- a/io/js/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixDatagramSocketsProvider.scala @@ -23,24 +23,24 @@ package fs2 package io package net -import cats.effect.{Async, Resource} -import com.comcast.ip4s.{Host, SocketAddress} +import cats.effect.{Async, IO, LiftIO} +import fs2.io.file.Files -private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => +private[net] object AutoDetectingUnixDatagramSocketsProvider { + def forIO: UnixDatagramSocketsProvider[IO] = forLiftIO - private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = - new AsyncSocketsProvider[F] with IpSocketsProvider[F] { + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixDatagramSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } - override def connect( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, Socket[F]] = - connectIpOrUnix(Left(address), options) + def forAsyncAndFiles[F[_]: Async: Files]: UnixDatagramSocketsProvider[F] = + if (JnrUnixSocketsProvider.supported) JnrUnixDatagramSocketsProvider.forAsyncAndFiles + else + throw new UnsupportedOperationException( + """Unix datagram sockets only supported by having "com.github.jnr" % "jnr-unixsocket" % on the classpath""" + ) - override def bind( - address: SocketAddress[Host], - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] = - bindIpOrUnix(Left(address), options) - } + def forAsync[F[_]](implicit F: Async[F]): UnixDatagramSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync(F)) } diff --git a/io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala similarity index 58% rename from io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala rename to io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala index e4334d35cc..b69c91e6c9 100644 --- a/io/native/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AutoDetectingUnixSocketsProvider.scala @@ -23,11 +23,26 @@ package fs2 package io package net -import cats.effect.{Async, LiftIO} -import com.comcast.ip4s.Dns +import cats.effect.{Async, IO, LiftIO} -private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => +import fs2.io.file.Files - private[net] def forLiftIO[F[_]: Async: LiftIO]: IpSocketsProvider[F] = - new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) +private[net] object AutoDetectingUnixSocketsProvider { + def forIO: UnixSocketsProvider[IO] = forLiftIO + + implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = { + val _ = LiftIO[F] + forAsyncAndFiles + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + if (JdkUnixSocketsProvider.supported) JdkUnixSocketsProvider.forAsyncAndFiles + else if (JnrUnixSocketsProvider.supported) JnrUnixSocketsProvider.forAsyncAndFiles + else + throw new UnsupportedOperationException( + """Must either run on JDK 16+ or have "com.github.jnr" % "jnr-unixsocket" % on the classpath""" + ) + + def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync(F)) } diff --git a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala index 987f29a519..71ed8d5291 100644 --- a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketGroupPlatform.scala @@ -23,111 +23,6 @@ package fs2 package io package net -import java.net.{InetSocketAddress, NetworkInterface} -import java.nio.channels.DatagramChannel - -import cats.effect.kernel.{Async, Resource} -import cats.syntax.all._ - -import com.comcast.ip4s._ - private[net] trait DatagramSocketGroupCompanionPlatform { type ProtocolFamily = java.net.ProtocolFamily - - def unsafe[F[_]: Async: Dns](adsg: AsynchronousDatagramSocketGroup): DatagramSocketGroup[F] = - new AsyncDatagramSocketGroup(adsg) - - private final class AsyncDatagramSocketGroup[F[_]: Async: Dns]( - adsg: AsynchronousDatagramSocketGroup - ) extends DatagramSocketGroup[F] { - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] - ): Resource[F, DatagramSocket[F]] = - Resource.eval(address.traverse(_.resolve[F])).flatMap { addr => - val mkChannel = Async[F].delay { - val channel = protocolFamily - .map(pf => DatagramChannel.open(pf)) - .getOrElse(DatagramChannel.open()) - options.foreach(o => channel.setOption[o.Value](o.key, o.value)) - channel.bind( - new InetSocketAddress(addr.map(_.toInetAddress).orNull, port.map(_.value).getOrElse(0)) - ) - channel - } - Resource.make(mkChannel)(c => Async[F].delay(c.close())).flatMap(mkSocket) - } - - private def mkSocket( - channel: DatagramChannel - ): Resource[F, DatagramSocket[F]] = - Resource(Async[F].delay { - val ctx0 = adsg.register(channel) - - val socket = new DatagramSocket[F] { - private val ctx = ctx0 - - def localAddress: F[SocketAddress[IpAddress]] = - Async[F].delay { - val addr = - Option(channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]) - .getOrElse(throw new ClosedChannelException) - SocketAddress.fromInetSocketAddress(addr) - } - - def read: F[Datagram] = - Async[F].async[Datagram] { cb => - Async[F].delay { - val cancel = adsg.read(ctx, result => cb(result)) - Some(Async[F].delay(cancel())) - } - } - - def reads: Stream[F, Datagram] = - Stream.repeatEval(read) - - def write(datagram: Datagram): F[Unit] = - Async[F].async[Unit] { cb => - Async[F].delay { - val cancel = adsg.write(ctx, datagram, t => cb(t.toLeft(()))) - Some(Async[F].delay(cancel())) - } - } - - def writes: Pipe[F, Datagram, Nothing] = - _.foreach(write) - - def close: F[Unit] = Async[F].delay(adsg.close(ctx)) - - def join( - join: MulticastJoin[IpAddress], - interface: NetworkInterface - ): F[GroupMembership] = - Async[F].delay { - val membership = join.fold( - j => channel.join(j.group.address.toInetAddress, interface), - j => channel.join(j.group.address.toInetAddress, interface, j.source.toInetAddress) - ) - new GroupMembership { - def drop = Async[F].delay(membership.drop) - def block(source: IpAddress) = - Async[F].delay { membership.block(source.toInetAddress); () } - def unblock(source: IpAddress) = - Async[F].delay { membership.unblock(source.toInetAddress); () } - override def toString = "GroupMembership" - } - } - - override def toString = - s"Socket(${Option( - channel.socket.getLocalSocketAddress - ).getOrElse("")})" - } - - (socket, Async[F].delay(adsg.close(ctx0))) - }) - - } } diff --git a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index a25fc1ea0b..6d8d9b810f 100644 --- a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -37,5 +37,6 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { + // TODO deprecate and replace with real cross-platform type type NetworkInterface = java.net.NetworkInterface } diff --git a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala index 0c7598844e..53f91fe47c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JdkUnixSocketsProvider.scala @@ -46,7 +46,7 @@ private[net] object JdkUnixSocketsProvider { } private[net] class JdkUnixSocketsProvider[F[_]: Files](implicit F: Async[F]) - extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { + extends AsyncUnixSocketsProvider[F] { protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = evalOnVirtualThreadIfAvailable( Resource diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala new file mode 100644 index 0000000000..3b0b7c5d6e --- /dev/null +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.{Async, Resource} +import cats.effect.std.Mutex +import cats.effect.syntax.all._ +import cats.syntax.all._ + +import com.comcast.ip4s.{GenSocketAddress, IpAddress, MulticastJoin, UnixSocketAddress} + +import fs2.io.file.Files + +import java.nio.{Buffer, ByteBuffer} +import jnr.unixsocket.{UnixDatagramChannel, UnixSocketAddress => JnrUnixSocketAddress} + +private[net] object JnrUnixDatagramSocketsProvider { + + lazy val supported: Boolean = + try { + Class.forName("jnr.unixsocket.UnixSocketChannel") + true + } catch { + case _: ClassNotFoundException => false + } + + def forAsyncAndFiles[F[_]: Async: Files]: UnixDatagramSocketsProvider[F] = + new JnrUnixDatagramSocketsProvider[F] + + def forAsync[F[_]](implicit F: Async[F]): UnixDatagramSocketsProvider[F] = + forAsyncAndFiles(F, Files.forAsync[F]) +} + +private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) + extends UnixDatagramSocketsProvider[F] { + + override def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] = { + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + + delete *> Resource + .make(F.blocking(UnixDatagramChannel.open()))(ch => F.blocking(ch.close())) + .evalTap { ch => + F.blocking(ch.bind(new JnrUnixSocketAddress(address.path))) + .cancelable(F.blocking(ch.close())) + } + .evalMap { ch => + Mutex[F].map(ch -> _) + } + .map { case (ch, readMutex) => + val address0 = address + new DatagramSocket[F] { + private val readBuffer = ByteBuffer.allocate(65535) + + override val address = address0 + override def localAddress = F.delay(address.asIpUnsafe) + + override def supportedOptions = ??? + override def getOption[A](key: SocketOption.Key[A]) = ??? + override def setOption[A](key: SocketOption.Key[A], value: A) = ??? + + private def withUnixSocketAddress[A]( + address: GenSocketAddress + )(f: UnixSocketAddress => A): A = + address match { + case u: UnixSocketAddress => f(u) + case _ => + throw new IllegalArgumentException( + s"Unsupported address type $address; must pass a UnixSocketAddress" + ) + } + + private def blockingAndCloseIfCanceled[A](a: => A): F[A] = + F.blocking(a).cancelable(F.blocking(ch.close())) + + override def connect(address: GenSocketAddress) = + blockingAndCloseIfCanceled { + withUnixSocketAddress(address) { u => + ch.connect(new JnrUnixSocketAddress(u.path)) + () + } + } + + override def disconnect = + blockingAndCloseIfCanceled { + ch.disconnect() + () + } + + override def read = readGen.map(_.toDatagram) + + override def readGen = readMutex.lock.surround { + blockingAndCloseIfCanceled { + val clientAddress = ch.receive(readBuffer) + val read = readBuffer.position() + val result = + if (read == 0) Chunk.empty + else { + val dest = new Array[Byte](read) + (readBuffer: Buffer).flip() + readBuffer.get(dest) + Chunk.array(dest) + } + (readBuffer: Buffer).clear() + GenDatagram(UnixSocketAddress(clientAddress.path), result) + } + } + + override def reads = Stream.repeatEval(read) + + override def write(bytes: Chunk[Byte]) = + blockingAndCloseIfCanceled { + ch.send( + bytes.toByteBuffer, + ch.getRemoteSocketAddress + ) // note: shouldn't be necessary to pass remote socket address but jnr throws an error otherwise + () + } + + override def write(bytes: Chunk[Byte], address: GenSocketAddress) = + blockingAndCloseIfCanceled { + withUnixSocketAddress(address) { u => + val buffer = bytes.toByteBuffer + val target = new JnrUnixSocketAddress(u.path) + ch.send(buffer, target) + () + } + } + + override def write(datagram: Datagram) = + F.raiseError( + new UnsupportedOperationException( + "Unix datagram socket does not support variant of write that takes a Datagram. Use variant that takes a GenSocketAddress or use connect & write(bytes)" + ) + ) + + override def writes = _.foreach(write) + + override def join( + join: MulticastJoin[IpAddress], + interface: DatagramSocket.NetworkInterface + ) = + F.raiseError( + new UnsupportedOperationException("Multicast not supported on unix datagram sockets") + ) + } + } + } +} diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala index bf5ec77d45..9c220322f3 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixSocketsProvider.scala @@ -47,15 +47,12 @@ private[net] object JnrUnixSocketsProvider { case _: ClassNotFoundException => false } - def forAsyncAndFiles[F[_]: Async: Files]: UnixSocketsProvider[F] = + def forAsyncAndFiles[F[_]](implicit F: Async[F], F2: Files[F]): UnixSocketsProvider[F] = new JnrUnixSocketsProvider[F] - - def forAsync[F[_]](implicit F: Async[F]): UnixSocketsProvider[F] = - forAsyncAndFiles(F, Files.forAsync[F]) } private[net] class JnrUnixSocketsProvider[F[_]](implicit F: Async[F], F2: Files[F]) - extends UnixSocketsProvider.AsyncUnixSocketsProvider[F] { + extends AsyncUnixSocketsProvider[F] { protected def openChannel(address: UnixSocketAddress, options: List[SocketOption]) = Resource diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 6d02702bf0..03241c6407 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -28,11 +28,10 @@ import cats.effect.LiftIO import cats.effect.Selector import cats.effect.kernel.{Async, Resource} -import com.comcast.ip4s.{Dns, GenSocketAddress, Host, Port} +import com.comcast.ip4s.{Dns, GenSocketAddress} import fs2.internal.ThreadFactories -import java.net.ProtocolFamily import java.util.concurrent.ThreadFactory private[net] trait NetworkPlatform[F[_]] { @@ -65,6 +64,10 @@ private[net] trait NetworkPlatform[F[_]] { * * @param threadFactory factory used to create selector thread */ + @deprecated( + "3.13.0", + "Explicitly managed socket groups are no longer supported; use bindDatagramSocket operation on Network instead" + ) def datagramSocketGroup( threadFactory: ThreadFactory = ThreadFactories.named("fs2-udp", true) ): Resource[F, DatagramSocketGroup[F]] @@ -72,8 +75,6 @@ private[net] trait NetworkPlatform[F[_]] { } private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: Network.type => - private lazy val globalAdsg = - AsynchronousDatagramSocketGroup.unsafe(ThreadFactories.named("fs2-global-udp", true)) implicit def forLiftIO[F[_]: Async: LiftIO]: Network[F] = new AsyncNetwork[F] { @@ -93,70 +94,66 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N case None => orElse } - def connect( + override def connect( address: GenSocketAddress, options: List[SocketOption] ): Resource[F, Socket[F]] = matchAddress( address, - sa => selecting(_.connect(sa, options), fallback.connect(sa, options)), + sa => selecting(_.connectIp(sa, options), fallback.connect(sa, options)), ua => fallback.connect(ua, options) ) - def bind( + override def bind( address: GenSocketAddress, options: List[SocketOption] ): Resource[F, ServerSocket[F]] = matchAddress( address, - sa => selecting(_.bind(sa, options), fallback.bind(sa, options)), + sa => selecting(_.bindIp(sa, options), fallback.bind(sa, options)), ua => fallback.bind(ua, options) ) - def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - fallback.datagramSocketGroup(threadFactory) - - def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[SocketOption], - protocolFamily: Option[ProtocolFamily] + override def bindDatagramSocket( + address: GenSocketAddress, + options: List[SocketOption] ): Resource[F, DatagramSocket[F]] = - fallback.openDatagramSocket(address, port, options, protocolFamily) + fallback.bindDatagramSocket(address, options) // Implementations of deprecated operations - @deprecated( - "3.13.0", - "Explicitly managed socket groups are no longer supported; use connect and bind operations on Network instead" - ) - def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = - Resource.eval(tryGetSelector).flatMap { - case Some(selector) => - Resource.pure(SocketGroup.fromIpSockets(new SelectingIpSocketsProvider(selector))) - case None => fallback.socketGroup(threadCount, threadFactory) - } + override def datagramSocketGroup( + threadFactory: ThreadFactory + ): Resource[F, DatagramSocketGroup[F]] = + Resource.pure(this) + + override def socketGroup( + threadCount: Int, + threadFactory: ThreadFactory + ): Resource[F, SocketGroup[F]] = + Resource.pure(this) } - def forAsync[F[_]](implicit F: Async[F]): Network[F] = - forAsyncAndDns(F, Dns.forAsync(F)) + def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = { + val _ = dns + forAsync + } - def forAsyncAndDns[F[_]](implicit F: Async[F], dns: Dns[F]): Network[F] = + def forAsync[F[_]](implicit F: Async[F]): Network[F] = new AsyncProviderBasedNetwork[F] { - protected def mkIpSocketsProvider = IpSocketsProvider.forAsync[F] - protected def mkUnixSocketsProvider = UnixSocketsProvider.forAsync[F] - protected def mkDatagramSocketGroup = DatagramSocketGroup.unsafe[F](globalAdsg) + protected def mkIpSocketsProvider = AsynchronousChannelGroupIpSocketsProvider.forAsync[F] + protected def mkUnixSocketsProvider = AutoDetectingUnixSocketsProvider.forAsync[F] - def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = - Resource - .make(F.delay(AsynchronousDatagramSocketGroup.unsafe(threadFactory)))(adsg => - F.delay(adsg.close()) - ) - .map(DatagramSocketGroup.unsafe[F](_)) + protected def mkIpDatagramSocketsProvider = AsyncIpDatagramSocketsProvider.forAsync[F] + protected def mkUnixDatagramSocketsProvider = + AutoDetectingUnixDatagramSocketsProvider.forAsync[F] // Implementations of deprecated operations + def datagramSocketGroup(threadFactory: ThreadFactory): Resource[F, DatagramSocketGroup[F]] = + Resource.pure(this) + def socketGroup(threadCount: Int, threadFactory: ThreadFactory): Resource[F, SocketGroup[F]] = - Resource.pure(SocketGroup.fromIpSockets(ipSockets)) + Resource.pure(this) } } diff --git a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala index 685177ec50..8bffd96ff9 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SelectingIpSocketsProvider.scala @@ -40,7 +40,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici F3: Dns[F] ) extends IpSocketsProvider[F] { - def connect( + override def connectIp( to: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] = @@ -78,7 +78,7 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici configure *> connect } - def bind( + override def bindIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = @@ -141,5 +141,4 @@ private final class SelectingIpSocketsProvider[F[_]](selector: Selector)(implici SocketAddress.fromInetSocketAddress( ch.getRemoteAddress.asInstanceOf[InetSocketAddress] ) - } diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala index 649c1b5a6f..16dc1c2dbb 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala @@ -63,25 +63,59 @@ object DTLSSocket { ): F[DTLSSocket[F]] = Applicative[F].pure { new DTLSSocket[F] { + override def connect(address: GenSocketAddress) = + socket.connect(address) - def read: F[Datagram] = + override def disconnect = + socket.disconnect + + override def readGen: F[GenDatagram] = read.map(_.toGenDatagram) + + override def read: F[Datagram] = engine.read(Int.MaxValue).flatMap { case Some(bytes) => Applicative[F].pure(Datagram(remoteAddress, bytes)) case None => read } - def reads: Stream[F, Datagram] = + override def reads: Stream[F, Datagram] = Stream.repeatEval(read) - def write(datagram: Datagram): F[Unit] = + + override def write(bytes: Chunk[Byte]): F[Unit] = + engine.write(bytes) + + override def write(bytes: Chunk[Byte], address: GenSocketAddress): F[Unit] = + if (address == remoteAddress) engine.write(bytes) + else + new IOException( + "Cannot write to a DTLSSocket with an address that differs from the connected address" + ).raiseError + + override def write(datagram: Datagram): F[Unit] = engine.write(datagram.bytes) - def writes: Pipe[F, Datagram, Nothing] = + override def writes: Pipe[F, Datagram, Nothing] = _.foreach(write) - def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress - def join(join: MulticastJoin[IpAddress], interface: NetworkInterface): F[GroupMembership] = + + override def address = socket.address + + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) + override def localAddress: F[SocketAddress[IpAddress]] = socket.localAddress + + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] = Sync[F].raiseError(new RuntimeException("DTLSSocket does not support multicast")) - def beginHandshake: F[Unit] = engine.beginHandshake - def session: F[SSLSession] = engine.session + + override def supportedOptions = socket.supportedOptions + override def getOption[A](key: SocketOption.Key[A]) = socket.getOption(key) + override def setOption[A](key: SocketOption.Key[A], value: A) = socket.setOption(key, value) + + override def beginHandshake: F[Unit] = engine.beginHandshake + override def session: F[SSLSession] = engine.session } } } diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala index 681f7793ef..c0dd0969c9 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala @@ -186,7 +186,7 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => new TLSEngine.Binding[F] { def write(data: Chunk[Byte]): F[Unit] = if (data.isEmpty) Applicative[F].unit - else socket.write(Datagram(remoteAddress, data)) + else socket.write(data, remoteAddress) def read(maxBytes: Int): F[Option[Chunk[Byte]]] = socket.read.map(p => Some(p.bytes)) }, diff --git a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala index 8771bf5856..adcbc1ff30 100644 --- a/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/unixsocket/UnixSocketsPlatform.scala @@ -40,7 +40,7 @@ private[unixsocket] trait UnixSocketsCompanionPlatform { self: UnixSockets.type @deprecated("Use Network instead", "3.13.0") def forAsyncAndFiles[F[_]: Async: Files]: UnixSockets[F] = { val _ = Files[F] - new AsyncUnixSockets(UnixSocketsProvider.forAsync) + new AsyncUnixSockets(AutoDetectingUnixSocketsProvider.forAsync) } @deprecated("Use Network instead", "3.13.0") diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala new file mode 100644 index 0000000000..60b8ff99e1 --- /dev/null +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package io +package net + +import cats.effect.IO +import cats.syntax.all._ + +import com.comcast.ip4s._ + +import scala.concurrent.duration._ + +import fs2.io.file.Files + +class UnixDatagramSuite extends Fs2Suite { + + private def sendAndReceive( + socket: DatagramSocket[IO], + bytes: Chunk[Byte], + address: UnixSocketAddress + ): IO[Datagram] = + socket + .write(bytes, address) >> socket.read.timeoutTo( + 1.second, + IO.defer(sendAndReceive(socket, bytes, address)) + ) + + private def sendAndReceiveBytes(socket: DatagramSocket[IO], bytes: Chunk[Byte]): IO[Datagram] = + socket + .write(bytes) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceiveBytes(socket, bytes))) + + val opts = List(SocketOption.unixSocketDeleteIfExists(true)) + val tempUnixSocketAddress = Files[IO].tempFile.map(f => UnixSocketAddress(f.toString)) + + test("echo one") { + val msg = Chunk.array("Hello, world!".getBytes) + Stream + .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) + .flatMap { case (serverAddress, clientAddress) => + + Stream + .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) + .flatMap { serverSocket => + val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + val client = + Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { + clientSocket => + sendAndReceive(clientSocket, msg, serverAddress) + } + client.concurrently(server) + } + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } + + test("echo connected") { + val msg = Chunk.array("Hello, world!".getBytes) + Stream + .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) + .flatMap { case (serverAddress, clientAddress) => + Stream + .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) + .flatMap { serverSocket => + val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + val client = + Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { + clientSocket => + clientSocket.connect(serverAddress) >> sendAndReceiveBytes(clientSocket, msg) + } + client.concurrently(server) + } + } + .compile + .lastOrError + .map(_.bytes) + .assertEquals(msg) + } +} diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index 818f56fa59..4408b2a29b 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -25,6 +25,8 @@ package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - if (JdkUnixSocketsProvider.supported) testProvider("jdk", JdkUnixSocketsProvider.forAsync[IO]) - if (JnrUnixSocketsProvider.supported) testProvider("jnr", JnrUnixSocketsProvider.forAsync[IO]) + if (JdkUnixSocketsProvider.supported) + testProvider("jdk", JdkUnixSocketsProvider.forAsyncAndFiles[IO]) + if (JnrUnixSocketsProvider.supported) + testProvider("jnr", JnrUnixSocketsProvider.forAsyncAndFiles[IO]) } diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala index 83ef0ac961..715dc418ff 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/DTLSSocketSuite.scala @@ -35,16 +35,14 @@ class DTLSSocketSuite extends TLSSuite { val msg = Chunk.array("Hello, world!".getBytes) def address(s: DatagramSocket[IO]) = - Resource - .eval(s.localAddress) - .map(a => SocketAddress(ip"127.0.0.1", a.port)) + SocketAddress(ip"127.0.0.1", s.address.asIpUnsafe.port) val setup = for { tlsContext <- Resource.eval(testTlsContext) - serverSocket <- Network[IO].openDatagramSocket() - serverAddress <- address(serverSocket) - clientSocket <- Network[IO].openDatagramSocket() - clientAddress <- address(clientSocket) + serverSocket <- Network[IO].bindDatagramSocket() + serverAddress = address(serverSocket) + clientSocket <- Network[IO].bindDatagramSocket() + clientAddress = address(clientSocket) tlsServerSocket <- tlsContext .dtlsServerBuilder(serverSocket, clientAddress) .withLogger(logger) diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala index a38b166392..a7928ca2eb 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingIpSocketsProvider.scala @@ -42,7 +42,10 @@ import scala.scalanative.posix.unistd._ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: Async[F]) extends IpSocketsProvider[F] { - def connect(to: SocketAddress[Host], options: List[SocketOption]): Resource[F, Socket[F]] = for { + override def connectIp( + to: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) address <- Resource.eval(to.resolve) ipv4 = address.host.isInstanceOf[Ipv4Address] @@ -77,7 +80,7 @@ private final class FdPollingIpSocketsProvider[F[_]: Dns: LiftIO](implicit F: As ) } yield socket - def bind( + override def bindIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] = for { diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 934bf4347c..822d948266 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -28,7 +28,7 @@ import cats.syntax.all._ import com.comcast.ip4s.UnixSocketAddress -import fs2.io.file.{Files, Path} +import fs2.io.file.Files import fs2.io.internal.NativeUtil._ import fs2.io.internal.SocketHelpers import fs2.io.internal.syssocket.{connect => uconnect, bind => ubind, _} @@ -45,7 +45,10 @@ import scala.scalanative.unsigned._ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F: Async[F]) extends UnixSocketsProvider[F] { - def connect(address: UnixSocketAddress, options: List[SocketOption]): Resource[F, Socket[F]] = + override def connectUnix( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, Socket[F]] = for { poller <- Resource.eval(fileDescriptorPoller[F]) fd <- SocketHelpers.openNonBlocking(AF_UNIX, SOCK_STREAM) @@ -75,31 +78,12 @@ private final class FdPollingUnixSocketsProvider[F[_]: Files: LiftIO](implicit F ) } yield socket - def bind( + override def bindUnix( address: UnixSocketAddress, options: List[SocketOption] ): Resource[F, ServerSocket[F]] = { - var deleteIfExists: Boolean = false - var deleteOnClose: Boolean = true - - val filteredOptions = options.filter { opt => - opt.key match { - case SocketOption.UnixServerSocketDeleteIfExists => - deleteIfExists = opt.value.asInstanceOf[java.lang.Boolean] - false - case SocketOption.UnixServerSocketDeleteOnClose => - deleteOnClose = opt.value.asInstanceOf[java.lang.Boolean] - false - case _ => true - } - } - - val delete = Resource.make { - Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) - } { _ => - Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) - } + val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) for { poller <- Resource.eval(fileDescriptorPoller[F]) diff --git a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala index fc04a94c2f..af583a624e 100644 --- a/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -36,7 +36,14 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N protected def mkIpSocketsProvider = new FdPollingIpSocketsProvider[F]()(Dns.forAsync, implicitly, implicitly) protected def mkUnixSocketsProvider = new FdPollingUnixSocketsProvider[F] - protected def mkDatagramSocketGroup = throw new UnsupportedOperationException + protected def mkIpDatagramSocketsProvider = + throw new UnsupportedOperationException( + "Datagram sockets not currently supported on Native" + ) + protected def mkUnixDatagramSocketsProvider = + throw new UnsupportedOperationException( + "Unix datagram sockets not currently supported on Native" + ) } def forAsync[F[_]](implicit F: Async[F]): Network[F] = diff --git a/io/shared/src/main/scala/fs2/io/net/Datagram.scala b/io/shared/src/main/scala/fs2/io/net/Datagram.scala index ea84525c26..72d0696f5e 100644 --- a/io/shared/src/main/scala/fs2/io/net/Datagram.scala +++ b/io/shared/src/main/scala/fs2/io/net/Datagram.scala @@ -23,11 +23,17 @@ package fs2 package io package net -import com.comcast.ip4s.{IpAddress, SocketAddress} +import com.comcast.ip4s.{GenSocketAddress, IpAddress, Ipv4Address, Port, SocketAddress} /** A single datagram to send to the specified remote address or received from the specified address. * * @param remote remote party to send/receive datagram to/from * @param bytes data to send/receive */ -final case class Datagram(remote: SocketAddress[IpAddress], bytes: Chunk[Byte]) +final case class Datagram(remote: SocketAddress[IpAddress], bytes: Chunk[Byte]) { + def toGenDatagram: GenDatagram = GenDatagram(remote, bytes) +} + +final case class GenDatagram(remote: GenSocketAddress, bytes: Chunk[Byte]) { + def toDatagram: Datagram = Datagram(SocketAddress(Ipv4Address.Wildcard, Port.Wildcard), bytes) +} diff --git a/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala index 8c637c84a2..f92d0be8dc 100644 --- a/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala @@ -29,6 +29,23 @@ import com.comcast.ip4s._ */ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { + /** Local address of this socket. */ + def address: GenSocketAddress + + /** Gets the set of options that may be used with `setOption`. Note some options may not support `getOption`. */ + def supportedOptions: F[Set[SocketOption.Key[?]]] + + /** Gets the value of the specified option, if defined. */ + def getOption[A](key: SocketOption.Key[A]): F[Option[A]] + + /** Sets the specified option to the supplied value. */ + def setOption[A](key: SocketOption.Key[A], value: A): F[Unit] + + def readGen: F[GenDatagram] + + def connect(address: GenSocketAddress): F[Unit] + def disconnect: F[Unit] + /** Reads a single datagram from this udp socket. */ def read: F[Datagram] @@ -46,13 +63,25 @@ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { * * @param datagram datagram to write */ - def write(datagram: Datagram): F[Unit] + def write(datagram: Datagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + def write(datagram: GenDatagram): F[Unit] = + write(datagram.bytes, datagram.remote) + + def write(bytes: Chunk[Byte]): F[Unit] + + def write(bytes: Chunk[Byte], address: GenSocketAddress): F[Unit] /** Writes supplied datagrams to this udp socket. */ def writes: Pipe[F, Datagram, Nothing] /** Returns the local address of this udp socket. */ + @deprecated( + "3.13.0", + "Use address instead, which returns GenSocketAddress instead of F[SocketAddress[IpAddress]]. If ip and port are needed, call .asIpUnsafe" + ) def localAddress: F[SocketAddress[IpAddress]] /** Joins a multicast group on a specific network interface. diff --git a/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala index b843881819..75d2cbc31e 100644 --- a/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/DatagramSocketGroup.scala @@ -35,6 +35,7 @@ trait DatagramSocketGroup[F[_]] { * @param options socket options to apply to the underlying socket * @param protocolFamily protocol family to use when opening the supporting `DatagramChannel` */ + @deprecated("3.13.0", "Use Network[F].bindDatagramSocket instead") def openDatagramSocket( address: Option[Host] = None, port: Option[Port] = None, diff --git a/io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala b/io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala similarity index 81% rename from io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala rename to io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala index e35b9c69e1..fcff0d9bf1 100644 --- a/io/native/src/main/scala/fs2/io/net/UnixSocketsProviderPlatform.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpDatagramSocketsProvider.scala @@ -23,9 +23,13 @@ package fs2 package io package net -import cats.effect.{Async, LiftIO} +import cats.effect.kernel.Resource +import com.comcast.ip4s.{Host, SocketAddress} -private[net] trait UnixSocketsProviderCompanionPlatform { - implicit def forLiftIO[F[_]: Async: LiftIO]: UnixSocketsProvider[F] = - new FdPollingUnixSocketsProvider[F] +private[net] trait IpDatagramSocketsProvider[F[_]] { + + def bindDatagramSocket( + address: SocketAddress[Host], + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] } diff --git a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala index ab49f57c72..f0489bf21f 100644 --- a/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/IpSocketsProvider.scala @@ -28,15 +28,13 @@ import com.comcast.ip4s.{Host, SocketAddress} private[net] trait IpSocketsProvider[F[_]] { - def connect( + def connectIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, Socket[F]] - def bind( + def bindIp( address: SocketAddress[Host], options: List[SocketOption] ): Resource[F, ServerSocket[F]] } - -private[net] object IpSocketsProvider extends IpSocketsProviderCompanionPlatform diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 21430fb372..fa52df1909 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -61,15 +61,25 @@ sealed trait Network[F[_]] def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] def bind( - address: GenSocketAddress, + address: GenSocketAddress = SocketAddress.Wildcard, options: List[SocketOption] = Nil ): Resource[F, ServerSocket[F]] def bindAndAccept( - address: GenSocketAddress, + address: GenSocketAddress = SocketAddress.Wildcard, options: List[SocketOption] = Nil ): Stream[F, Socket[F]] + /** Creates a datagram socket bound to the specified address. + * + * @param address address to bind to + * @param options socket options to apply to the underlying socket + */ + def bindDatagramSocket( + address: GenSocketAddress = SocketAddress.Wildcard, + options: List[SocketOption] = Nil + ): Resource[F, DatagramSocket[F]] + /** Returns a builder for `TLSContext[F]` values. * * For example, `Network[IO].tlsContext.system` returns a `F[TLSContext[F]]`. @@ -139,17 +149,31 @@ object Network extends NetworkCompanionPlatform { options ) .map(ss => ss.address.asIpUnsafe -> ss.accept) + + override def openDatagramSocket( + address: Option[Host], + port: Option[Port], + options: List[DatagramSocketOption], + protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] + ): Resource[F, DatagramSocket[F]] = + bindDatagramSocket( + SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), + Nil /* TODO convert options */ + ) } private[fs2] abstract class AsyncProviderBasedNetwork[F[_]](implicit F: Async[F]) extends AsyncNetwork[F] { protected def mkIpSocketsProvider: IpSocketsProvider[F] protected def mkUnixSocketsProvider: UnixSocketsProvider[F] - protected def mkDatagramSocketGroup: DatagramSocketGroup[F] + protected def mkIpDatagramSocketsProvider: IpDatagramSocketsProvider[F] + protected def mkUnixDatagramSocketsProvider: UnixDatagramSocketsProvider[F] protected lazy val ipSockets: IpSocketsProvider[F] = mkIpSocketsProvider protected lazy val unixSockets: UnixSocketsProvider[F] = mkUnixSocketsProvider - protected lazy val datagramSocketGroup: DatagramSocketGroup[F] = mkDatagramSocketGroup + protected lazy val ipDatagramSockets: IpDatagramSocketsProvider[F] = mkIpDatagramSocketsProvider + protected lazy val unixDatagramSockets: UnixDatagramSocketsProvider[F] = + mkUnixDatagramSocketsProvider override def connect( address: GenSocketAddress, @@ -157,8 +181,8 @@ object Network extends NetworkCompanionPlatform { ): Resource[F, Socket[F]] = matchAddress( address, - sa => ipSockets.connect(sa, options), - ua => unixSockets.connect(ua, options) + sa => ipSockets.connectIp(sa, options), + ua => unixSockets.connectUnix(ua, options) ) override def bind( @@ -167,18 +191,19 @@ object Network extends NetworkCompanionPlatform { ): Resource[F, ServerSocket[F]] = matchAddress( address, - sa => ipSockets.bind(sa, options), - ua => unixSockets.bind(ua, options) + sa => ipSockets.bindIp(sa, options), + ua => unixSockets.bindUnix(ua, options) ) - override def openDatagramSocket( - address: Option[Host], - port: Option[Port], - options: List[DatagramSocketOption], - protocolFamily: Option[DatagramSocketGroup.ProtocolFamily] + override def bindDatagramSocket( + address: GenSocketAddress, + options: List[SocketOption] = Nil ): Resource[F, DatagramSocket[F]] = - datagramSocketGroup.openDatagramSocket(address, port, options, protocolFamily) - + matchAddress( + address, + sa => ipDatagramSockets.bindDatagramSocket(sa, options), + ua => unixDatagramSockets.bindDatagramSocket(ua, options) + ) } def apply[F[_]](implicit F: Network[F]): F.type = F diff --git a/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala b/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala new file mode 100644 index 0000000000..588c2be292 --- /dev/null +++ b/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala @@ -0,0 +1 @@ +final case class PeerCredentials(userName: String, groupName: String, processId: Option[Int]) diff --git a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala index 66ce7998e2..48c5c3ab27 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketGroup.scala @@ -24,8 +24,7 @@ package io package net import cats.effect.kernel.Resource -import com.comcast.ip4s.{Host, IpAddress, Ipv4Address, Port, SocketAddress} -import cats.effect.kernel.Async +import com.comcast.ip4s.{Host, IpAddress, Port, SocketAddress} /** Supports creation of client and server TCP sockets that all share * an underlying non-blocking channel group. @@ -73,31 +72,3 @@ trait SocketGroup[F[_]] { options: List[SocketOption] = List.empty ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] } - -private[net] object SocketGroup { - - def fromIpSockets[F[_]: Async](ipSockets: IpSocketsProvider[F]): SocketGroup[F] = - new SocketGroup[F] { - def client(to: SocketAddress[Host], options: List[SocketOption]) = - ipSockets.connect(to, options) - - def server( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Stream[F, Socket[F]] = - Stream.resource(serverResource(address, port, options)).flatMap(_._2) - - def serverResource( - address: Option[Host], - port: Option[Port], - options: List[SocketOption] - ): Resource[F, (SocketAddress[IpAddress], Stream[F, Socket[F]])] = - ipSockets - .bind( - SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), - options - ) - .map(ss => ss.address.asIpUnsafe -> ss.accept) - } -} diff --git a/io/shared/src/main/scala/fs2/io/net/SocketOption.scala b/io/shared/src/main/scala/fs2/io/net/SocketOption.scala index 859bc5fe3a..e48f5bbef8 100644 --- a/io/shared/src/main/scala/fs2/io/net/SocketOption.scala +++ b/io/shared/src/main/scala/fs2/io/net/SocketOption.scala @@ -21,11 +21,15 @@ package fs2.io.net -/** Specifies a socket option on a TCP/UDP socket. +import cats.Applicative +import cats.effect.Resource +import cats.syntax.all.* +import com.comcast.ip4s.UnixSocketAddress +import fs2.io.file.{Files, Path} + +/** Specifies a socket option on a socket. * - * The companion provides methods for creating a socket option from each of the - * JDK `java.net.StandardSocketOptions` as well as the ability to construct arbitrary - * additional options. See the docs on `StandardSocketOptions` for details on each. + * The companion provides methods for creating various socket options. */ sealed trait SocketOption { type Value @@ -39,4 +43,32 @@ object SocketOption extends SocketOptionCompanionPlatform { val key = key0 val value = value0 } + + private[net] def extractUnixSocketDeletes[F[_]: Applicative: Files]( + options: List[SocketOption], + address: UnixSocketAddress + ): (List[SocketOption], Resource[F, Unit]) = { + var deleteIfExists: Boolean = false + var deleteOnClose: Boolean = true + + val filteredOptions = options.filter { opt => + if (opt.key == SocketOption.UnixSocketDeleteIfExists) { + deleteIfExists = opt.value.asInstanceOf[Boolean] + false + } else if (opt.key == SocketOption.UnixSocketDeleteOnClose) { + deleteOnClose = opt.value.asInstanceOf[Boolean] + false + } else { + true + } + } + + val delete = Resource.make { + Files[F].deleteIfExists(Path(address.path)).whenA(deleteIfExists) + } { _ => + Files[F].deleteIfExists(Path(address.path)).whenA(deleteOnClose) + } + + (filteredOptions, delete) + } } diff --git a/io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala b/io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala similarity index 82% rename from io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala rename to io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala index e068553171..1b7c99d583 100644 --- a/io/jvm/src/main/scala/fs2/io/net/IpSocketsProviderPlatform.scala +++ b/io/shared/src/main/scala/fs2/io/net/UnixDatagramSocketsProvider.scala @@ -23,10 +23,13 @@ package fs2 package io package net -import cats.effect.Async +import cats.effect.Resource +import com.comcast.ip4s.UnixSocketAddress -private[net] trait IpSocketsProviderCompanionPlatform { self: IpSocketsProvider.type => +private[net] trait UnixDatagramSocketsProvider[F[_]] { - private[net] def forAsync[F[_]: Async]: IpSocketsProvider[F] = - AsynchronousChannelGroupIpSocketsProvider.forAsync[F] + def bindDatagramSocket( + address: UnixSocketAddress, + options: List[SocketOption] + ): Resource[F, DatagramSocket[F]] } diff --git a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala index f84b8ec0f2..5b9a81c964 100644 --- a/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala +++ b/io/shared/src/main/scala/fs2/io/net/UnixSocketsProvider.scala @@ -28,15 +28,13 @@ import com.comcast.ip4s.UnixSocketAddress private[net] trait UnixSocketsProvider[F[_]] { - def connect( + def connectUnix( address: UnixSocketAddress, options: List[SocketOption] ): Resource[F, Socket[F]] - def bind( + def bindUnix( address: UnixSocketAddress, options: List[SocketOption] ): Resource[F, ServerSocket[F]] } - -private[net] object UnixSocketsProvider extends UnixSocketsProviderCompanionPlatform diff --git a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala index 7f071583fc..8b4abf59dc 100644 --- a/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala +++ b/io/shared/src/main/scala/fs2/io/net/unixsocket/UnixSockets.scala @@ -62,7 +62,7 @@ object UnixSockets extends UnixSocketsCompanionPlatform { extends UnixSockets[F] { def client(address: UnixSocketAddress): Resource[F, Socket[F]] = - delegate.connect(Ip4sUnixSocketAddress(address.path), Nil) + delegate.connectUnix(Ip4sUnixSocketAddress(address.path), Nil) def server( address: UnixSocketAddress, @@ -71,11 +71,11 @@ object UnixSockets extends UnixSocketsCompanionPlatform { ): Stream[F, Socket[F]] = Stream .resource( - delegate.bind( + delegate.bindUnix( Ip4sUnixSocketAddress(address.path), List( - SocketOption.unixServerSocketDeleteIfExists(deleteIfExists), - SocketOption.unixServerSocketDeleteOnClose(deleteOnClose) + SocketOption.unixSocketDeleteIfExists(deleteIfExists), + SocketOption.unixSocketDeleteOnClose(deleteOnClose) ) ) ) diff --git a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala index 93bf4e832b..43c493ce67 100644 --- a/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/UnixSocketsSuite.scala @@ -34,14 +34,14 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") val server = Stream - .resource(sockets.bind(address, Nil)) + .resource(sockets.bindUnix(address, Nil)) .flatMap(_.accept) .map { socket => socket.reads.through(socket.writes) } .parJoinUnbounded - def client(msg: Chunk[Byte]) = sockets.connect(address, Nil).use { socket => + def client(msg: Chunk[Byte]) = sockets.connectUnix(address, Nil).use { socket => socket.write(msg) *> socket.endOfOutput *> socket.reads.compile .to(Chunk) .map(read => assertEquals(read, msg)) @@ -59,7 +59,7 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { val address = UnixSocketAddress("fs2-unix-sockets-test.sock") val server = Stream - .resource(sockets.bind(address, Nil)) + .resource(sockets.bindUnix(address, Nil)) .flatMap { ss => assertEquals(ss.address, address) ss.accept @@ -72,7 +72,7 @@ class UnixSocketsSuite extends Fs2Suite with UnixSocketsSuitePlatform { .parJoinUnbounded val msg = Chunk.array("Hello, world".getBytes) - val client = Stream.resource(sockets.connect(address, Nil).evalMap { socket => + val client = Stream.resource(sockets.connectUnix(address, Nil).evalMap { socket => assertEquals(socket.address, UnixSocketAddress("")) assertEquals(socket.peerAddress, address) socket.write(msg) *> socket.endOfOutput *> socket.reads.compile From 9ef0cf7cd9d58f0c7f12523db434f7fcb579a475 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 08:13:00 -0400 Subject: [PATCH 51/79] Remove accidentally added PeerCredentials --- io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala | 1 - 1 file changed, 1 deletion(-) delete mode 100644 io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala diff --git a/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala b/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala deleted file mode 100644 index 588c2be292..0000000000 --- a/io/shared/src/main/scala/fs2/io/net/PeerCredentials.scala +++ /dev/null @@ -1 +0,0 @@ -final case class PeerCredentials(userName: String, groupName: String, processId: Option[Int]) From 72b119784b0ea466bb2fab937f6e654dd8d91640 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 08:27:50 -0400 Subject: [PATCH 52/79] Fix test compilation --- io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala | 2 +- .../src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index cc5a6fd91c..0d3a307f9e 100644 --- a/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala +++ b/io/js/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -25,5 +25,5 @@ package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("node.js", UnixSocketsProvider.forAsync[IO]) + testProvider("node.js", new AsyncSocketsProvider[IO]) } diff --git a/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala index 6f27975eaa..b8ed8e270b 100644 --- a/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala +++ b/io/native/src/test/scala/fs2/io/net/UnixSocketsSuitePlatform.scala @@ -25,5 +25,5 @@ package io.net import cats.effect.IO trait UnixSocketsSuitePlatform { self: UnixSocketsSuite => - testProvider("native", UnixSocketsProvider.forLiftIO[IO]) + testProvider("native", new FdPollingUnixSocketsProvider[IO]) } From 7a837000580c07c683c1fec6453b90c2f1c98158 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 08:41:22 -0400 Subject: [PATCH 53/79] Fix mima warnings --- build.sbt | 16 +++++++++++++++- .../scala/fs2/io/net/DatagramSocketOption.scala | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 2382de7613..3d5b85df60 100644 --- a/build.sbt +++ b/build.sbt @@ -320,7 +320,21 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ), ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.FdPollingSocket.apply"), ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.FdPollingSocketGroup"), - ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.FdPollingUnixSockets") + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.FdPollingUnixSockets"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.remote"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.this"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.address"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.supportedOptions"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.getOption"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.setOption"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.readGen"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.connect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.disconnect"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.write"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.DatagramSocketGroupCompanionPlatform$AsyncDatagramSocketGroup"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bindDatagramSocket"), + ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.SocketOptionCompanionPlatform#Key.fs2$io$net$SocketOptionCompanionPlatform$Key$$$outer"), ) lazy val root = tlCrossRootProject diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index 9bbfaef4cc..db7dfba622 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -64,7 +64,7 @@ object DatagramSocketOption { Sync[F].delay(sock.setMulticastInterface(value)) } - object MulticastLoop extends Key[Boolean] { + object MulticastLoopback extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay { sock.setMulticastLoopback(value) @@ -104,7 +104,7 @@ object DatagramSocketOption { def broadcast(value: Boolean): DatagramSocketOption = apply(Broadcast, value) def multicastInterface(value: String): DatagramSocketOption = apply(MulticastInterface, value) - def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoop, value) + def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoopback, value) def multicastTtl(value: Int): DatagramSocketOption = apply(MulticastTtl, value) def receiveBufferSize(value: Int): DatagramSocketOption = apply(ReceiveBufferSize, value) def sendBufferSize(value: Int): DatagramSocketOption = apply(SendBufferSize, value) From 4248d6841893f660ad0db0ded75ee0a84cf2d73e Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 08:56:31 -0400 Subject: [PATCH 54/79] Scalafmt --- build.sbt | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 3d5b85df60..6e0ed770b7 100644 --- a/build.sbt +++ b/build.sbt @@ -321,20 +321,30 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.FdPollingSocket.apply"), ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.FdPollingSocketGroup"), ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.unixsocket.FdPollingUnixSockets"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.remote"), - ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.this"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]( + "fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.remote" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "fs2.io.net.AsynchronousDatagramSocketGroup#WriterDatagram.this" + ), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.address"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.supportedOptions"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.DatagramSocket.supportedOptions" + ), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.getOption"), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.setOption"), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.readGen"), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.connect"), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.disconnect"), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.write"), - ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.DatagramSocketGroupCompanionPlatform$AsyncDatagramSocketGroup"), + ProblemFilters.exclude[MissingClassProblem]( + "fs2.io.net.DatagramSocketGroupCompanionPlatform$AsyncDatagramSocketGroup" + ), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.bindDatagramSocket"), ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.SocketOptionCompanionPlatform#Key.fs2$io$net$SocketOptionCompanionPlatform$Key$$$outer"), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.SocketOptionCompanionPlatform#Key.fs2$io$net$SocketOptionCompanionPlatform$Key$$$outer" + ) ) lazy val root = tlCrossRootProject From e6aeb4d82144dac7f0f626ae4a9b01fa00c381f0 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 11:34:33 -0400 Subject: [PATCH 55/79] Fix warnings --- .../main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index 3b0b7c5d6e..4349480dab 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -60,6 +60,8 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 options: List[SocketOption] ): Resource[F, DatagramSocket[F]] = { val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) + // TODO use filtered options + val _ = filteredOptions delete *> Resource .make(F.blocking(UnixDatagramChannel.open()))(ch => F.blocking(ch.close())) From e0d78b73b4842cbb0aff095c108d147a92e0e2cc Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 4 Jun 2025 12:12:35 -0400 Subject: [PATCH 56/79] Fix warnings --- .../fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala index c9126d14d6..87dde719a5 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/AsynchronousChannelGroupIpSocketsProvider.scala @@ -152,7 +152,7 @@ private[net] class AsynchronousChannelGroupIpSocketsProvider[F[_]] private ( private[net] object AsynchronousChannelGroupIpSocketsProvider { def forAsync[F[_]: Async]: AsynchronousChannelGroupIpSocketsProvider[F] = { - implicit val dnsInstance = Dns.forAsync[F] + implicit val dnsInstance: Dns[F] = Dns.forAsync[F] new AsynchronousChannelGroupIpSocketsProvider[F](null) } } From 854291929d2811c65bf1c8c47b8a1af28ace9a60 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 5 Jun 2025 07:32:46 -0400 Subject: [PATCH 57/79] Add temp debug to UnixDatagramSuite --- io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala index 60b8ff99e1..38e3ceb05c 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -61,10 +61,13 @@ class UnixDatagramSuite extends Fs2Suite { Stream .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) .flatMap { serverSocket => - val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + println(s"Bound server socket: " + serverSocket) + // val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) + val server = Stream.repeatEval(serverSocket.readGen).foreach { p => println(s"Server got: $p"); serverSocket.write(p) } val client = Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { clientSocket => + println(s"Bound client socket: " + clientSocket) sendAndReceive(clientSocket, msg, serverAddress) } client.concurrently(server) From dac60c14cd711de6860d3d9243a3a3fd92ea46bc Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 5 Jun 2025 07:47:13 -0400 Subject: [PATCH 58/79] Scalafmt --- .../fs2/io/net/JnrUnixDatagramSocketsProvider.scala | 9 ++++++--- io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index 4349480dab..ae85a926b4 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -35,6 +35,8 @@ import fs2.io.file.Files import java.nio.{Buffer, ByteBuffer} import jnr.unixsocket.{UnixDatagramChannel, UnixSocketAddress => JnrUnixSocketAddress} +import CollectionCompat.* + private[net] object JnrUnixDatagramSocketsProvider { lazy val supported: Boolean = @@ -60,8 +62,6 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 options: List[SocketOption] ): Resource[F, DatagramSocket[F]] = { val (filteredOptions, delete) = SocketOption.extractUnixSocketDeletes(options, address) - // TODO use filtered options - val _ = filteredOptions delete *> Resource .make(F.blocking(UnixDatagramChannel.open()))(ch => F.blocking(ch.close())) @@ -69,6 +69,7 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 F.blocking(ch.bind(new JnrUnixSocketAddress(address.path))) .cancelable(F.blocking(ch.close())) } + .evalTap(ch => F.delay(filteredOptions.foreach(o => ch.setOption(o.key, o.value)))) .evalMap { ch => Mutex[F].map(ch -> _) } @@ -80,7 +81,9 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 override val address = address0 override def localAddress = F.delay(address.asIpUnsafe) - override def supportedOptions = ??? + override def supportedOptions = + F.delay(ch.supportedOptions.asScala.toSet) + override def getOption[A](key: SocketOption.Key[A]) = ??? override def setOption[A](key: SocketOption.Key[A], value: A) = ??? diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala index 38e3ceb05c..df2977277a 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -63,7 +63,9 @@ class UnixDatagramSuite extends Fs2Suite { .flatMap { serverSocket => println(s"Bound server socket: " + serverSocket) // val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) - val server = Stream.repeatEval(serverSocket.readGen).foreach { p => println(s"Server got: $p"); serverSocket.write(p) } + val server = Stream.repeatEval(serverSocket.readGen).foreach { p => + println(s"Server got: $p"); serverSocket.write(p) + } val client = Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { clientSocket => From d9639537438098658358de3e615cf474ea4fd995 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 5 Jun 2025 08:15:54 -0400 Subject: [PATCH 59/79] Debug --- .../io/net/JnrUnixDatagramSocketsProvider.scala | 17 ++++++++++++++--- .../scala/fs2/io/net/UnixDatagramSuite.scala | 5 +++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index ae85a926b4..b0b7b5ff1c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -76,7 +76,7 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 .map { case (ch, readMutex) => val address0 = address new DatagramSocket[F] { - private val readBuffer = ByteBuffer.allocate(65535) + private val readBuffer = ByteBuffer.allocate(1 << 16) override val address = address0 override def localAddress = F.delay(address.asIpUnsafe) @@ -84,8 +84,17 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 override def supportedOptions = F.delay(ch.supportedOptions.asScala.toSet) - override def getOption[A](key: SocketOption.Key[A]) = ??? - override def setOption[A](key: SocketOption.Key[A], value: A) = ??? + override def getOption[A](key: SocketOption.Key[A]) = + F.delay { + try + Some(ch.getOption(key)) + catch { + case _: UnsupportedOperationException => None + } + } + + override def setOption[A](key: SocketOption.Key[A], value: A) = + F.delay(ch.setOption(key, value)).void private def withUnixSocketAddress[A]( address: GenSocketAddress @@ -119,7 +128,9 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 override def readGen = readMutex.lock.surround { blockingAndCloseIfCanceled { + println("- READING from " + address) val clientAddress = ch.receive(readBuffer) + println("-- success " + clientAddress) val read = readBuffer.position() val result = if (read == 0) Chunk.empty diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala index df2977277a..9b952bc5a2 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -61,7 +61,7 @@ class UnixDatagramSuite extends Fs2Suite { Stream .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) .flatMap { serverSocket => - println(s"Bound server socket: " + serverSocket) + println(s"Bound server socket: " + serverSocket.address) // val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) val server = Stream.repeatEval(serverSocket.readGen).foreach { p => println(s"Server got: $p"); serverSocket.write(p) @@ -69,7 +69,7 @@ class UnixDatagramSuite extends Fs2Suite { val client = Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { clientSocket => - println(s"Bound client socket: " + clientSocket) + println(s"Bound client socket: " + clientSocket.address) sendAndReceive(clientSocket, msg, serverAddress) } client.concurrently(server) @@ -82,6 +82,7 @@ class UnixDatagramSuite extends Fs2Suite { } test("echo connected") { + println("----------------") val msg = Chunk.array("Hello, world!".getBytes) Stream .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) From 73f640df5789ad5c9e39aab7610b91beefa26c87 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 5 Jun 2025 08:26:26 -0400 Subject: [PATCH 60/79] Debug --- io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala index 9b952bc5a2..b87abbc1d4 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -71,6 +71,7 @@ class UnixDatagramSuite extends Fs2Suite { clientSocket => println(s"Bound client socket: " + clientSocket.address) sendAndReceive(clientSocket, msg, serverAddress) + .flatTap(_ => IO.println("client done")) } client.concurrently(server) } From e02d9e5b64a28464732a59a3a8e80c3c688992dd Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 5 Jun 2025 14:12:58 -0400 Subject: [PATCH 61/79] Exclude UnixDatagramSuite from Linux due to bug in jnr-unixsocket --- .../fs2/io/net/JnrUnixDatagramSocketsProvider.scala | 2 -- .../test/scala/fs2/io/net/UnixDatagramSuite.scala | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index b0b7b5ff1c..e42bbf3b0c 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -128,9 +128,7 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 override def readGen = readMutex.lock.surround { blockingAndCloseIfCanceled { - println("- READING from " + address) val clientAddress = ch.receive(readBuffer) - println("-- success " + clientAddress) val read = readBuffer.position() val result = if (read == 0) Chunk.empty diff --git a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala index b87abbc1d4..271d711c12 100644 --- a/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/UnixDatagramSuite.scala @@ -29,6 +29,7 @@ import cats.syntax.all._ import com.comcast.ip4s._ import scala.concurrent.duration._ +import scala.util.Properties import fs2.io.file.Files @@ -53,6 +54,7 @@ class UnixDatagramSuite extends Fs2Suite { val tempUnixSocketAddress = Files[IO].tempFile.map(f => UnixSocketAddress(f.toString)) test("echo one") { + assume(!Properties.isLinux) // https://github.com/jnr/jnr-unixsocket/pull/107 val msg = Chunk.array("Hello, world!".getBytes) Stream .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) @@ -61,17 +63,11 @@ class UnixDatagramSuite extends Fs2Suite { Stream .resource(Network[IO].bindDatagramSocket(serverAddress, opts)) .flatMap { serverSocket => - println(s"Bound server socket: " + serverSocket.address) - // val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) - val server = Stream.repeatEval(serverSocket.readGen).foreach { p => - println(s"Server got: $p"); serverSocket.write(p) - } + val server = Stream.repeatEval(serverSocket.readGen).foreach(serverSocket.write) val client = Stream.resource(Network[IO].bindDatagramSocket(clientAddress, opts)).evalMap { clientSocket => - println(s"Bound client socket: " + clientSocket.address) sendAndReceive(clientSocket, msg, serverAddress) - .flatTap(_ => IO.println("client done")) } client.concurrently(server) } @@ -83,7 +79,7 @@ class UnixDatagramSuite extends Fs2Suite { } test("echo connected") { - println("----------------") + assume(!Properties.isLinux) // https://github.com/jnr/jnr-unixsocket/pull/107 val msg = Chunk.array("Hello, world!".getBytes) Stream .resource((tempUnixSocketAddress, tempUnixSocketAddress).tupled) From 84883218f406fabd259a49d9a23b3b3e1fbc12c3 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Fri, 6 Jun 2025 08:00:21 -0400 Subject: [PATCH 62/79] Bridge deprecated datagram soccket options --- .../main/scala/fs2/io/net/DatagramSocketOption.scala | 12 ++++++++++++ io/jvm-native/src/main/scala/fs2/io/net/net.scala | 5 +++++ io/shared/src/main/scala/fs2/io/net/Network.scala | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index db7dfba622..6f304fd2cd 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -36,6 +36,9 @@ sealed trait DatagramSocketOption { type Value val key: DatagramSocketOption.Key[Value] val value: Value + + private[net] def toSocketOption: SocketOption = + SocketOption(key.toSocketOption, value) } object DatagramSocketOption { @@ -46,6 +49,8 @@ object DatagramSocketOption { } private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: A): F[Unit] + + private[net] def toSocketOption: SocketOption.Key[A] } def apply[A](key0: Key[A], value0: A): DatagramSocketOption = new DatagramSocketOption { @@ -57,11 +62,13 @@ object DatagramSocketOption { object Broadcast extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = Sync[F].delay(sock.setBroadcast(value)) + override private[net] def toSocketOption: SocketOption.Key[Boolean] = SocketOption.Broadcast } object MulticastInterface extends Key[String] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = Sync[F].delay(sock.setMulticastInterface(value)) + override private[net] def toSocketOption: SocketOption.Key[String] = SocketOption.MulticastInterface } object MulticastLoopback extends Key[Boolean] { @@ -70,6 +77,7 @@ object DatagramSocketOption { sock.setMulticastLoopback(value) () } + override private[net] def toSocketOption: SocketOption.Key[Boolean] = SocketOption.MulticastLoop } object MulticastTtl extends Key[Int] { @@ -78,6 +86,7 @@ object DatagramSocketOption { sock.setMulticastTTL(value) () } + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.MulticastTtl } object ReceiveBufferSize extends Key[Int] { @@ -85,6 +94,7 @@ object DatagramSocketOption { Sync[F].delay(Some(sock.getRecvBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setRecvBufferSize(value)) + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.ReceiveBufferSize } object SendBufferSize extends Key[Int] { @@ -92,6 +102,7 @@ object DatagramSocketOption { Sync[F].delay(Some(sock.getSendBufferSize)) override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Int): F[Unit] = Sync[F].delay(sock.setSendBufferSize(value)) + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.SendBufferSize } object Ttl extends Key[Int] { @@ -100,6 +111,7 @@ object DatagramSocketOption { sock.setTTL(value) () } + override private[net] def toSocketOption: SocketOption.Key[Int] = SocketOption.Ttl } def broadcast(value: Boolean): DatagramSocketOption = apply(Broadcast, value) diff --git a/io/jvm-native/src/main/scala/fs2/io/net/net.scala b/io/jvm-native/src/main/scala/fs2/io/net/net.scala index 1e5a6f98b7..ebcd6ecba4 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/net.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/net.scala @@ -32,4 +32,9 @@ package object net { type UnknownHostException = com.comcast.ip4s.UnknownHostException type DatagramSocketOption = SocketOption val DatagramSocketOption = SocketOption + + private[net] implicit final class DatagramSocketOptionOps(private val self: DatagramSocketOption) + extends AnyVal { + def toSocketOption: SocketOption = self + } } diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index fa52df1909..16f71545e7 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -158,7 +158,7 @@ object Network extends NetworkCompanionPlatform { ): Resource[F, DatagramSocket[F]] = bindDatagramSocket( SocketAddress(address.getOrElse(Ipv4Address.Wildcard), port.getOrElse(Port.Wildcard)), - Nil /* TODO convert options */ + options.map(_.toSocketOption) ) } From 645f93fe55485ca01958e10a0c634c9fb766ab6f Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 9 Jun 2025 08:21:07 -0400 Subject: [PATCH 63/79] Docs --- .../src/main/scala/fs2/io/net/Network.scala | 45 ++++++++++++------- site/io.md | 19 ++++---- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 16f71545e7..2bb21e2cc6 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -36,15 +36,18 @@ import com.comcast.ip4s.{ } import fs2.io.net.tls.TLSContext -/** Provides the ability to work with TCP, UDP, and TLS. +/** Provides the ability to work with stream sockets (e.g. TCP), datagram sockets (e.g. UDP), and TLS. + * + * Both IP and unix sockets are supported, though unix socket support depends on the underlying platform support. + * The socket type is derived from addresses supplied to operations that open new sockets. * * @example {{{ * import fs2.Stream * import fs2.io.net.{Datagram, Network} * * def send[F[_]: Network](datagram: Datagram): F[Unit] = - * Network[F].openDatagramSocket().use { socket => - * socket.write(packet) + * Network[F].bindDatagramSocket().use { socket => + * socket.write(datagram) * } * }}} * @@ -58,22 +61,44 @@ sealed trait Network[F[_]] with SocketGroup[F] with DatagramSocketGroup[F] { + /** Opens a stream socket and connects it to the supplied address. + * + * TCP is used when the supplied address contains an IP address or hostname. Unix sockets are also + * supported (when the supplied address contains a unix socket address). + * + * @param address address to connect to + * @param options socket options to apply to the socket + */ def connect(address: GenSocketAddress, options: List[SocketOption] = Nil): Resource[F, Socket[F]] + /** Opens and binds a stream server socket to the supplied address. + * + * TCP is used when the supplied address contains an IP address or hostname. Unix sockets are also + * supported (when the supplied address contains a unix socket address). + * + * @param address address to bind to + * @param options socket options to apply to each accepted socket + */ def bind( address: GenSocketAddress = SocketAddress.Wildcard, options: List[SocketOption] = Nil ): Resource[F, ServerSocket[F]] + /** Opens and binds a stream server socket to the supplied address and returns a stream of + * client sockets, each representing a client connection to the server. + * + * @param address address to bind to + * @param options socket options to apply to each accepted socket + */ def bindAndAccept( address: GenSocketAddress = SocketAddress.Wildcard, options: List[SocketOption] = Nil ): Stream[F, Socket[F]] - /** Creates a datagram socket bound to the specified address. + /** Opens and binds a datagram socket bound to the specified address. * * @param address address to bind to - * @param options socket options to apply to the underlying socket + * @param options socket options to apply to the socket */ def bindDatagramSocket( address: GenSocketAddress = SocketAddress.Wildcard, @@ -94,16 +119,6 @@ object Network extends NetworkCompanionPlatform { private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { - override def connect( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, Socket[F]] - - override def bind( - address: GenSocketAddress, - options: List[SocketOption] - ): Resource[F, ServerSocket[F]] - override def bindAndAccept( address: GenSocketAddress, options: List[SocketOption] diff --git a/site/io.md b/site/io.md index 90f2c4af90..b8514d2b17 100644 --- a/site/io.md +++ b/site/io.md @@ -25,7 +25,7 @@ The `fs2.io.net.Socket` trait provides mechanisms for reading and writing data - To get started, let's write a client program that connects to a server, sends a message, and reads a response. ```scala mdoc -import fs2.{Chunk, Stream} +import fs2.Chunk import fs2.io.net.Network import cats.effect.MonadCancelThrow import cats.effect.std.Console @@ -48,11 +48,10 @@ The `Network[F].connect` method returns a `Resource[F, Socket[F]]` which automat Note we aren't doing any binary message framing or packetization in this example. Hence, it's very possible for the single read to only receive a portion of the original message -- perhaps just the bytes for `"Hello, w"`. We can use FS2 streams to simplify this. The `Socket` trait defines stream operations -- `writes` and `reads`. We could rewrite this example using the stream operations like so: ```scala mdoc:reset -import fs2.{Chunk, Stream, text} +import fs2.{Stream, text} import fs2.io.net.Network import cats.effect.MonadCancelThrow import cats.effect.std.Console -import cats.syntax.all._ import com.comcast.ip4s._ def client[F[_]: MonadCancelThrow: Console: Network]: Stream[F, Unit] = @@ -172,26 +171,24 @@ The [fs2-chat](https://github.com/functional-streams-for-scala/fs2-chat) sample ## UDP -UDP support works much the same way as TCP. The `fs2.io.net.DatagramSocket` trait provides mechanisms for reading and writing UDP datagrams. UDP sockets are created via the `openDatagramSocket` method on `fs2.io.net.Network`. Unlike TCP, there's no differentiation between client and server sockets. Additionally, since UDP is a packet based protocol, read and write operations use `fs2.io.net.Datagram` values, which consist of a `Chunk[Byte]` and a `SocketAddress[IpAddress]`. +UDP support works much the same way as TCP. The `fs2.io.net.DatagramSocket` trait provides mechanisms for reading and writing UDP datagrams. UDP sockets are created via the `bindDatagramSocket` method on `fs2.io.net.Network`. Unlike TCP, there's no differentiation between client and server sockets. Adapting the TCP client example for UDP gives us the following: ```scala mdoc:reset import fs2.{Stream, text} -import fs2.io.net.{Datagram, Network} +import fs2.io.net.Network import cats.effect.Concurrent import cats.effect.std.Console import com.comcast.ip4s._ def client[F[_]: Concurrent: Console: Network]: F[Unit] = { val address = SocketAddress(ip"127.0.0.1", port"5555") - Stream.resource(Network[F].openDatagramSocket()).flatMap { socket => + Stream.resource(Network[F].bindDatagramSocket()).flatMap { socket => Stream("Hello, world!") .through(text.utf8.encode) .chunks - .map(data => Datagram(address, data)) - .through(socket.writes) - .drain ++ + .evalTap(data => socket.write(data, address)) ++ socket.reads .flatMap(datagram => Stream.chunk(datagram.bytes)) .through(text.utf8.decode) @@ -202,11 +199,11 @@ def client[F[_]: Concurrent: Console: Network]: F[Unit] = { } ``` -When writing, we map each chunk of bytes to a `Datagram`, which includes the destination address of the packet. When reading, we convert the `Stream[F, Datagram]` to a `Stream[F, Byte]` via `flatMap(datagram => Stream.chunk(datagram.bytes))`. Otherwise, the example is unchanged. +We call `socket.write`, supplying a chunk of bytes and the destination address. When reading, we convert the `Stream[F, Datagram]` to a `Stream[F, Byte]` via `flatMap(datagram => Stream.chunk(datagram.bytes))`. Otherwise, the example is unchanged. ```scala mdoc def echoServer[F[_]: Concurrent: Network]: F[Unit] = - Stream.resource(Network[F].openDatagramSocket(port = Some(port"5555"))).flatMap { socket => + Stream.resource(Network[F].bindDatagramSocket(SocketAddress.port(port"5555"))).flatMap { socket => socket.reads.through(socket.writes) }.compile.drain ``` From 4d2924c7bdfb31047a8b86e6efb01e86d4f134ac Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 9 Jun 2025 08:32:11 -0400 Subject: [PATCH 64/79] Scalafmt --- io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index 6f304fd2cd..46ee35bbed 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -68,7 +68,8 @@ object DatagramSocketOption { object MulticastInterface extends Key[String] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = Sync[F].delay(sock.setMulticastInterface(value)) - override private[net] def toSocketOption: SocketOption.Key[String] = SocketOption.MulticastInterface + override private[net] def toSocketOption: SocketOption.Key[String] = + SocketOption.MulticastInterface } object MulticastLoopback extends Key[Boolean] { From 855070ec7296631d52a2519ddb74481cc9fbf72a Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 9 Jun 2025 09:24:52 -0400 Subject: [PATCH 65/79] Scalafmt --- build.sbt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.sbt b/build.sbt index 6e0ed770b7..e132f09b95 100644 --- a/build.sbt +++ b/build.sbt @@ -344,6 +344,9 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.SocketGroup$"), ProblemFilters.exclude[ReversedMissingMethodProblem]( "fs2.io.net.SocketOptionCompanionPlatform#Key.fs2$io$net$SocketOptionCompanionPlatform$Key$$$outer" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]( + "fs2.io.net.DatagramSocketOption#Key.toSocketOption" ) ) From c4c9ee34681367d9d41a62a6aa59d6f95c3ac4fb Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Sun, 22 Jun 2025 17:01:59 -0400 Subject: [PATCH 66/79] Add support for ip4s NetworkInterface --- build.sbt | 2 +- .../test/scala/fs2/io/net/udp/UdpSuite.scala | 54 +++++++++++----- .../scala/fs2/io/internal/facade/dgram.scala | 4 +- .../scala/fs2/io/internal/facade/os.scala | 11 ---- .../fs2/io/net/DatagramSocketOption.scala | 15 +++-- .../fs2/io/net/DatagramSocketPlatform.scala | 63 ++++++++++++++++--- .../fs2/io/net/SocketOptionPlatform.scala | 28 +++++++-- .../fs2/io/net/udp/UdpSuitePlatform.scala | 42 ------------- .../fs2/io/net/SocketOptionPlatform.scala | 7 ++- .../net/AsyncIpDatagramSocketsProvider.scala | 15 +++-- .../fs2/io/net/DatagramSocketPlatform.scala | 2 +- .../net/JnrUnixDatagramSocketsProvider.scala | 17 ++++- .../scala/fs2/io/net/NetworkPlatform.scala | 2 +- .../scala/fs2/io/net/tls/DTLSSocket.scala | 8 ++- .../fs2/io/net/udp/UdpSuitePlatform.scala | 40 ------------ .../fs2/io/net/DatagramSocketPlatform.scala | 1 + .../scala/fs2/io/net/DatagramSocket.scala | 11 ++++ .../src/main/scala/fs2/io/net/Network.scala | 11 ++++ project/plugins.sbt | 2 +- 19 files changed, 195 insertions(+), 140 deletions(-) delete mode 100644 io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala delete mode 100644 io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala diff --git a/build.sbt b/build.sbt index e132f09b95..3382245050 100644 --- a/build.sbt +++ b/build.sbt @@ -428,7 +428,7 @@ lazy val io = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "fs2-io", tlVersionIntroduced ~= { _.updated("3", "3.1.0") }, - libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.7.0", + libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.8.0-M3", tlJdkRelease := None ) .jvmSettings( diff --git a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala index e40915f0a2..25993458a5 100644 --- a/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala +++ b/io/js-jvm/src/test/scala/fs2/io/net/udp/UdpSuite.scala @@ -31,7 +31,7 @@ import com.comcast.ip4s._ import scala.concurrent.duration._ -class UdpSuite extends Fs2Suite with UdpSuitePlatform { +class UdpSuite extends Fs2Suite { private def sendAndReceive(socket: DatagramSocket[IO], toSend: Datagram): IO[Datagram] = socket .write(toSend) >> socket.read.timeoutTo(1.second, IO.defer(sendAndReceive(socket, toSend))) @@ -114,25 +114,45 @@ class UdpSuite extends Fs2Suite with UdpSuitePlatform { val group = mip"239.10.10.10" val groupJoin = MulticastJoin.asm(group) val msg = Chunk.array("Hello, world!".getBytes) + val outgoingInterface = + // Get first non-loopback interface with an IPv4 address + Network[IO].interfaces.getAll.map { interfaces => + interfaces.values + .filterNot(_.isLoopback) + .flatMap(iface => + iface.addresses.filter(_.address.fold(_ => true, _ => false)).as(iface) + ) + .head + } Stream - .resource( - Network[IO] - .bindDatagramSocket( - options = List(SocketOption.multicastTtl(1)) + .eval(outgoingInterface) + .flatMap { out => + Stream + .resource( + Network[IO] + .bindDatagramSocket( + options = List(SocketOption.multicastTtl(1), SocketOption.multicastInterface(out)) + ) + .evalMap { serverSocket => + Network[IO].interfaces.getAll.flatMap { interfaces => + interfaces.values.toList + .filter(iface => + iface.addresses.exists(_.address.fold(_ => true, _ => false)) + ) + .traverse_(iface => serverSocket.join(groupJoin, iface)) + .as(serverSocket) + } + } ) - .evalMap { serverSocket => - v4Interfaces - .traverse_(interface => serverSocket.join(groupJoin, interface)) - .as(serverSocket) + .flatMap { serverSocket => + val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) + val client = + Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => + val to = SocketAddress(group.address, serverSocket.address.asIpUnsafe.port) + Stream.eval(clientSocket.write(msg, to) >> clientSocket.read) + } + client.concurrently(server) } - ) - .flatMap { serverSocket => - val server = serverSocket.reads.foreach(packet => serverSocket.write(packet)) - val client = Stream.resource(Network[IO].bindDatagramSocket()).flatMap { clientSocket => - val to = SocketAddress(group.address, serverSocket.address.asIpUnsafe.port) - Stream.eval(clientSocket.write(msg, to) >> clientSocket.read) - } - client.concurrently(server) } .compile .lastOrError diff --git a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala index 8bfb0cd0d7..7ee4299ca4 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/dgram.scala @@ -97,7 +97,7 @@ private[io] object dgram { @js.native trait AddressInfo extends js.Object { def address: String = js.native - def family: Int = js.native + def family: String = js.native def port: Int = js.native } @@ -109,7 +109,7 @@ private[io] object dgram { @js.native trait RemoteInfo extends js.Object { def address: String = js.native - def family: Int = js.native + def family: String = js.native def port: Int = js.native def size: Int = js.native } diff --git a/io/js/src/main/scala/fs2/io/internal/facade/os.scala b/io/js/src/main/scala/fs2/io/internal/facade/os.scala index 4dd9fb4afa..e1cb0241ac 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/os.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/os.scala @@ -38,18 +38,7 @@ private[io] object os { @JSImport("os", "type") def `type`(): String = js.native - @js.native - @JSImport("os", "networkInterfaces") - def networkInterfaces(): js.Dictionary[js.Array[NetworkInterfaceInfo]] = js.native - @js.native @JSImport("os", "EOL") def EOL: String = js.native - - @js.native - trait NetworkInterfaceInfo extends js.Object { - def family: String = js.native - def address: String = js.native - } - } diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala index 46ee35bbed..b34f575393 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketOption.scala @@ -24,6 +24,7 @@ package io package net import cats.effect.kernel.Sync +import com.comcast.ip4s.NetworkInterface import fs2.io.internal.facade /** Specifies a socket option on a TCP/UDP socket. @@ -65,10 +66,13 @@ object DatagramSocketOption { override private[net] def toSocketOption: SocketOption.Key[Boolean] = SocketOption.Broadcast } - object MulticastInterface extends Key[String] { - override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = - Sync[F].delay(sock.setMulticastInterface(value)) - override private[net] def toSocketOption: SocketOption.Key[String] = + object MulticastInterface extends Key[NetworkInterface] { + override private[net] def set[F[_]: Sync]( + sock: facade.dgram.Socket, + value: NetworkInterface + ): F[Unit] = + SocketOption.MulticastInterface.set(sock, value) + override private[net] def toSocketOption: SocketOption.Key[NetworkInterface] = SocketOption.MulticastInterface } @@ -116,7 +120,8 @@ object DatagramSocketOption { } def broadcast(value: Boolean): DatagramSocketOption = apply(Broadcast, value) - def multicastInterface(value: String): DatagramSocketOption = apply(MulticastInterface, value) + def multicastInterface(value: NetworkInterface): DatagramSocketOption = + apply(MulticastInterface, value) def multicastLoopback(value: Boolean): DatagramSocketOption = apply(MulticastLoopback, value) def multicastTtl(value: Int): DatagramSocketOption = apply(MulticastTtl, value) def receiveBufferSize(value: Int): DatagramSocketOption = apply(ReceiveBufferSize, value) diff --git a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index ff38881d99..8135b6675f 100644 --- a/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -31,13 +31,16 @@ import cats.effect.std.Dispatcher import cats.effect.std.Queue import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.AnySourceMulticastJoin -import com.comcast.ip4s.GenSocketAddress -import com.comcast.ip4s.IpAddress -import com.comcast.ip4s.MulticastJoin -import com.comcast.ip4s.Port -import com.comcast.ip4s.SocketAddress -import com.comcast.ip4s.SourceSpecificMulticastJoin +import com.comcast.ip4s.{ + AnySourceMulticastJoin, + GenSocketAddress, + IpAddress, + MulticastJoin, + NetworkInterface => Ip4sNetworkInterface, + Port, + SocketAddress, + SourceSpecificMulticastJoin +} import fs2.io.internal.facade import scala.scalajs.js @@ -48,6 +51,8 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { + + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") type NetworkInterface = String private[net] def forAsync[F[_]]( @@ -142,7 +147,49 @@ private[net] trait DatagramSocketCompanionPlatform { override def join( join: MulticastJoin[IpAddress], - interface: NetworkInterface + interface: Ip4sNetworkInterface + ): F[GroupMembership] = F + .delay { + val interfaceAddress = interface.addresses + .collectFirst { case c if c.address.fold(_ => true, _ => false) => c.address } + .getOrElse( + throw new IllegalArgumentException("specified interface does not have ipv4 address") + ) + .toString + join match { + case AnySourceMulticastJoin(group) => + sock.addMembership(group.address.toString, interfaceAddress) + case SourceSpecificMulticastJoin(source, group) => + sock.addSourceSpecificMembership( + source.toString, + group.address.toString, + interfaceAddress + ) + } + interfaceAddress + } + .map { interfaceAddress => + new GroupMembership { + + override def drop: F[Unit] = F.delay { + join match { + case AnySourceMulticastJoin(group) => + sock.dropMembership(group.address.toString, interfaceAddress) + case SourceSpecificMulticastJoin(source, group) => + sock.dropSourceSpecificMembership( + source.toString, + group.address.toString, + interfaceAddress + ) + } + } + } + } + + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") + override def join( + join: MulticastJoin[IpAddress], + interface: DatagramSocket.NetworkInterface ): F[GroupMembership] = F .delay { join match { diff --git a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index c187344c7c..adafad2b07 100644 --- a/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/js/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -22,6 +22,7 @@ package fs2.io.net import cats.effect.kernel.Sync +import com.comcast.ip4s.NetworkInterface import fs2.io.internal.facade import scala.annotation.nowarn @@ -118,11 +119,30 @@ private[net] trait SocketOptionCompanionPlatform { self: SocketOption.type => } def broadcast(value: Boolean): SocketOption = apply(Broadcast, value) - object MulticastInterface extends Key[String] { - override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: String): F[Unit] = - Sync[F].delay(sock.setMulticastInterface(value)) + object MulticastInterface extends Key[NetworkInterface] { + override private[net] def set[F[_]: Sync]( + sock: facade.dgram.Socket, + value: NetworkInterface + ): F[Unit] = + Sync[F].delay { + val mi = sock.address().family match { + case "IPv4" => + value.addresses + .collectFirst { + case c if c.address.fold(_ => true, _ => false) => c.address.toString + } + .getOrElse( + throw new IllegalArgumentException( + "socket is IPv4 but specified interface does not have an IPv4 address" + ) + ) + case "IPv6" => "::%" + value.name + case other => throw new IllegalStateException(s"unexpected socket family: $other") + } + sock.setMulticastInterface(mi) + } } - def multicastInterface(value: String): SocketOption = apply(MulticastInterface, value) + def multicastInterface(value: NetworkInterface): SocketOption = apply(MulticastInterface, value) object MulticastLoop extends Key[Boolean] { override private[net] def set[F[_]: Sync](sock: facade.dgram.Socket, value: Boolean): F[Unit] = diff --git a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala b/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala deleted file mode 100644 index bc2cde011e..0000000000 --- a/io/js/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net -package udp - -import fs2.io.internal.facade - -trait UdpSuitePlatform extends Fs2Suite { - - val v4Interfaces = facade.os - .networkInterfaces() - .toMap - .collect { - case (_, addresses) if addresses.exists(_.family == "IPv4") => - addresses.collectFirst { case a if a.family == "IPv4" => a.address }.get - } - .toList - - val v4ProtocolFamily = "udp4" - -} diff --git a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala index bb5379c428..d188bf0937 100644 --- a/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/net/SocketOptionPlatform.scala @@ -25,7 +25,9 @@ import java.net.{SocketOption => JSocketOption} import java.net.StandardSocketOptions import java.lang.{Boolean => JBoolean, Integer => JInt} -import java.net.NetworkInterface +import java.net.{NetworkInterface => JNetworkInterface} + +import com.comcast.ip4s.NetworkInterface private[net] trait SocketOptionCompanionPlatform { type Key[A] = JSocketOption[A] @@ -38,6 +40,9 @@ private[net] trait SocketOptionCompanionPlatform { val MulticastInterface = StandardSocketOptions.IP_MULTICAST_IF def multicastInterface(value: NetworkInterface): SocketOption = + SocketOption(MulticastInterface, JNetworkInterface.getByName(value.name)) + + def multicastInterface(value: JNetworkInterface): SocketOption = SocketOption(MulticastInterface, value) val MulticastLoop = StandardSocketOptions.IP_MULTICAST_LOOP diff --git a/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala index e98fb238f6..b510e958d4 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncIpDatagramSocketsProvider.scala @@ -27,13 +27,13 @@ import java.net.InetSocketAddress import cats.syntax.all._ import cats.effect.{Async, Resource} -import com.comcast.ip4s.{Dns, Host, SocketAddress} +import com.comcast.ip4s.{Dns, Host, NetworkInterface, SocketAddress} import fs2.internal.ThreadFactories import java.net.StandardProtocolFamily import java.nio.channels.DatagramChannel import com.comcast.ip4s.* -import java.net.NetworkInterface +import java.net.{NetworkInterface => JNetworkInterface} import CollectionCompat.* private[net] object AsyncIpDatagramSocketsProvider { @@ -131,10 +131,11 @@ private[net] object AsyncIpDatagramSocketsProvider { interface: NetworkInterface ): F[GroupMembership] = Async[F].delay { + val jinterface = JNetworkInterface.getByName(interface.name) val membership = join.fold( - j => channel.join(j.group.address.toInetAddress, interface), + j => channel.join(j.group.address.toInetAddress, jinterface), j => - channel.join(j.group.address.toInetAddress, interface, j.source.toInetAddress) + channel.join(j.group.address.toInetAddress, jinterface, j.source.toInetAddress) ) new GroupMembership { def drop = Async[F].delay(membership.drop) @@ -146,6 +147,12 @@ private[net] object AsyncIpDatagramSocketsProvider { } } + override def join( + j: MulticastJoin[IpAddress], + interface: JNetworkInterface + ): F[GroupMembership] = + join(j, NetworkInterface.fromJava(interface)) + override def supportedOptions: F[Set[SocketOption.Key[?]]] = Async[F].delay { channel.supportedOptions.asScala.toSet diff --git a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index 6d8d9b810f..f572640888 100644 --- a/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -37,6 +37,6 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { - // TODO deprecate and replace with real cross-platform type + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") type NetworkInterface = java.net.NetworkInterface } diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index e42bbf3b0c..6aed5a1ae1 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -28,7 +28,13 @@ import cats.effect.std.Mutex import cats.effect.syntax.all._ import cats.syntax.all._ -import com.comcast.ip4s.{GenSocketAddress, IpAddress, MulticastJoin, UnixSocketAddress} +import com.comcast.ip4s.{ + GenSocketAddress, + IpAddress, + MulticastJoin, + NetworkInterface, + UnixSocketAddress +} import fs2.io.file.Files @@ -173,6 +179,15 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 override def writes = _.foreach(write) + override def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ) = + F.raiseError( + new UnsupportedOperationException("Multicast not supported on unix datagram sockets") + ) + + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") override def join( join: MulticastJoin[IpAddress], interface: DatagramSocket.NetworkInterface diff --git a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala index 03241c6407..f7ff010860 100644 --- a/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/NetworkPlatform.scala @@ -83,7 +83,7 @@ private[net] trait NetworkCompanionPlatform extends NetworkLowPriority { self: N private def tryGetSelector = IO.pollers.map(_.collectFirst { case selector: Selector => selector }).to[F] - private implicit def dns: Dns[F] = Dns.forAsync[F] + private implicit def dnsInstance: Dns[F] = dns private def selecting[A]( ifSelecting: SelectingIpSocketsProvider[F] => Resource[F, A], diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala index 16dc1c2dbb..b75167ed5a 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/DTLSSocket.scala @@ -24,7 +24,7 @@ package io package net package tls -import java.net.NetworkInterface +import java.net.{NetworkInterface => JNetworkInterface} import cats.Applicative import cats.effect.kernel.{Async, Resource, Sync} @@ -110,6 +110,12 @@ object DTLSSocket { ): F[GroupMembership] = Sync[F].raiseError(new RuntimeException("DTLSSocket does not support multicast")) + override def join( + join: MulticastJoin[IpAddress], + interface: JNetworkInterface + ): F[GroupMembership] = + Sync[F].raiseError(new RuntimeException("DTLSSocket does not support multicast")) + override def supportedOptions = socket.supportedOptions override def getOption[A](key: SocketOption.Key[A]) = socket.getOption(key) override def setOption[A](key: SocketOption.Key[A], value: A) = socket.setOption(key, value) diff --git a/io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala b/io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala deleted file mode 100644 index 3bc6df08ea..0000000000 --- a/io/jvm/src/test/scala/fs2/io/net/udp/UdpSuitePlatform.scala +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2 -package io -package net -package udp - -import java.net.{Inet4Address, NetworkInterface, StandardProtocolFamily} - -import CollectionCompat._ - -trait UdpSuitePlatform extends Fs2Suite { - - val v4Interfaces = - NetworkInterface.getNetworkInterfaces.asScala.toList.filter { interface => - interface.getInetAddresses.asScala.exists(_.isInstanceOf[Inet4Address]) - } - - val v4ProtocolFamily = StandardProtocolFamily.INET - -} diff --git a/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala index a25fc1ea0b..f572640888 100644 --- a/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala +++ b/io/native/src/main/scala/fs2/io/net/DatagramSocketPlatform.scala @@ -37,5 +37,6 @@ private[net] trait DatagramSocketPlatform[F[_]] { } private[net] trait DatagramSocketCompanionPlatform { + @deprecated("Use com.comcast.ip4s.NetworkInterface", "3.13.0") type NetworkInterface = java.net.NetworkInterface } diff --git a/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala index f92d0be8dc..e3161e5f4f 100644 --- a/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala +++ b/io/shared/src/main/scala/fs2/io/net/DatagramSocket.scala @@ -89,6 +89,17 @@ trait DatagramSocket[F[_]] extends DatagramSocketPlatform[F] { * @param join group to join * @param interface network interface upon which to listen for datagrams */ + def join( + join: MulticastJoin[IpAddress], + interface: NetworkInterface + ): F[GroupMembership] + + /** Joins a multicast group on a specific network interface. + * + * @param join group to join + * @param interface network interface upon which to listen for datagrams + */ + @deprecated("Use overload that takes a com.comcast.ip4s.NetworkInterface", "3.13.0") def join( join: MulticastJoin[IpAddress], interface: DatagramSocket.NetworkInterface diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 2bb21e2cc6..11e463bff5 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -26,10 +26,12 @@ package net import cats.ApplicativeThrow import cats.effect.{Async, IO, Resource} import com.comcast.ip4s.{ + Dns, GenSocketAddress, Host, IpAddress, Ipv4Address, + NetworkInterfaces, Port, SocketAddress, UnixSocketAddress @@ -110,6 +112,12 @@ sealed trait Network[F[_]] * For example, `Network[IO].tlsContext.system` returns a `F[TLSContext[F]]`. */ def tlsContext: TLSContext.Builder[F] + + /** Explicit instance of `Dns[F]`. */ + def dns: Dns[F] + + /** Explicit instance of `NetworkInterfaces[F]`. */ + def interfaces: NetworkInterfaces[F] } object Network extends NetworkCompanionPlatform { @@ -119,6 +127,9 @@ object Network extends NetworkCompanionPlatform { private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { + def dns: Dns[F] = Dns.forAsync(F) + def interfaces: NetworkInterfaces[F] = NetworkInterfaces.forAsync(F) + override def bindAndAccept( address: GenSocketAddress, options: List[SocketOption] diff --git a/project/plugins.sbt b/project/plugins.sbt index ba8d2a7247..ef50d5cacb 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ val sbtTypelevelVersion = "0.7.7" addSbtPlugin("org.typelevel" % "sbt-typelevel" % sbtTypelevelVersion) addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % sbtTypelevelVersion) -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("com.armanbilge" % "sbt-scala-native-config-brew-github-actions" % "0.3.0") addSbtPlugin("io.github.sbt-doctest" % "sbt-doctest" % "0.11.1") From 562cbc5558df152f79e40bab8c28d5078b76df01 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Sun, 22 Jun 2025 17:24:45 -0400 Subject: [PATCH 67/79] Mima --- build.sbt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 3382245050..0f2a503f23 100644 --- a/build.sbt +++ b/build.sbt @@ -347,7 +347,13 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( ), ProblemFilters.exclude[ReversedMissingMethodProblem]( "fs2.io.net.DatagramSocketOption#Key.toSocketOption" - ) + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.DatagramSocket.join"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "fs2.io.net.DatagramSocketOption.multicastInterface" + ), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.dns"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.interfaces") ) lazy val root = tlCrossRootProject From e8721b0fe1e564d8eba16319c4352b2782b7622a Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 26 Jun 2025 08:36:16 -0400 Subject: [PATCH 68/79] Bump to ip4s 3.8.0-RC1 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 0f2a503f23..c6707c5ad6 100644 --- a/build.sbt +++ b/build.sbt @@ -434,7 +434,7 @@ lazy val io = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings( name := "fs2-io", tlVersionIntroduced ~= { _.updated("3", "3.1.0") }, - libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.8.0-M3", + libraryDependencies += "com.comcast" %%% "ip4s-core" % "3.8.0-RC1", tlJdkRelease := None ) .jvmSettings( From 05e5e9927fe611791eac764f4004a5adfe1fa0ec Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 26 Jun 2025 08:36:56 -0400 Subject: [PATCH 69/79] Bump to ip4s 3.8.0-RC1 --- io/shared/src/main/scala/fs2/io/net/Network.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index 11e463bff5..d673bd9559 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -128,7 +128,7 @@ object Network extends NetworkCompanionPlatform { private[fs2] abstract class AsyncNetwork[F[_]](implicit F: Async[F]) extends Network[F] { def dns: Dns[F] = Dns.forAsync(F) - def interfaces: NetworkInterfaces[F] = NetworkInterfaces.forAsync(F) + def interfaces: NetworkInterfaces[F] = NetworkInterfaces.forSync(F) override def bindAndAccept( address: GenSocketAddress, From 6e49f13909be1813604c869ea3717e94fc9b2069 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 25 Aug 2025 09:45:04 -0400 Subject: [PATCH 70/79] Scalafmt --- io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala | 2 +- .../main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala | 2 +- io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala | 2 +- io/shared/src/main/scala/fs2/io/net/Network.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala index 073bc7fa2d..11aa99ead6 100644 --- a/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/AsyncUnixSocketsProvider.scala @@ -67,7 +67,7 @@ private[net] abstract class AsyncUnixSocketsProvider[F[_]: Files](implicit F: As Stream .resource(accept.attempt) .flatMap { - case Left(_) => Stream.empty[F] + case Left(_) => Stream.empty[F] case Right(accepted) => Stream.eval( AsyncUnixSocketsProvider.makeSocket(accepted, address, UnixSocketAddress("")) diff --git a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala index 6aed5a1ae1..879491f213 100644 --- a/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala +++ b/io/jvm/src/main/scala/fs2/io/net/JnrUnixDatagramSocketsProvider.scala @@ -107,7 +107,7 @@ private[net] class JnrUnixDatagramSocketsProvider[F[_]](implicit F: Async[F], F2 )(f: UnixSocketAddress => A): A = address match { case u: UnixSocketAddress => f(u) - case _ => + case _ => throw new IllegalArgumentException( s"Unsupported address type $address; must pass a UnixSocketAddress" ) diff --git a/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala index 7a2d45128e..cdb81d82d1 100644 --- a/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala +++ b/io/jvm/src/main/scala/fs2/io/net/SocketAddressHelpers.scala @@ -32,7 +32,7 @@ private[net] object SocketAddressHelpers { def toGenSocketAddress(address: JSocketAddress): GenSocketAddress = address match { case addr: InetSocketAddress => SocketAddress.fromInetSocketAddress(addr) - case _ => + case _ => if (JdkUnixSocketsProvider.supported && address.isInstanceOf[UnixDomainSocketAddress]) { UnixSocketAddress(address.asInstanceOf[UnixDomainSocketAddress].getPath.toString) } else if (JnrUnixSocketsProvider.supported && address.isInstanceOf[JnrUnixSocketAddress]) { diff --git a/io/shared/src/main/scala/fs2/io/net/Network.scala b/io/shared/src/main/scala/fs2/io/net/Network.scala index d673bd9559..7116cd89da 100644 --- a/io/shared/src/main/scala/fs2/io/net/Network.scala +++ b/io/shared/src/main/scala/fs2/io/net/Network.scala @@ -146,7 +146,7 @@ object Network extends NetworkCompanionPlatform { address match { case sa: SocketAddress[Host] => ifIp(sa) case ua: UnixSocketAddress => ifUnix(ua) - case other => + case other => ApplicativeThrow[G].raiseError( new UnsupportedOperationException(s"Unsupported address type: $other") ) From 02d73e537845e717ede8dd60e1f94ce3a986fb26 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Mon, 25 Aug 2025 10:22:49 -0400 Subject: [PATCH 71/79] Downgrade GHA runner for macos to fix multicast tests --- .github/workflows/ci.yml | 2 +- build.sbt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7977b8fd60..108a689b2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: name: Test I/O on macOS strategy: matrix: - os: [macos-latest] + os: [macos-14] java: [temurin@17] project: [ioJS, ioJVM, ioNative] runs-on: ${{ matrix.os }} diff --git a/build.sbt b/build.sbt index 2a870b08d6..566c735ad8 100644 --- a/build.sbt +++ b/build.sbt @@ -34,7 +34,9 @@ ThisBuild / githubWorkflowAddedJobs += scalas = Nil, sbtStepPreamble = Nil, javas = List(githubWorkflowJavaVersions.value.head), - oses = List("macos-latest"), + oses = List( + "macos-14" + ), // FIXME: macos-15 breaks sending multicast to local network - https://github.com/actions/runner-images/issues/10924 matrixAdds = Map("project" -> List("ioJS", "ioJVM", "ioNative")), steps = githubWorkflowJobSetup.value.toList ++ List( WorkflowStep.Run(List("brew install s2n"), cond = Some("matrix.project == 'ioNative'")), From de6e9556da5a780544108358970ae0a8e455e835 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 26 Aug 2025 07:09:55 -0400 Subject: [PATCH 72/79] Address deprecation warnings --- io/shared/src/test/scala/fs2/io/net/SocketSuite.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index c1e426c7da..3ff14b3b07 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -270,9 +270,9 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { test("sockets are released at the end of the resource scope") { val f = - Network[IO].serverResource(port = Some(port"9071")).use { case (bindAddress, clients) => - clients.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { - Network[IO].client(bindAddress).use { client => + Network[IO].bind(SocketAddress.port(port"9071")).use { serverSocket => + serverSocket.accept.foreach(_ => IO.sleep(1.second)).compile.drain.background.surround { + Network[IO].connect(serverSocket.address).use { client => client.read(1).assertEquals(None) } } From fe722ce0d0ef2cf29752039d2d0c295566d509e8 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 08:10:28 -0400 Subject: [PATCH 73/79] Update fromKeyStoreFile to take a Path and improve error message from fromKeyStoreResource --- build.sbt | 8 +++++++- .../fs2/io/net/tls/TLSContextPlatform.scala | 20 ++++++++++++++++--- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 17 ++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 566c735ad8..3399baf2b2 100644 --- a/build.sbt +++ b/build.sbt @@ -355,7 +355,13 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq( "fs2.io.net.DatagramSocketOption.multicastInterface" ), ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.dns"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.interfaces") + ProblemFilters.exclude[ReversedMissingMethodProblem]("fs2.io.net.Network.interfaces"), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSContext#Builder.fromKeyStoreFile" + ), + ProblemFilters.exclude[InheritedNewAbstractMethodProblem]( + "fs2.io.net.tls.TLSContext#Builder.fs2$io$net$tls$TLSContextCompanionPlatform$BuilderPlatform$$$outer" + ) ) lazy val root = tlCrossRootProject diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala index c0dd0969c9..8d301331e9 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSContextPlatform.scala @@ -40,7 +40,9 @@ import cats.syntax.all._ import com.comcast.ip4s.{IpAddress, SocketAddress} import java.util.function.BiFunction -import java.nio.file.Path +import java.nio.file.{Path => JPath} + +import fs2.io.file.Path private[tls] trait TLSContextPlatform[F[_]] { @@ -104,6 +106,14 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => def insecure: F[TLSContext[F]] /** Creates a `TLSContext` from the specified key store file. */ + @deprecated("Use overload that takes an fs2.io.file.Path instead", "3.13.0") + def fromKeyStoreFile( + file: JPath, + storePassword: Array[Char], + keyPassword: Array[Char] + ): F[TLSContext[F]] = + fromKeyStoreFile(Path.fromNioPath(file), storePassword, keyPassword) + def fromKeyStoreFile( file: Path, storePassword: Array[Char], @@ -275,7 +285,7 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => storePassword: Array[Char], keyPassword: Array[Char] ): F[TLSContext[F]] = { - val load = Async[F].blocking(new FileInputStream(file.toFile): InputStream) + val load = Async[F].blocking(new FileInputStream(file.toNioPath.toFile): InputStream) val stream = Resource.make(load)(s => Async[F].blocking(s.close)) fromKeyStoreStream(stream, storePassword, keyPassword) } @@ -285,7 +295,11 @@ private[tls] trait TLSContextCompanionPlatform { self: TLSContext.type => storePassword: Array[Char], keyPassword: Array[Char] ): F[TLSContext[F]] = { - val load = Async[F].blocking(getClass.getClassLoader.getResourceAsStream(resource)) + val load = Async[F].blocking { + val s = getClass.getClassLoader.getResourceAsStream(resource) + if (s eq null) throw new IOException(s"Classpath resource not found [$resource]") + else s + } val stream = Resource.make(load)(s => Async[F].blocking(s.close)) fromKeyStoreStream(stream, storePassword, keyPassword) } diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 66af8da877..2b1f8b1ded 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -32,9 +32,12 @@ import cats.effect.{IO, Resource} import cats.syntax.all._ import com.comcast.ip4s._ +import java.io.FileNotFoundException import java.security.Security import javax.net.ssl.SSLHandshakeException +import fs2.io.file.Path + class TLSSocketSuite extends TLSSuite { val size = 8192 @@ -217,4 +220,18 @@ class TLSSocketSuite extends TLSSuite { .assertEquals(msg) } } + + group("TLSContextBuilder") { + test("fromKeyStoreResource - not found") { + Network[IO].tlsContext + .fromKeyStoreResource("does-not-exist.jks", Array.empty, Array.empty) + .intercept[IOException] + } + + test("fromKeyStoreFile - not found") { + Network[IO].tlsContext + .fromKeyStoreFile(Path("does-not-exist.jks"), Array.empty, Array.empty) + .intercept[FileNotFoundException] + } + } } From 17f68523336933e0fa0d39604978a36519226bde Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 08:24:57 -0400 Subject: [PATCH 74/79] Fix 2.12 compilation --- io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 2b1f8b1ded..cdc0251a0d 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -224,13 +224,13 @@ class TLSSocketSuite extends TLSSuite { group("TLSContextBuilder") { test("fromKeyStoreResource - not found") { Network[IO].tlsContext - .fromKeyStoreResource("does-not-exist.jks", Array.empty, Array.empty) + .fromKeyStoreResource("does-not-exist.jks", Array.empty[Char], Array.empty[Char]) .intercept[IOException] } test("fromKeyStoreFile - not found") { Network[IO].tlsContext - .fromKeyStoreFile(Path("does-not-exist.jks"), Array.empty, Array.empty) + .fromKeyStoreFile(Path("does-not-exist.jks"), Array.empty[Char], Array.empty[Char]) .intercept[FileNotFoundException] } } From 01f139d24248b609d076d77755219b525df2fbb3 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 21:35:28 -0400 Subject: [PATCH 75/79] Fix spinloop bug in TLSEngine --- .../main/scala/fs2/io/net/tls/TLSEngine.scala | 4 +- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala b/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala index 8829f56e69..cb7a4bdf23 100644 --- a/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala +++ b/io/jvm/src/main/scala/fs2/io/net/tls/TLSEngine.scala @@ -215,9 +215,7 @@ private[tls] object TLSEngine { else binding.read(engine.getSession.getPacketBufferSize).flatMap { case Some(c) => unwrapBuffer.input(c) >> unwrapHandshake - case None => - unwrapBuffer.inputRemains - .flatMap(x => if (x > 0) Applicative[F].unit else stopUnwrap) + case None => stopUnwrap } } case SSLEngineResult.HandshakeStatus.NEED_UNWRAP_AGAIN => diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index cdc0251a0d..80d1664839 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -219,6 +219,65 @@ class TLSSocketSuite extends TLSSuite { .to(Chunk) .assertEquals(msg) } + + test("endOfOutput during handshake results in termination".only) { + val msg = Chunk.array(("Hello, world! " * 20000).getBytes) + + def limitWrites(raw: Socket[IO], limit: Int): Socket[IO] = new Socket[IO] { + def endOfInput = raw.endOfInput + def endOfOutput = raw.endOfOutput + @deprecated("", "") + def isOpen = raw.isOpen + @deprecated("", "") + def localAddress = raw.localAddress + def peerAddress = raw.peerAddress + def read(maxBytes: Int) = raw.read(maxBytes) + def readN(numBytes: Int) = raw.readN(numBytes) + def reads = raw.reads + @deprecated("", "") + def remoteAddress = raw.remoteAddress + def writes = raw.writes + + def address = raw.address + def getOption[A](key: SocketOption.Key[A]) = raw.getOption(key) + def setOption[A](key: SocketOption.Key[A], value: A) = raw.setOption(key, value) + def supportedOptions = raw.supportedOptions + + private var totalWritten: Long = 0 + def write(bytes: Chunk[Byte]) = + if (totalWritten >= limit) endOfOutput + else { + val b = bytes.take(limit) + raw.write(b) >> IO(totalWritten += b.size) + } + } + + val setup = for { + tlsContext <- Resource.eval(testTlsContext) + serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + client <- Network[IO].connect(serverSocket.address).flatMap { rawClient => + tlsContext.clientBuilder(rawClient).withLogger(logger).build + } + } yield serverSocket.accept + .flatMap(s => Stream.resource(tlsContext.server(limitWrites(s, 100)))) -> client + + Stream + .resource(setup) + .flatMap { case (server, clientSocket) => + val echoServer = server.map { socket => + socket.reads.chunks.foreach(socket.write(_)) + }.parJoinUnbounded + + val client = + Stream.exec(clientSocket.write(msg)).onFinalize(clientSocket.endOfOutput) ++ + clientSocket.reads.take(msg.size.toLong) + + client.concurrently(echoServer) + } + .compile + .drain + .intercept[javax.net.ssl.SSLException] + } } group("TLSContextBuilder") { From 57ab1862693c96789a42d7899ae3dfa5ffa6d477 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 21:41:38 -0400 Subject: [PATCH 76/79] Drop .only tag --- io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 80d1664839..0cdbf07e3c 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -220,7 +220,7 @@ class TLSSocketSuite extends TLSSuite { .assertEquals(msg) } - test("endOfOutput during handshake results in termination".only) { + test("endOfOutput during handshake results in termination") { val msg = Chunk.array(("Hello, world! " * 20000).getBytes) def limitWrites(raw: Socket[IO], limit: Int): Socket[IO] = new Socket[IO] { From ea03a6e31bca45c8c363a2225909d9360ed35a5a Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 21:50:45 -0400 Subject: [PATCH 77/79] Change byte limit in test --- io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 0cdbf07e3c..84f3449f09 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -259,7 +259,7 @@ class TLSSocketSuite extends TLSSuite { tlsContext.clientBuilder(rawClient).withLogger(logger).build } } yield serverSocket.accept - .flatMap(s => Stream.resource(tlsContext.server(limitWrites(s, 100)))) -> client + .flatMap(s => Stream.resource(tlsContext.server(limitWrites(s, 20)))) -> client Stream .resource(setup) From 1d91556030a24a94550a2681bd330750f9a5754e Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Tue, 2 Sep 2025 21:58:56 -0400 Subject: [PATCH 78/79] Change explicit intercept to attempt to handle behavior differences on various platforms --- io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 84f3449f09..7cc29f4ae0 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -276,7 +276,7 @@ class TLSSocketSuite extends TLSSuite { } .compile .drain - .intercept[javax.net.ssl.SSLException] + .attempt } } From fad230ecb662658a653255dd05aae1267effa8b8 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Wed, 3 Sep 2025 21:03:10 -0400 Subject: [PATCH 79/79] Rewrote test to be an example of a client that only sends partial handshake to peg a server cpu --- .../scala/fs2/io/net/tls/TLSSocketSuite.scala | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala index 7cc29f4ae0..459f2d0adf 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSocketSuite.scala @@ -243,31 +243,36 @@ class TLSSocketSuite extends TLSSuite { def setOption[A](key: SocketOption.Key[A], value: A) = raw.setOption(key, value) def supportedOptions = raw.supportedOptions - private var totalWritten: Long = 0 + private var totalWritten: Int = 0 def write(bytes: Chunk[Byte]) = if (totalWritten >= limit) endOfOutput else { - val b = bytes.take(limit) - raw.write(b) >> IO(totalWritten += b.size) + val b = bytes.take(limit - totalWritten) + raw.write(b) >> IO(totalWritten += b.size) >> IO(totalWritten >= limit) + .ifM(endOfOutput, IO.unit) } } + // Setup an HTTPS echo server & a client that starts a TLS handshake but only sends the first few bytes and then signals no more output + // Doing so should not cause the server to peg a CPU val setup = for { tlsContext <- Resource.eval(testTlsContext) serverSocket <- Network[IO].bind(SocketAddress(ip"127.0.0.1", Port.Wildcard)) + echoServer = + serverSocket.accept + .flatMap(s => Stream.resource(tlsContext.serverBuilder(s).withLogger(logger).build)) + .map { socket => + socket.reads.chunks.foreach(socket.write) + } + .parJoinUnbounded client <- Network[IO].connect(serverSocket.address).flatMap { rawClient => - tlsContext.clientBuilder(rawClient).withLogger(logger).build + tlsContext.client(limitWrites(rawClient, 10)) } - } yield serverSocket.accept - .flatMap(s => Stream.resource(tlsContext.server(limitWrites(s, 20)))) -> client + } yield echoServer -> client Stream .resource(setup) - .flatMap { case (server, clientSocket) => - val echoServer = server.map { socket => - socket.reads.chunks.foreach(socket.write(_)) - }.parJoinUnbounded - + .flatMap { case (echoServer, clientSocket) => val client = Stream.exec(clientSocket.write(msg)).onFinalize(clientSocket.endOfOutput) ++ clientSocket.reads.take(msg.size.toLong) @@ -276,7 +281,6 @@ class TLSSocketSuite extends TLSSuite { } .compile .drain - .attempt } }