All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ch.epfl.scala.debugadapter.DebugServer.scala Maven / Gradle / Ivy

package ch.epfl.scala.debugadapter

import ch.epfl.scala.debugadapter.internal.DebugAdapter
import ch.epfl.scala.debugadapter.internal.DebugSession
import com.microsoft.java.debug.core.adapter.IProviderContext

import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.URI
import java.util.concurrent.ConcurrentLinkedQueue
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.control.NonFatal

final class DebugServer private (
    debuggee: Debuggee,
    context: IProviderContext,
    logger: Logger,
    address: DebugServer.Address,
    config: DebugConfig
)(implicit ec: ExecutionContext) {
  private var closedServer = false
  private val ongoingSessions = new ConcurrentLinkedQueue[DebugSession]()
  private val lock = new Object()

  private val serverSocket = address.serverSocket
  val uri = address.uri

  /**
   * Wait for a connection then start a session
   * If the session returns `DebugSession.Restarted`, wait for a new connection and start a new session
   * Until the session returns `DebugSession.Terminated` or `DebugSession.Disconnected`
   */
  def start(): Future[Unit] = {
    for {
      session <- Future(connect())
      exitStatus <- session.exitStatus
      _ <- exitStatus match {
        case DebugSession.Restarted => start()
        case _ => Future.successful(())
      }
    } yield ()
  }

  /**
   * Connect once and return a running session
   * In case of race condition with the [[close]] method, the session can be closed before returned
   */
  private[debugadapter] def connect(): DebugSession = {
    val socket = serverSocket.accept()
    val session = DebugSession(socket, debuggee, context, logger, config)
    lock.synchronized {
      if (closedServer) {
        session.close()
      } else {
        ongoingSessions.add(session)
        session.start()
      }
      session
    }
  }

  def close(): Unit = {
    lock.synchronized {
      if (!closedServer) {
        closedServer = true
        ongoingSessions.forEach(_.close())
        try {
          logger.info(s"Closing debug server $uri")
          serverSocket.close()
        } catch {
          case NonFatal(e) =>
            logger.warn(s"Could not close debug server listening on $uri due to: ${e.getMessage}]")
        }
      }
    }
  }
}

object DebugServer {
  final class Handler(val uri: URI, val running: Future[Unit])

  final class Address() {
    private val address = new InetSocketAddress(0)
    /*
     * Set backlog to 1 to recommend the OS to process one connection at a time,
     * which can happen when a restart is request and the client immediately
     * connects without waiting for the other session to finish.
     */
    val serverSocket = new ServerSocket(address.getPort, 1, address.getAddress)

    def uri: URI = URI.create(s"tcp://${address.getHostString}:${serverSocket.getLocalPort}")
  }

  /**
   * Create the server.
   * The server must then be started manually
   *
   * @param runner The debuggee process
   * @param address
   * @param logger
   * @param autoCloseSession If true the session closes itself after receiving terminated event from the debuggee
   * @param gracePeriod When closed the session waits for the debuggee to terminated gracefully
   * @param ec
   * @return a new instance of DebugServer
   */
  def apply(
      debuggee: Debuggee,
      tools: DebugTools,
      logger: Logger,
      address: Address = new Address,
      config: DebugConfig = DebugConfig.default
  )(implicit ec: ExecutionContext): DebugServer = {
    val context = DebugAdapter.context(debuggee, tools, logger, config)
    new DebugServer(debuggee, context, logger, address, config)
  }

  /**
   * Create a new server and start it.
   * The server waits for a connection then starts a session
   * If the session returns Restarted, the server will wait for a new connection
   * If the session returns Terminated or Disconnected it stops
   *
   * @param runner The debuggee process
   * @param logger
   * @param autoCloseSession If true the session closes itself after receiving terminated event from the debuggee
   * @param gracePeriod When closed the session waits for the debuggee to terminated gracefully
   * @param ec
   * @return The uri and running future of the server
   */
  def start(
      debuggee: Debuggee,
      tools: DebugTools,
      logger: Logger,
      autoCloseSession: Boolean = false,
      gracePeriod: Duration = 5.seconds
  )(implicit ec: ExecutionContext): Handler = {
    val config = DebugConfig.default.copy(gracePeriod = gracePeriod, autoCloseSession = autoCloseSession)
    val server = DebugServer(debuggee, tools, logger, config = config)
    val running = server.start()
    running.onComplete(_ => server.close())
    new Handler(server.uri, running)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy