mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 02:21:54 +03:00
Implementation of project/open and project/close commands. (#631)
This commit is contained in:
parent
0ffce13894
commit
5c616c2727
41
build.sbt
41
build.sbt
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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)
|
@ -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 {
|
@ -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)
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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]
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package org.enso.projectmanager.data
|
||||
|
||||
case class SocketData(host: String, port: Int)
|
@ -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
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.enso.projectmanager.event
|
||||
|
||||
/**
|
||||
* Base trait for all kinds of events.
|
||||
*/
|
||||
trait Event
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
|
||||
}
|
@ -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))
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -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]
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -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]]
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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))
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
}
|
@ -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))
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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]
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
```
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -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)
|
||||
|
@ -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._
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user