Implementation of project/open and project/close commands. (#631)

This commit is contained in:
Łukasz Olczak 2020-03-31 15:51:05 +02:00 committed by GitHub
parent 0ffce13894
commit 5c616c2727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2204 additions and 159 deletions

View File

@ -94,7 +94,6 @@ lazy val enso = (project in file("."))
syntax_definition.jvm,
syntax.jvm,
pkg,
project_manager,
runtime,
polyglot_api,
parser_service,
@ -344,9 +343,21 @@ lazy val file_manager = (project in file("common/file-manager"))
lazy val project_manager = (project in file("common/project-manager"))
.settings(
(Compile / mainClass) := Some("org.enso.projectmanager.Server")
(Compile / mainClass) := Some("org.enso.projectmanager.boot.ProjectManager")
)
.settings(
(Compile / run / fork) := true,
(Test / fork) := true,
(Compile / run / connectInput) := true,
javaOptions ++= Seq(
// Puts the language runtime on the truffle classpath, rather than the
// standard classpath. This is the recommended way of handling this and
// we should strive to use such structure everywhere. See
// https://www.graalvm.org/docs/graalvm-as-a-platform/implement-language#graalvm
s"-Dtruffle.class.path.append=${(runtime / Compile / fullClasspath).value
.map(_.data)
.mkString(File.pathSeparator)}"
),
libraryDependencies ++= akka,
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
@ -366,6 +377,7 @@ lazy val project_manager = (project in file("common/project-manager"))
)
)
.dependsOn(pkg)
.dependsOn(language_server)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
@ -453,18 +465,19 @@ lazy val polyglot_api = project
lazy val language_server = (project in file("engine/language-server"))
.settings(
libraryDependencies ++= akka ++ circe ++ Seq(
"ch.qos.logback" % "logback-classic" % "1.2.3",
"io.circe" %% "circe-generic-extras" % "0.12.2",
"io.circe" %% "circe-literal" % circeVersion,
"org.bouncycastle" % "bcpkix-jdk15on" % "1.64",
"dev.zio" %% "zio" % "1.0.0-RC18-2",
"io.methvin" % "directory-watcher" % "0.9.6",
"com.beachape" %% "enumeratum-circe" % "1.5.23",
akkaTestkit % Test,
"commons-io" % "commons-io" % "2.6",
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test,
"org.scalacheck" %% "scalacheck" % "1.14.0" % Test,
"org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided"
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
"io.circe" %% "circe-generic-extras" % "0.12.2",
"io.circe" %% "circe-literal" % circeVersion,
"org.bouncycastle" % "bcpkix-jdk15on" % "1.64",
"dev.zio" %% "zio" % "1.0.0-RC18-2",
"io.methvin" % "directory-watcher" % "0.9.6",
"com.beachape" %% "enumeratum-circe" % "1.5.23",
akkaTestkit % Test,
"commons-io" % "commons-io" % "2.6",
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test,
"org.scalacheck" %% "scalacheck" % "1.14.0" % Test,
"org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided"
),
testOptions in Test += Tests
.Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "1000")

View File

@ -1,9 +1,26 @@
project-manager {
network {
interface = "127.0.0.1"
interface = ${?NETWORK_INTERFACE}
min-port = 49152
min-port = ${?NETWORK_MIN_PORT}
max-port = 65535
max-port = ${?NETWORK_MAX_PORT}
}
server {
host = "0.0.0.0"
host = ${project-manager.network.interface}
port = 30535
}
bootloader {
no-retries = 10
delay-between-retry = 2 second
}
storage {
projects-root = ${user.home}/enso
projects-root=${?PROJECTS_ROOT}
@ -17,6 +34,7 @@ project-manager {
timeout {
io-timeout = 5 seconds
request-timeout = 10 seconds
boot-timeout = 30 seconds
}
tutorials {

View File

@ -4,7 +4,7 @@
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-40thread] %-5level %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} [%-15thread] %-5level %msg%n</pattern>
</encoder>
</appender>

View File

@ -0,0 +1,38 @@
package org.enso.projectmanager.boot
import zio.{Has, ZEnv}
import zio.blocking.Blocking
import zio.clock.Clock
import zio.console.Console
import zio.random.Random
import zio.system.System
/**
* Constants manager for app constants.
*/
object Globals {
val FailureExitCode = 1
val SuccessExitCode = 0
val ConfigFilename = "application.conf"
val ConfigNamespace = "project-manager"
val zioEnvironment: ZEnv =
Has.allOf[
Clock.Service,
Console.Service,
System.Service,
Random.Service,
Blocking.Service
](
Clock.Service.live,
Console.Service.live,
System.Service.live,
Random.Service.live,
Blocking.Service.live
)
}

View File

@ -1,16 +1,21 @@
package org.enso.projectmanager.main
package org.enso.projectmanager.boot
import akka.actor.ActorSystem
import akka.stream.SystemMaterializer
import cats.{Bifunctor, MonadError}
import cats.MonadError
import io.circe.generic.auto._
import org.enso.jsonrpc.JsonRpcServer
import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec, Sync}
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
import org.enso.projectmanager.infrastructure.file.{
BlockingFileSystem,
SynchronizedFileStorage
}
import org.enso.projectmanager.infrastructure.languageserver.{
LanguageServerRegistry,
LanguageServerRegistryProxy
}
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
import org.enso.projectmanager.infrastructure.random.SystemGenerator
import org.enso.projectmanager.infrastructure.repository.{
@ -18,7 +23,6 @@ import org.enso.projectmanager.infrastructure.repository.{
ProjectIndex
}
import org.enso.projectmanager.infrastructure.time.RealClock
import org.enso.projectmanager.main.configuration.ProjectManagerConfig
import org.enso.projectmanager.protocol.{
JsonRpc,
ManagerClientControllerFactory
@ -30,18 +34,22 @@ import org.enso.projectmanager.service.{
ValidationFailure
}
import scala.concurrent.ExecutionContext
/**
* A main module containing all components of the project manager.
*
*/
class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Bifunctor](
config: ProjectManagerConfig
class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Async](
config: ProjectManagerConfig,
computeExecutionContext: ExecutionContext
)(
implicit E1: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure],
E2: MonadError[F[ValidationFailure, *], ValidationFailure]
) {
implicit val system = ActorSystem()
implicit val system =
ActorSystem("project-manager", None, None, Some(computeExecutionContext))
implicit val materializer = SystemMaterializer.get(system)
@ -68,20 +76,32 @@ class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Bifuncto
lazy val projectValidator = new MonadicProjectValidator[F]()
lazy val languageServerController =
system.actorOf(
LanguageServerRegistry.props(config.network, config.bootloader),
"language-server-controller"
)
lazy val languageServerService = new LanguageServerRegistryProxy[F](
languageServerController,
config.timeout
)
lazy val projectService =
new ProjectService[F](
projectValidator,
projectRepository,
logging,
clock,
gen
gen,
languageServerService
)
lazy val clientControllerFactory =
new ManagerClientControllerFactory[F](
system,
projectService,
config.timeout.requestTimeout
config.timeout
)
lazy val server = new JsonRpcServer(JsonRpc.protocol, clientControllerFactory)

View File

@ -1,29 +1,28 @@
package org.enso.projectmanager.main
package org.enso.projectmanager.boot
import java.io.IOException
import java.util.concurrent.ScheduledThreadPoolExecutor
import akka.http.scaladsl.Http
import com.typesafe.scalalogging.LazyLogging
import org.enso.projectmanager.main.Globals.{
import org.enso.projectmanager.boot.Globals.{
ConfigFilename,
ConfigNamespace,
FailureExitCode,
SuccessExitCode
}
import org.enso.projectmanager.main.configuration.ProjectManagerConfig
import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
import pureconfig.ConfigSource
import zio.ZIO.effectTotal
import zio._
import zio.console._
import scala.concurrent.Await
import scala.concurrent.duration._
import pureconfig.ConfigSource
import zio.interop.catz.core._
import org.enso.projectmanager.infrastructure.config.ConfigurationReaders.fileReader
import pureconfig._
import pureconfig.generic.auto._
import zio._
import zio.interop.catz.core._
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor}
/**
* Project manager runner containing the main method.
@ -42,16 +41,28 @@ object ProjectManager extends App with LazyLogging {
.at(ConfigNamespace)
.loadOrThrow[ProjectManagerConfig]
val computeThreadPool = new ScheduledThreadPoolExecutor(
java.lang.Runtime.getRuntime.availableProcessors()
)
val computeExecutionContext: ExecutionContextExecutor =
ExecutionContext.fromExecutor(
computeThreadPool,
th => logger.error("An expected error occurred", th)
)
/**
* ZIO runtime.
*/
implicit val runtime = Runtime.default
implicit val runtime =
Runtime(Globals.zioEnvironment, new ZioPlatform(computeExecutionContext))
/**
* Main process starting up the server.
*/
lazy val mainProcess: ZIO[ZEnv, IOException, Unit] = {
val mainModule = new MainModule[ZIO[ZEnv, +*, +*]](config)
val mainModule =
new MainModule[ZIO[ZEnv, +*, +*]](config, computeExecutionContext)
for {
binding <- bindServer(mainModule)
_ <- logServerStartup()
@ -67,7 +78,10 @@ object ProjectManager extends App with LazyLogging {
* arguments to the program and has to return an `IO` with the errors fully handled.
*/
override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
mainProcess.fold(_ => FailureExitCode, _ => SuccessExitCode)
mainProcess.fold(
th => { th.printStackTrace(); FailureExitCode },
_ => SuccessExitCode
)
private def logServerStartup(): UIO[Unit] =
effectTotal {

View File

@ -0,0 +1,44 @@
package org.enso.projectmanager.boot
import com.typesafe.scalalogging.LazyLogging
import zio.Cause
import zio.internal.stacktracer.Tracer
import zio.internal.stacktracer.impl.AkkaLineNumbersTracer
import zio.internal.tracing.TracingConfig
import zio.internal.{Executor, Platform, Tracing}
import scala.concurrent.ExecutionContext
/**
* An environment needed to execute ZIO actions.
*
* @param computeExecutionContext compute thread pool
*/
class ZioPlatform(computeExecutionContext: ExecutionContext)
extends Platform
with LazyLogging {
override def executor: Executor =
Executor.fromExecutionContext(2048)(computeExecutionContext)
override val tracing = Tracing(
Tracer.globallyCached(new AkkaLineNumbersTracer),
TracingConfig.enabled
)
override def fatal(t: Throwable): Boolean =
t.isInstanceOf[VirtualMachineError]
override def reportFatal(t: Throwable): Nothing = {
t.printStackTrace()
try {
System.exit(-1)
throw t
} catch { case _: Throwable => throw t }
}
override def reportFailure(cause: Cause[Any]): Unit =
if (cause.died)
logger.error(cause.prettyPrint)
}

View File

@ -0,0 +1,75 @@
package org.enso.projectmanager.boot
import java.io.File
import scala.concurrent.duration.FiniteDuration
object configuration {
/**
* A configuration object for properties of the Project Manager.
*
* @param server a JSON RPC server configuration
*/
case class ProjectManagerConfig(
server: ServerConfig,
storage: StorageConfig,
timeout: TimeoutConfig,
network: NetworkConfig,
bootloader: BootloaderConfig
)
/**
* A configuration object for properties of the JSON RPC server.
*
* @param host an address that the server listen on
* @param port a port that the server listen on
*/
case class ServerConfig(host: String, port: Int)
/**
* A configuration object for properties of project storage.
*
* @param projectsRoot a project root
* @param projectMetadataPath a path to metadata
* @param userProjectsPath a user project root
*/
case class StorageConfig(
projectsRoot: File,
projectMetadataPath: File,
userProjectsPath: File
)
/**
* A configuration object for timeout properties.
*
* @param ioTimeout a timeout for IO operations
* @param requestTimeout a timeout for JSON RPC request timeout
*/
case class TimeoutConfig(
ioTimeout: FiniteDuration,
requestTimeout: FiniteDuration,
bootTimeout: FiniteDuration
)
/**
* A configuration object for networking.
*
* @param interface an interface to listen to
* @param minPort min port for the LS
* @param maxPort max port for the LS
*/
case class NetworkConfig(interface: String, minPort: Int, maxPort: Int)
/**
* A configuration object for bootloader properties.
*
* @param numberOfRetries how many times a bootloader should try to boot the LS
* @param delayBetweenRetry delays between retries
*/
case class BootloaderConfig(
numberOfRetries: Int,
delayBetweenRetry: FiniteDuration
)
}

View File

@ -0,0 +1,41 @@
package org.enso.projectmanager.control.effect
import scala.concurrent.Future
import scala.util.Either
/**
* A class for asynchronous effects that do not block threads.
*
* @tparam F an effectful context
*/
trait Async[F[+_, +_]] {
/**
* Imports an asynchronous side-effect into a pure `F` value.
*
* @param register is a function that should be called with a
* callback for signaling the result once it is ready
* @tparam E an error type
* @tparam A a result type
* @return pure `F` value
*/
def async[E, A](register: (Either[E, A] => Unit) => Unit): F[E, A]
/**
* Converts side-effecting future into a pure `F` value.
*
* @param thunk a thunk that starts asynchronous computations
* @tparam A a returned type
* @return pure `F` value
*/
def fromFuture[A](thunk: () => Future[A]): F[Throwable, A]
}
object Async {
def apply[F[+_, +_]](implicit async: Async[F]): Async[F] = async
implicit def zioAsync[R]: ZioAsync[R] = new ZioAsync[R]
}

View File

@ -0,0 +1,7 @@
package org.enso.projectmanager.control.effect
import java.util.concurrent.Executor
private[effect] object ImmediateExecutor extends Executor {
override def execute(command: Runnable): Unit = command.run()
}

View File

@ -7,7 +7,7 @@ import zio.{ZEnv, ZIO}
import scala.concurrent.duration.FiniteDuration
/**
* A class for synchronous effects that blocks threads.
* A class for synchronous effects that block threads.
*
* @tparam F an effectful context
*/

View File

@ -0,0 +1,35 @@
package org.enso.projectmanager.control.effect
import zio.ZIO
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
/**
* Instance of [[Async]] class for ZIO.
*/
class ZioAsync[R] extends Async[ZIO[R, +*, +*]] {
implicit private val immediateEc =
ExecutionContext.fromExecutor(ImmediateExecutor)
/** @inheritdoc **/
override def async[E, A](
register: (Either[E, A] => Unit) => Unit
): ZIO[R, E, A] =
ZIO.effectAsync[R, E, A] { callback =>
register { result =>
callback(ZIO.fromEither(result))
}
}
/** @inheritdoc **/
override def fromFuture[A](thunk: () => Future[A]): ZIO[R, Throwable, A] =
ZIO.effectAsync[R, Throwable, A] { cb =>
thunk().onComplete {
case Success(value) => cb(ZIO.succeed(value))
case Failure(exception) => cb(ZIO.fail(exception))
}
}
}

View File

@ -0,0 +1,3 @@
package org.enso.projectmanager.data
case class SocketData(host: String, port: Int)

View File

@ -0,0 +1,27 @@
package org.enso.projectmanager.event
import java.util.UUID
/**
* Base trait for all client events.
*/
sealed trait ClientEvent extends Event
object ClientEvent {
/**
* Notifies the Language Server about a new client connecting.
*
* @param clientId an object representing a client
*/
case class ClientConnected(clientId: UUID) extends ClientEvent
/**
* Notifies the Language Server about a client disconnecting.
* The client may not send any further messages after this one.
*
* @param clientId the internal id of this client
*/
case class ClientDisconnected(clientId: UUID) extends ClientEvent
}

View File

@ -0,0 +1,6 @@
package org.enso.projectmanager.event
/**
* Base trait for all kinds of events.
*/
trait Event

View File

@ -0,0 +1,145 @@
package org.enso.projectmanager.infrastructure.languageserver
import akka.actor.Status.Failure
import akka.actor.{Actor, ActorLogging, Props}
import akka.pattern.pipe
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.projectmanager.boot.configuration.BootloaderConfig
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
Boot,
FindFreeSocket,
ServerBootFailed,
ServerBooted
}
import org.enso.projectmanager.infrastructure.net.Tcp
/**
* It boots a Language Sever described by the `descriptor`. Upon boot failure
* looks up new available port and retries to boot the server.
*
* @param descriptor a LS descriptor
* @param config a bootloader config
*/
class LanguageServerBootLoader(
descriptor: LanguageServerDescriptor,
config: BootloaderConfig
) extends Actor
with ActorLogging {
import context.dispatcher
override def preStart(): Unit = {
log.info(s"Booting a language server [$descriptor]")
self ! FindFreeSocket
}
override def receive: Receive = findingSocket()
private def findingSocket(retry: Int = 0): Receive = {
case FindFreeSocket =>
log.debug("Looking for available socket to bind the language server")
val port = Tcp.findAvailablePort(
descriptor.networkConfig.interface,
descriptor.networkConfig.minPort,
descriptor.networkConfig.maxPort
)
log.info(
s"Found a socket for the language server [${descriptor.networkConfig.interface}:$port]"
)
self ! Boot
context.become(
booting(SocketData(descriptor.networkConfig.interface, port), retry)
)
}
private def booting(socket: SocketData, retryCount: Int): Receive = {
case Boot =>
log.debug("Booting a language server")
val config = LanguageServerConfig(
socket.host,
socket.port,
descriptor.rootId,
descriptor.root,
descriptor.name,
context.dispatcher
)
val server = new LanguageServerComponent(config)
server.start().map(_ => config -> server) pipeTo self
case Failure(th) =>
log.error(
th,
s"An error occurred during boot of Language Server [${descriptor.name}]"
)
if (retryCount < config.numberOfRetries) {
context.system.scheduler
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
context.become(findingSocket(retryCount + 1))
} else {
log.error(
s"Tried $retryCount times to boot Language Server. Giving up."
)
context.parent ! ServerBootFailed(th)
context.stop(self)
}
case (config: LanguageServerConfig, server: LanguageServerComponent) =>
log.info(s"Language server booted [$config].")
context.parent ! ServerBooted(config, server)
context.stop(self)
}
override def unhandled(message: Any): Unit =
log.warning("Received unknown message: {}", message)
}
object LanguageServerBootLoader {
/**
* Creates a configuration object used to create a [[LanguageServerBootLoader]].
*
* @param descriptor a LS descriptor
* @param config a bootloader config
* @return a configuration object
*/
def props(
descriptor: LanguageServerDescriptor,
config: BootloaderConfig
): Props =
Props(new LanguageServerBootLoader(descriptor, config))
/**
* Find free socket command.
*/
case object FindFreeSocket
/**
* Boot command.
*/
case object Boot
/**
* Signals that server boot failed.
*
* @param th a throwable
*/
case class ServerBootFailed(th: Throwable)
/**
* Signals that server booted successfully.
*
* @param config a server config
* @param server a server lifecycle component
*/
case class ServerBooted(
config: LanguageServerConfig,
server: LanguageServerComponent
)
}

View File

@ -0,0 +1,221 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.Status.Failure
import akka.actor.{
Actor,
ActorLogging,
ActorRef,
Cancellable,
OneForOneStrategy,
Props,
Stash,
SupervisorStrategy,
Terminated
}
import akka.pattern.pipe
import org.enso.languageserver.boot.LanguageServerComponent.ServerStopped
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.projectmanager.boot.configuration.{
BootloaderConfig,
NetworkConfig
}
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.event.ClientEvent.ClientDisconnected
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
ServerBootFailed,
ServerBooted
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.{
Boot,
BootTimeout
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.model.Project
import scala.concurrent.duration._
/**
* A language server controller responsible for managing the server lifecycle.
* It delegates all tasks to other actors like bootloader or supervisor.
*
* @param project a project open by the server
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
*/
class LanguageServerController(
project: Project,
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig
) extends Actor
with Stash
with ActorLogging {
import context.dispatcher
private val descriptor =
LanguageServerDescriptor(
name = s"language-server-${project.id}",
rootId = UUID.randomUUID(),
root = project.path.get,
networkConfig = networkConfig
)
override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(10) {
case _ => SupervisorStrategy.Restart
}
override def preStart(): Unit = {
context.system.eventStream.subscribe(self, classOf[ClientDisconnected])
self ! Boot
}
override def receive: Receive = {
case Boot =>
val bootloader =
context.actorOf(
LanguageServerBootLoader.props(descriptor, bootloaderConfig)
)
context.watch(bootloader)
val timeoutCancellable =
context.system.scheduler.scheduleOnce(30.seconds, self, BootTimeout)
context.become(booting(bootloader, timeoutCancellable))
case _ => stash()
}
private def booting(
Bootloader: ActorRef,
timeoutCancellable: Cancellable
): Receive = {
case BootTimeout =>
log.error(s"Booting failed for $descriptor")
stop()
case ServerBootFailed(th) =>
unstashAll()
timeoutCancellable.cancel()
context.become(bootFailed(th))
case ServerBooted(config, server) =>
unstashAll()
timeoutCancellable.cancel()
context.become(supervising(config, server))
case Terminated(Bootloader) =>
log.error(s"Bootloader for project ${project.name} failed")
unstashAll()
timeoutCancellable.cancel()
context.become(
bootFailed(new Exception("The number of boot retries exceeded"))
)
case _ => stash()
}
private def supervising(
config: LanguageServerConfig,
server: LanguageServerComponent,
clients: Set[UUID] = Set.empty
): Receive = {
case StartServer(clientId, _) =>
sender() ! ServerStarted(
SocketData(config.interface, config.port)
)
context.become(supervising(config, server, clients + clientId))
case Terminated(_) =>
log.debug(s"Bootloader for $project terminated.")
case StopServer(clientId, _) =>
removeClient(config, server, clients, clientId, Some(sender()))
case ClientDisconnected(clientId) =>
removeClient(config, server, clients, clientId, None)
}
private def removeClient(
config: LanguageServerConfig,
server: LanguageServerComponent,
clients: Set[UUID],
clientId: UUID,
maybeRequester: Option[ActorRef]
): Unit = {
val updatedClients = clients - clientId
if (updatedClients.isEmpty) {
server.stop() pipeTo self
context.become(stopping(maybeRequester))
} else {
sender() ! CannotDisconnectOtherClients
context.become(supervising(config, server, updatedClients))
}
}
private def bootFailed(th: Throwable): Receive = {
case StartServer(_, _) =>
sender() ! LanguageServerProtocol.ServerBootFailed(th)
stop()
}
private def stopping(maybeRequester: Option[ActorRef]): Receive = {
case Failure(th) =>
log.error(
th,
s"An error occurred during Language server shutdown [$project]."
)
maybeRequester.foreach(_ ! FailureDuringStoppage(th))
stop()
case ServerStopped =>
log.info(s"Language server shut down successfully [$project].")
maybeRequester.foreach(_ ! LanguageServerProtocol.ServerStopped)
stop()
}
private def stop(): Unit = {
context.stop(self)
context.parent ! ServerShutDown(project.id)
}
override def unhandled(message: Any): Unit =
log.warning("Received unknown message: {}", message)
}
object LanguageServerController {
/**
* Creates a configuration object used to create a [[LanguageServerController]].
*
* @param project a project open by the server
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
* @return a configuration object
*/
def props(
project: Project,
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig
): Props =
Props(
new LanguageServerController(project, networkConfig, bootloaderConfig)
)
/**
* Signals boot timeout.
*/
case object BootTimeout
/**
* Boot command.
*/
case object Boot
}

View File

@ -0,0 +1,20 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import org.enso.projectmanager.boot.configuration.NetworkConfig
/**
* A descriptor used to start up a Language Server.
*
* @param name a name of the LS
* @param rootId a content root id
* @param root a path to the content root
* @param networkConfig a network config
*/
case class LanguageServerDescriptor(
name: String,
rootId: UUID,
root: String,
networkConfig: NetworkConfig
)

View File

@ -0,0 +1,103 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.model.Project
/**
* A language subsystem protocol.
*/
object LanguageServerProtocol {
/**
* Command to start a server.
*
* @param clientId the requester id
* @param project the project to start
*/
case class StartServer(clientId: UUID, project: Project)
/**
* Base trait for server startup results.
*/
sealed trait ServerStartupResult
/**
* Signals that server started successfully.
*
* @param socket the server socket
*/
case class ServerStarted(socket: SocketData) extends ServerStartupResult
/**
* Base trait for server startup failures.
*/
sealed trait ServerStartupFailure extends ServerStartupResult
/**
* Signals that server boot failed with exception.
*
* @param throwable an exception thrown by bootloader
*/
case class ServerBootFailed(throwable: Throwable) extends ServerStartupFailure
/**
* Signals server boot timeout.
*/
case object ServerBootTimedOut extends ServerStartupFailure
/**
* Command to stop a server.
*
* @param clientId the requester id
* @param projectId the project id
*/
case class StopServer(clientId: UUID, projectId: UUID)
/**
* Base trait for server stoppage results.
*/
sealed trait ServerStoppageResult
/**
* Signals that server stopped successfully.
*/
case object ServerStopped extends ServerStoppageResult
/**
* Base trait for server stoppage failures.
*/
sealed trait ServerStoppageFailure extends ServerStoppageResult
/**
* Signals that an exception was thrown during stopping a server.
*
* @param th an exception
*/
case class FailureDuringStoppage(th: Throwable) extends ServerStoppageFailure
/**
* Signals that server wasn't started.
*/
case object ServerNotRunning extends ServerStoppageFailure
/**
* Signals that server cannot be stopped, because other clients are connected
* to the server.
*/
case object CannotDisconnectOtherClients extends ServerStoppageFailure
/**
* Request to check is server is running.
*
* @param projectId the project id
*/
case class CheckIfServerIsRunning(projectId: UUID)
/**
* Signals that check timed out.
*/
case object CheckTimeout
}

View File

@ -0,0 +1,95 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated}
import org.enso.projectmanager.boot.configuration.{
BootloaderConfig,
NetworkConfig
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{
CheckIfServerIsRunning,
ServerNotRunning,
StartServer,
StopServer
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
/**
* An actor that routes request regarding lang. server lifecycle to the
* right controller managing the server.
* It creates a controller actor, if a server doesn't exists.
*
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
*/
class LanguageServerRegistry(
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig
) extends Actor
with ActorLogging {
override def receive: Receive = running()
private def running(
serverControllers: Map[UUID, ActorRef] = Map.empty
): Receive = {
case msg @ StartServer(_, project) =>
if (serverControllers.contains(project.id)) {
serverControllers(project.id).forward(msg)
} else {
val controller = context.actorOf(
LanguageServerController
.props(project, networkConfig, bootloaderConfig)
)
context.watch(controller)
controller.forward(msg)
context.become(running(serverControllers + (project.id -> controller)))
}
case msg @ StopServer(_, projectId) =>
if (serverControllers.contains(projectId)) {
serverControllers(projectId).forward(msg)
} else {
sender() ! ServerNotRunning
}
case ServerShutDown(projectId) =>
context.become(running(serverControllers - projectId))
case Terminated(ref) =>
context.become(running(serverControllers.filterNot(_._2 == ref)))
case CheckIfServerIsRunning(projectId) =>
sender() ! serverControllers.contains(projectId)
}
override def unhandled(message: Any): Unit =
log.warning("Received unknown message: {}", message)
}
object LanguageServerRegistry {
/**
* A notification informing that a server has shut down.
*
* @param projectId a project id
*/
case class ServerShutDown(projectId: UUID)
/**
* Creates a configuration object used to create a [[LanguageServerRegistry]].
*
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
* @return
*/
def props(
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig
): Props =
Props(new LanguageServerRegistry(networkConfig, bootloaderConfig))
}

View File

@ -0,0 +1,70 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{Async, ErrorChannel}
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.model.Project
/**
* It is a proxy to actor based language subsystem. It a bridge between
* actor interface and pure functional effects.
*
* @param registry a lang. server registry
* @param timeoutConfig a timeout config
* @tparam F a effectful context
*/
class LanguageServerRegistryProxy[F[+_, +_]: Async: ErrorChannel: CovariantFlatMap](
registry: ActorRef,
timeoutConfig: TimeoutConfig
) extends LanguageServerService[F] {
implicit val timeout: Timeout = Timeout(timeoutConfig.bootTimeout)
/** @inheritdoc **/
override def start(
clientId: UUID,
project: Project
): F[ServerStartupFailure, SocketData] =
Async[F]
.fromFuture { () =>
(registry ? StartServer(clientId, project)).mapTo[ServerStartupResult]
}
.mapError(_ => ServerBootTimedOut)
.flatMap {
case ServerStarted(socket) => CovariantFlatMap[F].pure(socket)
case f: ServerStartupFailure => ErrorChannel[F].fail(f)
}
/** @inheritdoc **/
override def stop(
clientId: UUID,
projectId: UUID
): F[ServerStoppageFailure, Unit] =
Async[F]
.fromFuture { () =>
(registry ? StopServer(clientId, projectId)).mapTo[ServerStoppageResult]
}
.mapError(FailureDuringStoppage(_))
.flatMap {
case ServerStopped => CovariantFlatMap[F].pure()
case f: ServerStoppageFailure => ErrorChannel[F].fail(f)
}
/** @inheritdoc **/
override def isRunning(projectId: UUID): F[CheckTimeout.type, Boolean] =
Async[F]
.fromFuture { () =>
(registry ? CheckIfServerIsRunning(projectId)).mapTo[Boolean]
}
.mapError(_ => CheckTimeout)
}

View File

@ -0,0 +1,52 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{
CheckTimeout,
ServerStartupFailure,
ServerStoppageFailure
}
import org.enso.projectmanager.model.Project
/**
* A infrastructure service for managing lang. servers.
*
* @tparam F a effectful context
*/
trait LanguageServerService[F[+_, +_]] {
/**
* Starts a lang. server.
*
* @param clientId a requester id
* @param project a project to start
* @return either failure or socket
*/
def start(
clientId: UUID,
project: Project
): F[ServerStartupFailure, SocketData]
/**
* Stops a lang. server.
*
* @param clientId a requester id
* @param projectId a project id to stop
* @return either failure or Unit representing void success
*/
def stop(
clientId: UUID,
projectId: UUID
): F[ServerStoppageFailure, Unit]
/**
* Checks if server is running for project.
*
* @param projectId a project id
* @return true if project is open
*/
def isRunning(projectId: UUID): F[CheckTimeout.type, Boolean]
}

View File

@ -0,0 +1,53 @@
package org.enso.projectmanager.infrastructure.net
import java.net.InetAddress
import javax.net.ServerSocketFactory
import scala.annotation.tailrec
import scala.util.Random
/**
* A namespace for TCP auxiliary functions.
*/
object Tcp {
/**
* Finds first available socket.
*
* @param host a host
* @param minPort a minimum value of port
* @param maxPort a maximum value of port
* @return a port that is available to bind
*/
@tailrec
def findAvailablePort(host: String, minPort: Int, maxPort: Int): Int = {
val random = Random.nextInt(maxPort - minPort + 1)
val port = minPort + random
if (isPortAvailable(host, port)) {
port
} else {
findAvailablePort(host, minPort, maxPort)
}
}
/**
* Checks if socket is available.
*
* @param host a host
* @param port a port
* @return true if socket is available
*/
def isPortAvailable(host: String, port: Int): Boolean =
try {
val serverSocket = ServerSocketFactory.getDefault.createServerSocket(
port,
1,
InetAddress.getByName(host)
)
serverSocket.close()
true
} catch {
case _: Exception => false
}
}

View File

@ -14,7 +14,7 @@ import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailur
ProjectNotFoundInIndex,
StorageFailure
}
import org.enso.projectmanager.main.configuration.StorageConfig
import org.enso.projectmanager.boot.configuration.StorageConfig
import org.enso.projectmanager.model.Project
/**
@ -44,6 +44,15 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
.map(_.exists(name))
.mapError(_.fold(convertFileStorageFailure))
/** @inheritdoc **/
override def findUserProject(
projectId: UUID
): F[ProjectRepositoryFailure, Option[Project]] =
indexStorage
.load()
.map(_.userProjects.get(projectId))
.mapError(_.fold(convertFileStorageFailure))
/**
* Inserts the provided user project to the storage.
*

View File

@ -37,4 +37,14 @@ trait ProjectRepository[F[+_, +_]] {
*/
def deleteUserProject(projectId: UUID): F[ProjectRepositoryFailure, Unit]
/**
* Finds a project by project id.
*
* @param projectId a project id
* @return option with the project entity
*/
def findUserProject(
projectId: UUID
): F[ProjectRepositoryFailure, Option[Project]]
}

View File

@ -1,16 +0,0 @@
package org.enso.projectmanager.main
/**
* Constants manager for app constants.
*/
object Globals {
val FailureExitCode = 1
val SuccessExitCode = 0
val ConfigFilename = "application.conf"
val ConfigNamespace = "project-manager"
}

View File

@ -1,45 +0,0 @@
package org.enso.projectmanager.main
import java.io.File
import scala.concurrent.duration.FiniteDuration
object configuration {
/**
* A configuration object for properties of the Project Manager.
*
* @param server a JSON RPC server configuration
*/
case class ProjectManagerConfig(
server: ServerConfig,
storage: StorageConfig,
timeout: TimeoutConfig
)
/**
* A configuration object for properties of the JSON RPC server.
*
* @param host an address that the server listen on
* @param port a port that the server listen on
*/
case class ServerConfig(host: String, port: Int)
case class StorageConfig(
projectsRoot: File,
projectMetadataPath: File,
userProjectsPath: File
)
/**
* A configuration object for timeout proeperties.
*
* @param ioTimeout a timeout for IO operations
* @param requestTimeout a timeout for JSON RPC request timeout
*/
case class TimeoutConfig(
ioTimeout: FiniteDuration,
requestTimeout: FiniteDuration
)
}

View File

@ -4,39 +4,52 @@ import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
import org.enso.jsonrpc.{JsonRpcServer, MessageHandler, Method, Request}
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.event.ClientEvent.{
ClientConnected,
ClientDisconnected
}
import org.enso.projectmanager.protocol.ProjectManagementApi.{
ProjectClose,
ProjectCreate,
ProjectDelete
ProjectDelete,
ProjectOpen
}
import org.enso.projectmanager.requesthandler.{
ProjectCloseHandler,
ProjectCreateHandler,
ProjectDeleteHandler
ProjectDeleteHandler,
ProjectOpenHandler
}
import org.enso.projectmanager.service.ProjectServiceApi
import scala.concurrent.duration.FiniteDuration
/**
* An actor handling communications between a single client and the project
* manager.
*
* @param clientId the internal client id.
* @param projectService a project service
* @param timeout a request timeout
* @param config a request timeout cofig
*/
class ClientController[F[+_, +_]: Exec](
clientId: UUID,
projectService: ProjectServiceApi[F],
timeout: FiniteDuration
config: TimeoutConfig
) extends Actor
with Stash
with ActorLogging {
private val requestHandlers: Map[Method, Props] =
Map(
ProjectCreate -> ProjectCreateHandler.props[F](projectService, timeout),
ProjectDelete -> ProjectDeleteHandler.props[F](projectService, timeout)
ProjectCreate -> ProjectCreateHandler
.props[F](projectService, config.requestTimeout),
ProjectDelete -> ProjectDeleteHandler
.props[F](projectService, config.requestTimeout),
ProjectOpen -> ProjectOpenHandler
.props[F](clientId, projectService, config.bootTimeout),
ProjectClose -> ProjectCloseHandler
.props[F](clientId, projectService, config.bootTimeout)
)
override def unhandled(message: Any): Unit =
@ -44,14 +57,18 @@ class ClientController[F[+_, +_]: Exec](
override def receive: Receive = {
case JsonRpcServer.WebConnect(webActor) =>
log.info(s"Client connected to Project Manager [$clientId]")
unstashAll()
context.become(connected(webActor))
context.system.eventStream.publish(ClientConnected(clientId))
case _ => stash()
}
def connected(webActor: ActorRef): Receive = {
case MessageHandler.Disconnected =>
log.info(s"Client disconnected from the Project Manager [$clientId]")
context.system.eventStream.publish(ClientDisconnected(clientId))
context.stop(self)
case r @ Request(method, _, _) if (requestHandlers.contains(method)) =>
@ -71,8 +88,8 @@ object ClientController {
def props[F[+_, +_]: Exec](
clientId: UUID,
projectService: ProjectServiceApi[F],
timeout: FiniteDuration
config: TimeoutConfig
): Props =
Props(new ClientController(clientId, projectService, timeout))
Props(new ClientController(clientId, projectService, config: TimeoutConfig))
}

View File

@ -3,8 +3,10 @@ package org.enso.projectmanager.protocol
import io.circe.generic.auto._
import org.enso.jsonrpc.Protocol
import org.enso.projectmanager.protocol.ProjectManagementApi.{
ProjectClose,
ProjectCreate,
ProjectDelete
ProjectDelete,
ProjectOpen
}
object JsonRpc {
@ -16,5 +18,7 @@ object JsonRpc {
Protocol.empty
.registerRequest(ProjectCreate)
.registerRequest(ProjectDelete)
.registerRequest(ProjectOpen)
.registerRequest(ProjectClose)
}

View File

@ -4,11 +4,10 @@ import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import org.enso.jsonrpc.ClientControllerFactory
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.service.ProjectServiceApi
import scala.concurrent.duration.FiniteDuration
/**
* Project manager client controller factory.
*
@ -17,7 +16,7 @@ import scala.concurrent.duration.FiniteDuration
class ManagerClientControllerFactory[F[+_, +_]: Exec](
system: ActorSystem,
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration
timeoutConfig: TimeoutConfig
) extends ClientControllerFactory {
/**
@ -28,7 +27,7 @@ class ManagerClientControllerFactory[F[+_, +_]: Exec](
*/
override def createClientController(clientId: UUID): ActorRef =
system.actorOf(
ClientController.props[F](clientId, projectService, requestTimeout)
ClientController.props[F](clientId, projectService, timeoutConfig)
)
}

View File

@ -3,6 +3,7 @@ package org.enso.projectmanager.protocol
import java.util.UUID
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.projectmanager.data.SocketData
/**
* The project management JSON RPC API provided by the project manager.
@ -37,6 +38,32 @@ object ProjectManagementApi {
}
}
case object ProjectOpen extends Method("project/open") {
case class Params(projectId: UUID)
case class Result(languageServerAddress: SocketData)
implicit val hasParams = new HasParams[this.type] {
type Params = ProjectOpen.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = ProjectOpen.Result
}
}
case object ProjectClose extends Method("project/close") {
case class Params(projectId: UUID)
implicit val hasParams = new HasParams[this.type] {
type Params = ProjectClose.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type
}
}
case class ProjectNameValidationError(msg: String) extends Error(4001, msg)
case class ProjectDataStoreError(msg: String) extends Error(4002, msg)
@ -47,4 +74,20 @@ object ProjectManagementApi {
case object ProjectNotFoundError
extends Error(4004, "Project with the provided id does not exist")
case class ProjectOpenError(msg: String) extends Error(4005, msg)
case object ProjectNotOpenError
extends Error(4006, "Cannot close project that is not open")
case object ProjectOpenByOtherPeersError
extends Error(
4007,
"Cannot close project because it is open by other peers"
)
case object CannotRemoveOpenProjectError
extends Error(4008, "Cannot remove open project")
case class ProjectCloseError(msg: String) extends Error(4009, msg)
}

View File

@ -0,0 +1,94 @@
package org.enso.projectmanager.requesthandler
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import akka.pattern.pipe
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectClose
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
import org.enso.projectmanager.service.{
ProjectServiceApi,
ProjectServiceFailure
}
import scala.concurrent.duration.FiniteDuration
/**
* A request handler for `project/close` commands.
*
* @param clientId the requester id
* @param service a project service
* @param requestTimeout a request timeout
*/
class ProjectCloseHandler[F[+_, +_]: Exec](
clientId: UUID,
service: ProjectServiceApi[F],
requestTimeout: FiniteDuration
) extends Actor
with ActorLogging {
override def receive: Receive = requestStage
import context.dispatcher
private def requestStage: Receive = {
case Request(ProjectClose, id, params: ProjectClose.Params) =>
Exec[F]
.exec(service.closeProject(clientId, params.projectId))
.pipeTo(self)
val cancellable =
context.system.scheduler
.scheduleOnce(requestTimeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(s"Failure during $ProjectClose operation:", ex)
replyTo ! ResponseError(Some(id), ServiceError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $ProjectClose with $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case Left(failure: ProjectServiceFailure) =>
log.error(s"Request $id failed due to $failure")
replyTo ! ResponseError(Some(id), mapFailure(failure))
cancellable.cancel()
context.stop(self)
case Right(()) =>
replyTo ! ResponseResult(ProjectClose, id, Unused)
cancellable.cancel()
context.stop(self)
}
}
object ProjectCloseHandler {
/**
* Creates a configuration object used to create a [[ProjectCloseHandler]].
*
* @param clientId the requester id
* @param service a project service
* @param requestTimeout a request timeout
* @return a configuration object
*/
def props[F[+_, +_]: Exec](
clientId: UUID,
service: ProjectServiceApi[F],
requestTimeout: FiniteDuration
): Props =
Props(new ProjectCloseHandler(clientId, service, requestTimeout))
}

View File

@ -0,0 +1,97 @@
package org.enso.projectmanager.requesthandler
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import akka.pattern.pipe
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectOpen
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
import org.enso.projectmanager.service.{
ProjectServiceApi,
ProjectServiceFailure
}
import scala.concurrent.duration.FiniteDuration
/**
* A request handler for `project/open` commands.
*
* @param clientId the requester id
* @param service a project service
* @param requestTimeout a request timeout
*/
class ProjectOpenHandler[F[+_, +_]: Exec](
clientId: UUID,
service: ProjectServiceApi[F],
requestTimeout: FiniteDuration
) extends Actor
with ActorLogging {
override def receive: Receive = requestStage
import context.dispatcher
private def requestStage: Receive = {
case Request(ProjectOpen, id, params: ProjectOpen.Params) =>
Exec[F].exec(service.openProject(clientId, params.projectId)).pipeTo(self)
val cancellable =
context.system.scheduler
.scheduleOnce(requestTimeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(s"Failure during $ProjectOpen operation:", ex)
replyTo ! ResponseError(Some(id), ServiceError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $ProjectOpen with $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case Left(failure: ProjectServiceFailure) =>
log.error(s"Request $id failed due to $failure")
replyTo ! ResponseError(Some(id), mapFailure(failure))
cancellable.cancel()
context.stop(self)
case Right(socket: SocketData) =>
replyTo ! ResponseResult(
ProjectOpen,
id,
ProjectOpen.Result(socket)
)
cancellable.cancel()
context.stop(self)
}
}
object ProjectOpenHandler {
/**
* Creates a configuration object used to create a [[ProjectOpenHandler]].
*
* @param clientId the requester id
* @param service a project service
* @param requestTimeout a request timeout
* @return a configuration object
*/
def props[F[+_, +_]: Exec](
clientId: UUID,
service: ProjectServiceApi[F],
requestTimeout: FiniteDuration
): Props =
Props(new ProjectOpenHandler(clientId, service, requestTimeout))
}

View File

@ -1,13 +1,13 @@
package org.enso.projectmanager.requesthandler
import org.enso.jsonrpc.Error
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.projectmanager.protocol.ProjectManagementApi._
import org.enso.projectmanager.service.ProjectServiceFailure
import org.enso.projectmanager.service.ProjectServiceFailure.{
DataStoreFailure,
ProjectExists,
ProjectNotFound,
ValidationFailure
ProjectNotOpen,
ProjectOpenByOtherPeers,
_
}
object ProjectServiceFailureMapper {
@ -16,10 +16,16 @@ object ProjectServiceFailureMapper {
* Maps project service failures to JSON RPC errors.
*/
val mapFailure: ProjectServiceFailure => Error = {
case ValidationFailure(msg) => ProjectNameValidationError(msg)
case DataStoreFailure(msg) => ProjectDataStoreError(msg)
case ProjectExists => ProjectExistsError
case ProjectNotFound => ProjectNotFoundError
case ValidationFailure(msg) => ProjectNameValidationError(msg)
case DataStoreFailure(msg) => ProjectDataStoreError(msg)
case ProjectExists => ProjectExistsError
case ProjectNotFound => ProjectNotFoundError
case ProjectOpenFailed(msg) => ProjectOpenError(msg)
case ProjectCloseFailed(msg) => ProjectCloseError(msg)
case ProjectNotOpen => ProjectNotOpenError
case ProjectOpenByOtherPeers => ProjectOpenByOtherPeersError
case CannotRemoveOpenProject => CannotRemoveOpenProjectError
case ProjectOperationTimeout => ServiceError
}
}

View File

@ -2,8 +2,14 @@ package org.enso.projectmanager.service
import java.util.UUID
import cats.{Bifunctor, MonadError}
import cats.MonadError
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.ErrorChannel
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.data.SocketData
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerService
import org.enso.projectmanager.infrastructure.log.Logging
import org.enso.projectmanager.infrastructure.random.Generator
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
@ -18,17 +24,11 @@ import org.enso.projectmanager.infrastructure.repository.{
}
import org.enso.projectmanager.infrastructure.time.Clock
import org.enso.projectmanager.model.Project
import org.enso.projectmanager.service.ProjectServiceFailure.{
DataStoreFailure,
ProjectExists,
ProjectNotFound
}
import org.enso.projectmanager.service.ProjectServiceFailure._
import org.enso.projectmanager.service.ValidationFailure.{
EmptyName,
NameContainsForbiddenCharacter
}
import org.enso.projectmanager.control.core.syntax._
import cats.implicits._
/**
* Implementation of business logic for project management.
@ -39,12 +39,13 @@ import cats.implicits._
* @param clock a clock
* @param gen a random generator
*/
class ProjectService[F[+_, +_]: Bifunctor: CovariantFlatMap](
class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap](
validator: ProjectValidator[F],
repo: ProjectRepository[F],
log: Logging[F],
clock: Clock[F],
gen: Generator[F]
gen: Generator[F],
languageServerService: LanguageServerService[F]
)(implicit E: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure])
extends ProjectServiceApi[F] {
@ -67,7 +68,7 @@ class ProjectService[F[+_, +_]: Bifunctor: CovariantFlatMap](
creationTime <- clock.nowInUtc()
projectId <- gen.randomUUID()
project = Project(projectId, name, creationTime)
_ <- repo.insertUserProject(project).leftMap(toServiceFailure)
_ <- repo.insertUserProject(project).mapError(toServiceFailure)
_ <- log.info(s"Project $project created.")
} yield projectId
// format: on
@ -83,15 +84,85 @@ class ProjectService[F[+_, +_]: Bifunctor: CovariantFlatMap](
projectId: UUID
): F[ProjectServiceFailure, Unit] =
log.debug(s"Deleting project $projectId.") *>
repo.deleteUserProject(projectId).leftMap(toServiceFailure) *>
ensureProjectIsNotRunning(projectId) *>
repo.deleteUserProject(projectId).mapError(toServiceFailure) *>
log.info(s"Project $projectId deleted.")
private def ensureProjectIsNotRunning(
projectId: UUID
): F[ProjectServiceFailure, Unit] =
languageServerService
.isRunning(projectId)
.mapError(_ => ProjectOperationTimeout)
.flatMap {
case false => CovariantFlatMap[F].pure()
case true => ErrorChannel[F].fail(CannotRemoveOpenProject)
}
/**
* Opens a project. It starts up a Language Server if needed.
*
* @param clientId the requester id
* @param projectId the project id
* @return either failure or a socket of the Language Server
*/
override def openProject(
clientId: UUID,
projectId: UUID
): F[ProjectServiceFailure, SocketData] = {
for {
_ <- log.debug(s"Opening project $projectId")
project <- getUserProject(projectId)
socket <- languageServerService
.start(clientId, project)
.mapError {
case ServerBootTimedOut =>
ProjectOpenFailed("Language server boot timed out")
case ServerBootFailed(th) =>
ProjectOpenFailed(
s"Language server boot failed: ${th.getMessage}"
)
}
} yield socket
}
/**
* Closes a project. Tries to shut down the Language Server.
*
* @param clientId the requester id
* @param projectId the project id
* @return either failure or [[Unit]] representing void success
*/
override def closeProject(
clientId: UUID,
projectId: UUID
): F[ProjectServiceFailure, Unit] = {
log.debug(s"Closing project $projectId") *>
languageServerService.stop(clientId, projectId).mapError {
case FailureDuringStoppage(th) => ProjectCloseFailed(th.getMessage)
case ServerNotRunning => ProjectNotOpen
case CannotDisconnectOtherClients => ProjectOpenByOtherPeers
}
}
private def getUserProject(
projectId: UUID
): F[ProjectServiceFailure, Project] =
repo
.findUserProject(projectId)
.mapError(toServiceFailure)
.flatMap {
case None => ErrorChannel[F].fail(ProjectNotFound)
case Some(project) => CovariantFlatMap[F].pure(project)
}
private def validateExists(
name: String
): F[ProjectServiceFailure, Unit] =
repo
.exists(name)
.leftMap(toServiceFailure)
.mapError(toServiceFailure)
.flatMap { exists =>
if (exists) raiseError(ProjectExists)
else unit
@ -114,7 +185,7 @@ class ProjectService[F[+_, +_]: Bifunctor: CovariantFlatMap](
): F[ProjectServiceFailure, Unit] =
validator
.validateName(name)
.leftMap {
.mapError {
case EmptyName =>
ProjectServiceFailure.ValidationFailure(
"Cannot create project with empty name"

View File

@ -2,6 +2,8 @@ package org.enso.projectmanager.service
import java.util.UUID
import org.enso.projectmanager.data.SocketData
/**
* A contract for the Project Service.
*
@ -25,4 +27,28 @@ trait ProjectServiceApi[F[+_, +_]] {
*/
def deleteUserProject(projectId: UUID): F[ProjectServiceFailure, Unit]
/**
* Opens a project. It starts up a Language Server if needed.
*
* @param clientId the requester id
* @param projectId the project id
* @return either failure or a socket of the Language Server
*/
def openProject(
clientId: UUID,
projectId: UUID
): F[ProjectServiceFailure, SocketData]
/**
* Closes a project. Tries to shut down the Language Server.
*
* @param clientId the requester id
* @param projectId the project id
* @return either failure or [[Unit]] representing void success
*/
def closeProject(
clientId: UUID,
projectId: UUID
): F[ProjectServiceFailure, Unit]
}

View File

@ -31,4 +31,39 @@ object ProjectServiceFailure {
*/
case object ProjectNotFound extends ProjectServiceFailure
/**
* Signals that a failure occurred during project startup.
*
* @param message a failure message
*/
case class ProjectOpenFailed(message: String) extends ProjectServiceFailure
/**
* Signals that a failure occurred during project shutdown.
*
* @param message a failure message
*/
case class ProjectCloseFailed(message: String) extends ProjectServiceFailure
/**
* Signals that operation cannot be executed, because a project is not open.
*/
case object ProjectNotOpen extends ProjectServiceFailure
/**
* Signals that the project cannot be closed, because other clients are
* connected.
*/
case object ProjectOpenByOtherPeers extends ProjectServiceFailure
/**
* Signals that removal of project failed because one client still use it.
*/
case object CannotRemoveOpenProject extends ProjectServiceFailure
/**
* Signals operation timeout.
*/
case object ProjectOperationTimeout extends ProjectServiceFailure
}

View File

@ -13,11 +13,21 @@ import org.enso.projectmanager.infrastructure.file.{
BlockingFileSystem,
SynchronizedFileStorage
}
import org.enso.projectmanager.infrastructure.languageserver.{
LanguageServerRegistry,
LanguageServerRegistryProxy,
LanguageServerService
}
import org.enso.projectmanager.infrastructure.repository.{
ProjectFileRepository,
ProjectIndex
}
import org.enso.projectmanager.main.configuration.StorageConfig
import org.enso.projectmanager.boot.configuration.{
BootloaderConfig,
NetworkConfig,
StorageConfig,
TimeoutConfig
}
import org.enso.projectmanager.service.{MonadicProjectValidator, ProjectService}
import org.enso.projectmanager.test.{ConstGenerator, NopLogging, StoppedClock}
import zio.interop.catz.core._
@ -50,6 +60,12 @@ class BaseServerSpec extends JsonRpcServerTestKit {
userProjectsPath = userProjectDir
)
lazy val bootloaderConfig = BootloaderConfig(3, 1.second)
lazy val timeoutConfig = TimeoutConfig(3.seconds, 3.seconds, 3.seconds)
lazy val netConfig = NetworkConfig("127.0.0.1", 40000, 60000)
implicit val exec = new ZioEnvExec(Runtime.default)
lazy val fileSystem = new BlockingFileSystem(5.seconds)
@ -72,20 +88,30 @@ class BaseServerSpec extends JsonRpcServerTestKit {
lazy val projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()
lazy val languageServerRegistry =
system.actorOf(LanguageServerRegistry.props(netConfig, bootloaderConfig))
lazy val languageServerService =
new LanguageServerRegistryProxy[ZIO[ZEnv, +*, +*]](
languageServerRegistry,
timeoutConfig
)
lazy val projectService =
new ProjectService[ZIO[ZEnv, +*, +*]](
projectValidator,
projectRepository,
new NopLogging[ZEnv],
testClock,
gen
gen,
languageServerService
)
override def clientControllerFactory: ClientControllerFactory = {
new ManagerClientControllerFactory[ZIO[ZEnv, +*, +*]](
system,
projectService,
10.seconds
timeoutConfig
)
}

View File

@ -5,6 +5,7 @@ import java.nio.file.Paths
import java.util.UUID
import io.circe.literal._
import io.circe.parser.parse
class ProjectManagementApiSpec extends BaseServerSpec {
@ -148,6 +149,85 @@ class ProjectManagementApiSpec extends BaseServerSpec {
}
"fail when project is running" in {
val projectName = "to-remove"
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 0,
"params": {
"name": $projectName
}
}
""")
client.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : 0,
"result" : {
"projectId" : $TestUUID
}
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 1,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectMessage()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
"id": 2,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":2,
"error":{
"code":4008,
"message":"Cannot remove open project"
}
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 3,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":3,
"result": null
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
"id": 3,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectMessage()
}
"remove project structure" in {
val projectName = "to-remove"
val projectDir = new File(userProjectDir, projectName)
@ -171,7 +251,7 @@ class ProjectManagementApiSpec extends BaseServerSpec {
}
}
""")
projectDir shouldBe 'directory
projectDir shouldBe Symbol("directory")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
@ -194,4 +274,348 @@ class ProjectManagementApiSpec extends BaseServerSpec {
}
"project/open" must {
"fail when project doesn't exist" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 0,
"params": {
"projectId": ${UUID.randomUUID()}
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error":{
"code":4004,
"message":"Project with the provided id does not exist"
}
}
""")
}
"start the Language Server if not running" in {
val projectName = "to-remove"
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 0,
"params": {
"name": $projectName
}
}
""")
client.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : 0,
"result" : {
"projectId" : $TestUUID
}
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 1,
"params": {
"projectId": $TestUUID
}
}
""")
val Right(openReply) = parse(client.expectMessage())
val socketField = openReply.hcursor
.downField("result")
.downField("languageServerAddress")
val Right(host) = socketField.downField("host").as[String]
val Right(port) = socketField.downField("port").as[Int]
val languageServerClient = new WsTestClient(s"ws://$host:$port")
languageServerClient.send(json"""
{
"jsonrpc": "2.0",
"method": "file/read",
"id": 1,
"params": {
"path": {
"rootId": ${UUID.randomUUID()},
"segments": ["src", "Main.enso"]
}
}
}
""")
languageServerClient.expectJson(json"""
{
"jsonrpc":"2.0",
"id":1,
"error":{"code":1001,"message":"Content root not found"}}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 2,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":2,
"result": null
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
"id": 3,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":3,
"result": null
}
""")
}
"not start new Language Server if one is running" in {
val projectName = "to-remove"
val client1 = new WsTestClient(address)
client1.send(json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 0,
"params": {
"name": $projectName
}
}
""")
client1.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : 0,
"result" : {
"projectId" : $TestUUID
}
}
""")
client1.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 1,
"params": {
"projectId": $TestUUID
}
}
""")
val Right(openReply) = parse(client1.expectMessage())
val socketField = openReply.hcursor
.downField("result")
.downField("languageServerAddress")
val Right(host) = socketField.downField("host").as[String]
val Right(port) = socketField.downField("port").as[Int]
val client2 = new WsTestClient(address)
client2.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 0,
"params": {
"projectId": $TestUUID
}
}
""")
client2.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : 0,
"result" : {
"languageServerAddress" : { "host": $host, "port": $port }
}
}
""")
client1.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 2,
"params": {
"projectId": $TestUUID
}
}
""")
client1.expectJson(json"""
{
"jsonrpc":"2.0",
"id":2,
"error" : {
"code" : 4007,
"message" : "Cannot close project because it is open by other peers"
}
}
""")
client2.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 2,
"params": {
"projectId": $TestUUID
}
}
""")
client2.expectJson(json"""
{
"jsonrpc":"2.0",
"id":2,
"result": null
}
""")
client1.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
"id": 3,
"params": {
"projectId": $TestUUID
}
}
""")
client1.expectJson(json"""
{
"jsonrpc":"2.0",
"id":3,
"result": null
}
""")
}
}
"project/close" must {
"fail when project is not open" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 0,
"params": {
"projectId": ${UUID.randomUUID()}
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error":{
"code":4006,
"message":"Cannot close project that is not open"
}
}
""")
}
"close project when the requester is the only client" in {
val projectName = "to-remove"
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 0,
"params": {
"name": $projectName
}
}
""")
client.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : 0,
"result" : {
"projectId" : $TestUUID
}
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 1,
"params": {
"projectId": $TestUUID
}
}
""")
val Right(openReply) = parse(client.expectMessage())
val socketField = openReply.hcursor
.downField("result")
.downField("languageServerAddress")
val Right(host) = socketField.downField("host").as[String]
val Right(port) = socketField.downField("port").as[Int]
val languageServerClient = new WsTestClient(s"ws://$host:$port")
languageServerClient.send("test")
languageServerClient.expectJson(json"""
{
"jsonrpc" : "2.0",
"id" : null,
"error" : {
"code" : -32700,
"message" : "Parse error"
}
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/close",
"id": 2,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":2,
"result": null
}
""")
languageServerClient.send("test")
languageServerClient.expectNoMessage()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/delete",
"id": 3,
"params": {
"projectId": $TestUUID
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":3,
"result": null
}
""")
}
}
}

View File

@ -649,12 +649,16 @@ interface ProjectOpenRequest {
```typescript
interface ProjectOpenResult {
lsAddress: IPWithSocket;
languageServerAddress: IPWithSocket;
}
```
##### Errors
TBC
- [`ProjectNotFoundError`](#projectnotfounderror) to signal that the project
doesn't exist.
- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with
underlying data store.
- [`ProjectOpenError`](#projectopenerror) to signal failures during server boot.
#### `project/close`
This message requests that the project picker close a specified project. This
@ -679,7 +683,16 @@ interface ProjectCloseRequest {
```
##### Errors
TBC
- [`ProjectNotFoundError`](#projectnotfounderror) to signal that the project
doesn't exist.
- [`ProjectDataStoreError`](#projectdatastoreerror) to signal problems with
underlying data store.
- [`ProjectCloseError`](#projectcloseerror) to signal failures that occurred
during language server stoppage.
- [`ProjectNotOpenError`](#projectnotopenerror) to signal cannot close a project
that is not open.
- [`ProjectOpenByOtherPeersError`](#projectopenbyotherpeerserror) to signal
that cannot close a project that is open by other clients.
#### `project/listRecent`
This message requests that the project picker lists the user's most recently
@ -724,7 +737,9 @@ interface ProjectCreateRequest {
##### Result
```typescript
{}
interface ProjectOpenResponse {
projectId: UUID;
}
```
##### Errors
@ -761,6 +776,8 @@ interface ProjectDeleteRequest {
underlying data store.
- [`ProjectNotFoundError`](#projectnotfounderror) to signal that the project
doesn't exist.
- [`CannotRemoveOpenProjectError`](#cannotremoveopenprojecterror) to signal that
the project cannot be removed, because is open by at least one user.
#### `project/listSample`
@ -2246,6 +2263,57 @@ Signals that the project doesn't exist.
}
```
```
##### `ProjectOpenError`
Signals that the project cannot be open due to boot failures.
```typescript
"error" : {
"code" : 4005,
"message" : "A boot failure."
}
```
##### `ProjectCloseError`
Signals failures during shutdown of a server.
```typescript
"error" : {
"code" : 4009,
"message" : "A shutdown failure."
}
```
##### `ProjectNotOpenError`
Signals that cannot close project that is not open.
```typescript
"error" : {
"code" : 4006,
"message" : "Cannot close project that is not open"
}
```
##### `ProjectOpenByOtherPeersError`
Signals that cannot close a project that is open by other clients.
```typescript
"error" : {
"code" : 4007,
"message" : "Cannot close project because it is open by other peers"
}
```
##### `CannotRemoveOpenProjectError`
Signals that cannot remove open project.
```typescript
"error" : {
"code" : 4008,
"message" : "Cannot remove open project"
}
```
##### `CapabilityNotAcquired`
Signals that requested capability is not acquired.
@ -2253,5 +2321,4 @@ Signals that requested capability is not acquired.
"error" : {
"code" : 5001,
"message" : "Capability not acquired"
}
```
}

View File

@ -0,0 +1,70 @@
package org.enso.languageserver.boot
import akka.http.scaladsl.Http
import com.typesafe.scalalogging.LazyLogging
import org.enso.languageserver.LanguageProtocol
import org.enso.languageserver.boot.LanguageServerComponent.{
ServerStarted,
ServerStopped
}
import scala.concurrent.Future
import scala.concurrent.duration._
/**
* A lifecycle component used to start and stop a Language Server.
*
* @param config a LS config
*/
class LanguageServerComponent(config: LanguageServerConfig)
extends LazyLogging {
@volatile
private var maybeServerState: Option[(MainModule, Http.ServerBinding)] = None
implicit private val ec = config.computeExecutionContext
/**
* Starts asynchronously a server.
*
* @return a notice that the server started successfully
*/
def start(): Future[ServerStarted.type] = {
logger.info("Starting Language Server...")
for {
mainModule <- Future { new MainModule(config) }
_ <- Future { mainModule.languageServer ! LanguageProtocol.Initialize }
binding <- mainModule.server.bind(config.interface, config.port)
_ <- Future { maybeServerState = Some(mainModule, binding) }
_ <- Future {
logger.info(s"Started server at ${config.interface}:${config.port}")
}
} yield ServerStarted
}
/**
* Stops asynchronously a server.
*
* @return a notice that the server stopped successfully
*/
def stop(): Future[ServerStopped.type] =
maybeServerState match {
case None =>
Future.failed(new Exception("Server isn't running"))
case Some((mainModule, binding)) =>
for {
_ <- binding.terminate(10.seconds)
_ <- mainModule.system.terminate()
} yield ServerStopped
}
}
object LanguageServerComponent {
case object ServerStarted
case object ServerStopped
}

View File

@ -1,7 +1,9 @@
package org.enso.languageserver
package org.enso.languageserver.boot
import java.util.UUID
import scala.concurrent.ExecutionContext
/**
* The config of the running Language Server instance.
*
@ -14,5 +16,7 @@ case class LanguageServerConfig(
interface: String,
port: Int,
contentRootUuid: UUID,
contentRootPath: String
contentRootPath: String,
name: String = "language-server",
computeExecutionContext: ExecutionContext = ExecutionContext.global
)

View File

@ -1,4 +1,4 @@
package org.enso.languageserver
package org.enso.languageserver.boot
import java.io.File
import java.net.URI
@ -23,6 +23,7 @@ import org.enso.languageserver.filemanager.{
import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory}
import org.enso.languageserver.runtime.RuntimeConnector
import org.enso.languageserver.text.BufferRegistry
import org.enso.languageserver.LanguageServer
import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo}
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
@ -49,7 +50,13 @@ class MainModule(serverConfig: LanguageServerConfig) {
implicit val versionCalculator: ContentBasedVersioning =
Sha3_224VersionCalculator
implicit val system = ActorSystem()
implicit val system =
ActorSystem(
serverConfig.name,
None,
None,
Some(serverConfig.computeExecutionContext)
)
implicit val materializer = SystemMaterializer.get(system)

View File

@ -1,10 +1,7 @@
package org.enso.runner
import org.enso.languageserver.{
LanguageProtocol,
LanguageServerConfig,
MainModule
}
import org.enso.languageserver.boot.{LanguageServerConfig, MainModule}
import org.enso.languageserver.LanguageProtocol
import scala.concurrent.Await
import scala.concurrent.duration._

View File

@ -6,8 +6,8 @@ import java.util.UUID
import buildinfo.Info
import cats.implicits._
import org.apache.commons.cli.{Option => CliOption, _}
import org.enso.languageserver
import org.enso.languageserver.LanguageServerConfig
import org.enso.languageserver.boot
import org.enso.languageserver.boot.LanguageServerConfig
import org.enso.pkg.Package
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
import org.graalvm.polyglot.Value
@ -254,7 +254,7 @@ object Main {
port <- Either
.catchNonFatal(portString.toInt)
.leftMap(_ => "Port must be integer")
} yield languageserver.LanguageServerConfig(interface, port, rootId, rootPath)
} yield boot.LanguageServerConfig(interface, port, rootId, rootPath)
// format: on
/** Prints the version of the enso executable.