tcp_listener.pony

use "collections"

class TCPListener[TCP: TCPBackend ref = RuntimeBackend]
  """
  The TCP listener: opens a listening socket, runs the accept loop, and
  enforces the connection limit. A `TCPListenerActor` owns one and delegates to
  it. Create it with `TCPListener(auth, host, port, this)`, using
  `TCPListener.none()` as the field initializer before that.
  """
  var _tcp: TCP = TCP
  let _host: String
  let _port: String
  let _limit: (MaxSpawn | None)
  let _ip_version: IPVersion
  var _open_connections: U32 = 0
  var _paused: Bool = false
  var _event: AsioEventID = AsioEvent.none()
  var _fd: U32 = -1
  var _listening: Bool = false
  var _enclosing: (TCPListenerActor[TCP] ref | None)

  new create(auth: TCPListenAuth,
    host: String,
    port: String,
    enclosing: TCPListenerActor[TCP] ref,
    ip_version: IPVersion = DualStack,
    limit: (MaxSpawn | None) = DefaultMaxSpawn())
  =>
    _host = host
    _port = port
    _ip_version = ip_version
    _limit = limit
    _enclosing = enclosing
    enclosing._finish_initialization()

  new none() =>
    """
    A placeholder listener for the actor's field before real initialization,
    replaced by a `create` listener once the actor starts.
    """
    _host = ""
    _port = ""
    _limit = None
    _ip_version = DualStack
    _enclosing = None

  fun ref close() =>
    match \exhaustive\ _enclosing
    | let e: TCPListenerActor[TCP] ref =>
      // TODO: when in debug mode we should blow up if listener is closed
      if _listening then
        _listening = false

        if not _event.is_null() then
          PonyAsio.unsubscribe(_event)
          // POSIX closes the listener fd here. On Windows the readiness backend
          // owns the close: it happens when the deferred
          // ProcessSocketNotifications REMOVE from the unsubscribe above is
          // seen, so closing here would strand the disposal handshake. The
          // accepted/rejected fds in _accept are raw (never subscribed), so
          // those closes stay cross-platform.
          ifdef not windows then
            _tcp.close(_fd)
          end
          _fd = -1
          e._on_closed()
        end
      end
    | None =>
      _Unreachable()
    end

  fun ref local_address(): NetAddress =>
    """
    Return the local IP address. If this TCPListener is closed then the
    address returned is invalid.
    """
    let ip = recover NetAddress end
    _tcp.sockname(_fd, ip)
    ip

  fun ref _event_notify(event: AsioEventID, flags: U32) =>
    if event isnt _event then
      return
    end

    if AsioEvent.errored(flags) then
      close()
      return
    end

    if AsioEvent.readable(flags) then
      _accept()
    end

    if AsioEvent.disposable(flags) then
      PonyAsio.destroy(_event)
      _event = AsioEvent.none()
      _listening = false
    end

  fun ref _accept() =>
    match \exhaustive\ _enclosing
    | let e: TCPListenerActor[TCP] ref =>
      if _listening then
        while not _at_connection_limit() do
          var fd = _tcp.accept(_event)

          // 0: would block, -1: error
          if fd <= 0 then
            return
          end

          try
            let opened = e._on_accept(fd.u32())?
            opened._register_spawner(e)
            _open_connections = _open_connections + 1
          else
            // Rejected before an event was created — raw fd, close on both
            // platforms.
            _tcp.close(fd.u32())
          end
        end

        _paused = true
      else
        // It's possible that after closing, we got an event for a connection
        // attempt. If the listener is not open, do not start a new connection.
        return
      end
    | None =>
      _Unreachable()
    end

  fun _at_connection_limit(): Bool =>
    match \exhaustive\ _limit
    | let l: MaxSpawn => _open_connections >= l()
    | None => false
    end

  fun ref _connection_closed() =>
    _open_connections = _open_connections - 1
    if _paused and not _at_connection_limit() then
      _paused = false
      _accept()
    end

  fun ref _finish_initialization() =>
    match \exhaustive\ _enclosing
    | let e: TCPListenerActor[TCP] ref =>
      _event = _tcp.listen(e, _host, _port where ip_version = _ip_version)
      if not _event.is_null() then
        _fd = PonyAsio.event_fd(_event)
        _listening = true
        e._on_listening()
      else
        e._on_listen_failure()
      end
    | None =>
      _Unreachable()
    end