Scaffold the Project Manager (#610)

This commit is contained in:
Łukasz Olczak 2020-03-18 11:41:55 +01:00 committed by GitHub
parent 530d54bce0
commit 2863498da3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 522 additions and 457 deletions

View File

@ -347,9 +347,19 @@ lazy val project_manager = (project in file("common/project-manager"))
)
.settings(
libraryDependencies ++= akka,
libraryDependencies ++= circe
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
// config
"com.typesafe" % "config" % "1.4.0",
"com.github.pureconfig" %% "pureconfig" % "0.12.2",
// logging
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
)
)
.dependsOn(pkg)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
//////////////////////
//// Sub Projects ////
@ -453,6 +463,8 @@ lazy val language_server = (project in file("engine/language-server"))
)
)
.dependsOn(polyglot_api)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
lazy val runtime = (project in file("engine/runtime"))
.configs(Benchmark)
@ -611,3 +623,28 @@ lazy val runner = project
.dependsOn(pkg)
.dependsOn(language_server)
.dependsOn(polyglot_api)
lazy val `json-rpc-server` = project
.in(file("common/json-rpc-server"))
.settings(
libraryDependencies ++= akka,
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
"io.circe" %% "circe-literal" % circeVersion,
akkaTestkit % Test,
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test
)
)
lazy val `json-rpc-server-test` = project
.in(file("common/json-rpc-server-test"))
.settings(
libraryDependencies ++= akka,
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
"io.circe" %% "circe-literal" % circeVersion,
akkaTestkit,
"org.scalatest" %% "scalatest" % "3.2.0-M2"
)
)
.dependsOn(`json-rpc-server`)

View File

@ -1,35 +1,26 @@
package org.enso.languageserver.websocket
import java.nio.file.Files
import java.util.UUID
package org.enso.jsonrpc.test
import akka.NotUsed
import akka.actor.{ActorRef, ActorSystem, PoisonPill, Props}
import akka.actor.{ActorRef, ActorSystem, PoisonPill}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
import akka.stream.OverflowStrategy
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import cats.effect.IO
import io.circe.Json
import io.circe.parser.parse
import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator}
import org.enso.languageserver.{
LanguageProtocol,
LanguageServer,
WebSocketServer
}
import org.enso.languageserver.filemanager.FileSystem
import org.enso.languageserver.runtime.RuntimeConnector
import org.enso.languageserver.text.BufferRegistry
import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach}
import org.enso.jsonrpc.{ClientControllerFactory, JsonRpcServer, Protocol}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach}
import scala.concurrent.Await
import scala.concurrent.duration._
abstract class WebSocketServerTest
/**
* Test kit for testing JSON RPC servers.
*/
abstract class JsonRpcServerTestKit
extends TestKit(ActorSystem("TestSystem"))
with ImplicitSender
with AnyWordSpecLike
@ -44,37 +35,16 @@ abstract class WebSocketServerTest
val interface = "127.0.0.1"
var address: String = _
val testContentRoot = Files.createTempDirectory(null)
val testContentRootId = UUID.randomUUID()
val config = Config(Map(testContentRootId -> testContentRoot.toFile))
testContentRoot.toFile.deleteOnExit()
var server: WebSocketServer = _
var server: JsonRpcServer = _
var binding: Http.ServerBinding = _
def protocol: Protocol
def clientControllerFactory: ClientControllerFactory
override def beforeEach(): Unit = {
val languageServer =
system.actorOf(
Props(new LanguageServer(config, new FileSystem[IO]))
)
languageServer ! LanguageProtocol.Initialize
val bufferRegistry =
system.actorOf(
BufferRegistry.props(languageServer)(Sha3_224VersionCalculator)
)
lazy val capabilityRouter =
system.actorOf(CapabilityRouter.props(bufferRegistry))
lazy val runtimeConnector = system.actorOf(RuntimeConnector.props)
server = new WebSocketServer(
languageServer,
bufferRegistry,
capabilityRouter,
runtimeConnector
)
server = new JsonRpcServer(protocol, clientControllerFactory)
binding = Await.result(server.bind(interface, port = 0), 3.seconds)
address = s"ws://$interface:${binding.localAddress.getPort}"
}

View File

@ -0,0 +1,22 @@
package org.enso.jsonrpc
import java.util.UUID
import akka.actor.ActorRef
/**
* Classes implementing this trait are responsible for creating client
* controllers upon a new connection. An client controller handles
* communications between a single client and the JSON RPC server.
*/
trait ClientControllerFactory {
/**
* Creates a client controller actor.
*
* @param clientId the internal client id.
* @return an actor ref to the client controller
*/
def createClientController(clientId: UUID): ActorRef
}

View File

@ -1,4 +1,5 @@
package org.enso.languageserver.jsonrpc
package org.enso.jsonrpc
import io.circe.Decoder.Result
import io.circe._

View File

@ -1,4 +1,4 @@
package org.enso.languageserver
package org.enso.jsonrpc
import java.util.UUID
@ -6,50 +6,27 @@ import akka.NotUsed
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives.{get, handleWebSocketMessages, path}
import akka.http.scaladsl.server.Route
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.stream.{Materializer, OverflowStrategy}
import org.enso.languageserver.jsonrpc.MessageHandler
import akka.stream.scaladsl.{Flow, Sink, Source}
import scala.concurrent.duration.{FiniteDuration, _}
import scala.concurrent.{ExecutionContext, Future}
object WebSocketServer {
/**
* A configuration object for properties of the WebSocketServer.
*
* @param outgoingBufferSize the number of messages buffered internally
* if the downstream connection is lagging behind.
* @param lazyMessageTimeout the timeout for downloading the whole of a lazy
* stream message from the user.
*/
case class Config(outgoingBufferSize: Int, lazyMessageTimeout: FiniteDuration)
case object Config {
/**
* Creates a default instance of [[Config]].
*
* @return a default config.
*/
def default: Config =
Config(outgoingBufferSize = 10, lazyMessageTimeout = 10.seconds)
}
}
import scala.concurrent.duration._
/**
* Exposes a multi-client Lanugage Server instance over WebSocket connections.
* @param languageServer an instance of a running and initialized Language
* Server.
* Exposes a multi-client JSON RPC Server instance over WebSocket connections.
*
* @param protocol a protocol supported be the server
* @param clientControllerFactory a factory used to create a client controller
* @param config a server config
* @param system an actor system
* @param materializer a materializer
*/
class WebSocketServer(
languageServer: ActorRef,
bufferRegistry: ActorRef,
capabilityRouter: ActorRef,
runtimeConnector: ActorRef,
config: WebSocketServer.Config = WebSocketServer.Config.default
class JsonRpcServer(
protocol: Protocol,
clientControllerFactory: ClientControllerFactory,
config: JsonRpcServer.Config = JsonRpcServer.Config.default
)(
implicit val system: ActorSystem,
implicit val materializer: Materializer
@ -57,27 +34,15 @@ class WebSocketServer(
implicit val ec: ExecutionContext = system.dispatcher
private val newConnectionPath: String = ""
private def newUser(): Flow[Message, Message, NotUsed] = {
val clientId = UUID.randomUUID()
val clientActor =
system.actorOf(
Props(
new ClientController(
clientId,
languageServer,
bufferRegistry,
capabilityRouter
)
)
)
val clientId = UUID.randomUUID()
val clientActor = clientControllerFactory.createClientController(clientId)
val messageHandler =
system.actorOf(
Props(new MessageHandler(ClientApi.protocol, clientActor))
Props(new MessageHandler(protocol, clientActor))
)
clientActor ! ClientApi.WebConnect(messageHandler)
clientActor ! JsonRpcServer.WebConnect(messageHandler)
val incomingMessages: Sink[Message, NotUsed] =
Flow[Message]
@ -117,7 +82,7 @@ class WebSocketServer(
Flow.fromSinkAndSource(incomingMessages, outgoingMessages)
}
private val route: Route = path(newConnectionPath) {
private val route: Route = path(config.path) {
get { handleWebSocketMessages(newUser()) }
}
@ -132,3 +97,35 @@ class WebSocketServer(
def bind(interface: String, port: Int): Future[Http.ServerBinding] =
Http().bindAndHandle(route, interface, port)
}
object JsonRpcServer {
/**
* A configuration object for properties of the JsonRpcServer.
*
* @param outgoingBufferSize the number of messages buffered internally
* if the downstream connection is lagging behind.
* @param lazyMessageTimeout the timeout for downloading the whole of a lazy
* stream message from the user.
* @param path the http path that the server listen to.
*/
case class Config(
outgoingBufferSize: Int,
lazyMessageTimeout: FiniteDuration,
path: String = ""
)
case object Config {
/**
* Creates a default instance of [[Config]].
*
* @return a default config.
*/
def default: Config =
Config(outgoingBufferSize = 10, lazyMessageTimeout = 10.seconds)
}
case class WebConnect(webActor: ActorRef)
}

View File

@ -1,8 +1,8 @@
package org.enso.languageserver.jsonrpc
package org.enso.jsonrpc
import akka.actor.{Actor, ActorRef, Stash}
import io.circe.Json
import org.enso.languageserver.jsonrpc.Errors.InvalidParams
import org.enso.jsonrpc.Errors.InvalidParams
/**
* An actor responsible for passing parsed massages between the web and

View File

@ -1,4 +1,4 @@
package org.enso.languageserver.jsonrpc
package org.enso.jsonrpc
import io.circe.{Decoder, Encoder, Json}

View File

@ -1,31 +1,18 @@
package org.enso.languageserver
package org.enso.jsonrpc
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import io.circe.Json
import io.circe.literal._
import io.circe.parser._
import org.enso.languageserver.jsonrpc.MessageHandler.{Connected, WebMessage}
import org.enso.languageserver.jsonrpc.{
Error,
HasParams,
HasResult,
Id,
MessageHandler,
Method,
Notification,
Protocol,
Request,
ResponseError,
ResponseResult,
Unused
}
import org.enso.jsonrpc.MessageHandler.{Connected, WebMessage}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
import scala.concurrent.duration._
class MessageHandlerTest
class MessageHandlerSpec
extends TestKit(ActorSystem("TestSystem"))
with ImplicitSender
with AnyWordSpecLike

View File

@ -16,4 +16,6 @@ project-manager {
tutorials {
github-organisation = "luna-packages"
}
}
}
akka.http.server.idle-timeout = infinite

View File

@ -1,30 +0,0 @@
package org.enso.projectmanager
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.server.{PathMatcher0, PathMatcher1}
import akka.http.scaladsl.server.PathMatchers.JavaUUID
import org.enso.projectmanager.model.ProjectId
class RouteHelper {
val tutorials: String = "tutorials"
val projects: String = "projects"
val thumb: String = "thumb"
val tutorialsPath: Path = Path / tutorials
val tutorialsPathMatcher: PathMatcher0 = tutorials
val projectsPath: Path = Path / projects
val projectsPathMatcher: PathMatcher0 = projects
def projectPath(id: ProjectId): Path = projectsPath / id.toString
val projectPathMatcher: PathMatcher1[ProjectId] =
(projectsPathMatcher / JavaUUID).map(ProjectId)
def thumbPath(id: ProjectId): Path = projectPath(id) / thumb
val thumbPathMatcher: PathMatcher1[ProjectId] = projectPathMatcher / thumb
def uriFor(base: Uri, path: Path): Uri = base.withPath(path)
}

View File

@ -1,168 +0,0 @@
package org.enso.projectmanager
import java.io.File
import java.util.concurrent.TimeUnit
import akka.actor.ActorSystem
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.scaladsl.AskPattern._
import akka.actor.typed.scaladsl.adapter._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpResponse, StatusCodes, Uri}
import akka.http.scaladsl.server.{Directives, Route}
import akka.util.Timeout
import com.typesafe.config.ConfigFactory
import org.enso.projectmanager.api.{ProjectFactory, ProjectJsonSupport}
import org.enso.projectmanager.model.{Project, ProjectId}
import org.enso.projectmanager.services._
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._
import scala.util.{Failure, Success}
case class Server(
host: String,
port: Int,
repository: ActorRef[ProjectsCommand],
routeHelper: RouteHelper,
apiFactory: ProjectFactory
)(implicit val system: ActorSystem,
implicit val executor: ExecutionContext,
implicit val askTimeout: Timeout)
extends Directives
with ProjectJsonSupport {
implicit val scheduler: Scheduler = system.scheduler.toTyped
def projectDoesNotExistResponse(id: ProjectId): HttpResponse =
HttpResponse(StatusCodes.NotFound, entity = s"Project $id does not exist")
def thumbDoesNotExistResponse: HttpResponse =
HttpResponse(StatusCodes.NotFound, entity = "Thumbnail does not exist")
def withSuccess[T](
fut: Future[T],
errorResponse: HttpResponse = HttpResponse(StatusCodes.InternalServerError)
)(successHandler: T => Route
): Route = {
onComplete(fut) {
case Success(r) => successHandler(r)
case Failure(_) => complete(errorResponse)
}
}
def withProject(id: ProjectId)(route: Project => Route): Route = {
val projectFuture =
repository
.ask(
(ref: ActorRef[GetProjectResponse]) => GetProjectById(id, ref)
)
.map(_.project)
withSuccess(projectFuture) {
case Some(project) => route(project)
case None => complete(projectDoesNotExistResponse(id))
}
}
def listProjectsWith(
reqBuilder: ActorRef[ListProjectsResponse] => ProjectsCommand
)(baseUri: Uri
): Route = {
val projectsFuture = repository.ask(reqBuilder)
withSuccess(projectsFuture) { projectsResponse =>
val response = projectsResponse.projects.toSeq.map {
case (id, project) => apiFactory.fromModel(id, project, baseUri)
}
complete(response)
}
}
def createProject(baseUri: Uri): Route = {
val projectFuture = repository.ask(
(ref: ActorRef[CreateTemporaryResponse]) =>
CreateTemporary("NewProject", ref)
)
withSuccess(projectFuture) { response =>
complete(apiFactory.fromModel(response.id, response.project, baseUri))
}
}
def getThumb(projectId: ProjectId): Route = {
withProject(projectId) { project =>
if (project.pkg.hasThumb) getFromFile(project.pkg.thumbFile)
else complete(thumbDoesNotExistResponse)
}
}
val route: Route = ignoreTrailingSlash {
path(routeHelper.projectsPathMatcher)(
(get & extractUri)(listProjectsWith(ListProjectsRequest)) ~
(post & extractUri)(createProject)
) ~
(get & path(routeHelper.tutorialsPathMatcher) & extractUri)(
listProjectsWith(ListTutorialsRequest)
) ~
(get & path(routeHelper.thumbPathMatcher))(getThumb)
}
def serve: Future[Http.ServerBinding] = {
Http().bindAndHandle(route, host, port)
}
}
object Server {
def main(args: Array[String]): Unit = {
val config = ConfigFactory.load.getConfig("project-manager")
val serverConfig = config.getConfig("server")
val storageConfig = config.getConfig("storage")
val host = serverConfig.getString("host")
val port = serverConfig.getInt("port")
val timeout =
FiniteDuration(
serverConfig.getDuration("timeout").toNanos,
TimeUnit.NANOSECONDS
)
implicit val system: ActorSystem = ActorSystem("project-manager")
implicit val executor: ExecutionContext = system.dispatcher
implicit val askTimeout: Timeout = new Timeout(timeout)
val localProjectsPath =
new File(storageConfig.getString("local-projects-path"))
val tmpProjectsPath = new File(
storageConfig.getString("temporary-projects-path")
)
val tutorialsPath =
new File(storageConfig.getString("tutorials-path"))
val tutorialsCachePath =
new File(storageConfig.getString("tutorials-cache-path"))
val tutorialsDownloader =
TutorialsDownloader(
tutorialsPath,
tutorialsCachePath,
config.getString("tutorials.github-organisation")
)
val storageManager = StorageManager(
localProjectsPath,
tmpProjectsPath,
tutorialsPath
)
val repoActor = system.spawn(
ProjectsService.behavior(storageManager, tutorialsDownloader),
"projects-repository"
)
val routeHelper = new RouteHelper
val apiFactory = ProjectFactory(routeHelper)
val server = Server(host, port, repoActor, routeHelper, apiFactory)
server.serve: Unit
}
}

View File

@ -2,7 +2,7 @@ package org.enso.projectmanager.api
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.Uri
import org.enso.projectmanager.{RouteHelper, model}
import org.enso.projectmanager.model
import org.enso.projectmanager.model.ProjectId
import spray.json.DefaultJsonProtocol
@ -11,24 +11,21 @@ case class Project(
name: String,
path: String,
thumb: Option[String],
persisted: Boolean)
persisted: Boolean
)
case class ProjectFactory(routeHelper: RouteHelper) {
case class ProjectFactory() {
def fromModel(
id: ProjectId,
project: model.Project,
baseUri: Uri
): Project = {
val thumbUri =
if (project.hasThumb)
Some(routeHelper.uriFor(baseUri, routeHelper.thumbPath(id)))
else None
Project(
id.toString,
project.pkg.name,
project.pkg.root.getAbsolutePath,
thumbUri.map(_.toString),
None,
project.isPersistent
)
}

View File

@ -0,0 +1,27 @@
package org.enso.projectmanager.main
import akka.actor.ActorSystem
import akka.stream.SystemMaterializer
import org.enso.jsonrpc.JsonRpcServer
import org.enso.projectmanager.main.configuration.ProjectManagerConfig
import org.enso.projectmanager.protocol.{
JsonRpc,
ManagerClientControllerFactory
}
/**
* A main module containing all components of the project manager.
*
* @param config a server config
*/
class MainModule(config: ProjectManagerConfig) {
implicit val system = ActorSystem()
implicit val materializer = SystemMaterializer.get(system)
lazy val clientControllerFactory = new ManagerClientControllerFactory(system)
lazy val server = new JsonRpcServer(JsonRpc.protocol, clientControllerFactory)
}

View File

@ -0,0 +1,43 @@
package org.enso.projectmanager.main
import com.typesafe.scalalogging.LazyLogging
import org.enso.projectmanager.main.configuration.ProjectManagerConfig
import pureconfig.ConfigSource
import scala.concurrent.Await
import scala.io.StdIn
import scala.concurrent.duration._
import pureconfig.generic.auto._
/**
* Project manager runner containing the main method.
*/
object ProjectManager extends App with LazyLogging {
logger.info("Starting Language Server...")
val config: ProjectManagerConfig =
ConfigSource
.resources("application.conf")
.withFallback(ConfigSource.systemProperties)
.at("project-manager")
.loadOrThrow[ProjectManagerConfig]
val mainModule = new MainModule(config)
val binding =
Await.result(
mainModule.server.bind(config.server.host, config.server.port),
3.seconds
)
logger.info(
s"Started server at ${config.server.host}:${config.server.port}, press enter to kill server"
)
StdIn.readLine()
logger.info("Stopping server...")
binding.unbind()
mainModule.system.terminate()
}

View File

@ -0,0 +1,20 @@
package org.enso.projectmanager.main
object configuration {
/**
* A configuration object for properties of the Project Manager.
*
* @param server a JSON RPC server configuration
*/
case class ProjectManagerConfig(server: ServerConfig)
/**
* 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)
}

View File

@ -0,0 +1,27 @@
package org.enso.projectmanager.protocol
import java.util.UUID
import akka.actor.{Actor, Props}
/**
* An actor handling communications between a single client and the project
* manager.
*
* @param clientId the internal client id.
*/
class ClientController(clientId: UUID) extends Actor {
override def receive: Receive = ???
}
object ClientController {
/**
* Creates a configuration object used to create a [[ClientController]].
*
* @param clientId the internal client id.
* @return a configuration object
*/
def props(clientId: UUID): Props = Props(new ClientController(clientId))
}

View File

@ -0,0 +1,16 @@
package org.enso.projectmanager.protocol
import io.circe.generic.auto._
import org.enso.jsonrpc.Protocol
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectCreate
object JsonRpc {
/**
* A description of supported JSON RPC messages.
*/
lazy val protocol: Protocol =
Protocol.empty
.registerRequest(ProjectCreate)
}

View File

@ -0,0 +1,25 @@
package org.enso.projectmanager.protocol
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import org.enso.jsonrpc.ClientControllerFactory
/**
* Project manager client controller factory.
*
* @param system the actor system
*/
class ManagerClientControllerFactory(system: ActorSystem)
extends ClientControllerFactory {
/**
* Creates a client controller actor.
*
* @param clientId the internal client id.
* @return an actor ref to the client controller
*/
override def createClientController(clientId: UUID): ActorRef =
system.actorOf(ClientController.props(clientId))
}

View File

@ -0,0 +1,24 @@
package org.enso.projectmanager.protocol
import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused}
/**
* The project management JSON RPC API provided by the project manager.
* See [[https://github.com/luna/enso/blob/master/doc/design/engine/engine-services.md]]
* for message specifications.
*/
object ProjectManagementApi {
case object ProjectCreate extends Method("project/create") {
case class Params(name: String)
implicit val hasParams = new HasParams[this.type] {
type Params = ProjectCreate.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type
}
}
}

View File

@ -2,13 +2,11 @@ package org.enso.languageserver
import java.io.File
import java.net.URI
import java.nio.ByteBuffer
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.actor.{ActorSystem, Props}
import akka.stream.SystemMaterializer
import cats.effect.IO
import org.enso.languageserver
import org.enso.jsonrpc.JsonRpcServer
import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data.{
Config,
@ -16,9 +14,10 @@ import org.enso.languageserver.data.{
Sha3_224VersionCalculator
}
import org.enso.languageserver.filemanager.{FileSystem, FileSystemApi}
import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory}
import org.enso.languageserver.runtime.RuntimeConnector
import org.enso.languageserver.text.BufferRegistry
import org.enso.polyglot.{RuntimeApi, LanguageInfo, RuntimeServerInfo}
import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo}
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
@ -74,11 +73,13 @@ class MainModule(serverConfig: LanguageServerConfig) {
})
.build()
lazy val clientControllerFactory = new ServerClientControllerFactory(
languageServer,
bufferRegistry,
capabilityRouter
)
lazy val server =
new WebSocketServer(
languageServer,
bufferRegistry,
capabilityRouter,
runtimeConnector
)
new JsonRpcServer(JsonRpc.protocol, clientControllerFactory)
}

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.capability
import org.enso.languageserver.data.CapabilityRegistration
import org.enso.languageserver.jsonrpc.{HasParams, HasResult, Method, Unused}
import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused}
/**
* The capability JSON RPC API provided by the language server.

View File

@ -1,12 +1,6 @@
package org.enso.languageserver.filemanager
import org.enso.languageserver.jsonrpc.{
Error,
HasParams,
HasResult,
Method,
Unused
}
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
/**
* The file manager JSON RPC API provided by the language server.

View File

@ -9,7 +9,7 @@ import org.enso.languageserver.filemanager.FileManagerApi.{
NotDirectoryError,
OperationTimeoutError
}
import org.enso.languageserver.jsonrpc.Error
import org.enso.jsonrpc.Error
object FileSystemFailureMapper {

View File

@ -1,8 +1,12 @@
package org.enso.languageserver
package org.enso.languageserver.protocol
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
import akka.pattern.ask
import akka.util.Timeout
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.capability.CapabilityApi.{
AcquireCapability,
ForceReleaseCapability,
@ -21,64 +25,22 @@ import org.enso.languageserver.filemanager.{
FileManagerProtocol,
FileSystemFailureMapper
}
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import org.enso.languageserver.requesthandler.{
AcquireCapabilityHandler,
ApplyEditHandler,
CloseFileHandler,
OpenFileHandler,
ReleaseCapabilityHandler,
SaveFileHandler
}
import org.enso.languageserver.text.TextApi.{
ApplyEdit,
CloseFile,
OpenFile,
SaveFile,
TextDidChange
}
import org.enso.languageserver.requesthandler._
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.text.TextProtocol
import scala.concurrent.duration._
import scala.util.{Failure, Success}
/**
* The JSON RPC API provided by the language server.
* See [[https://github.com/luna/enso/blob/master/doc/design/engine/engine-services.md]]
* for message specifications.
*/
object ClientApi {
import io.circe.generic.auto._
val protocol: Protocol = Protocol.empty
.registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability)
.registerRequest(WriteFile)
.registerRequest(ReadFile)
.registerRequest(CreateFile)
.registerRequest(OpenFile)
.registerRequest(CloseFile)
.registerRequest(SaveFile)
.registerRequest(ApplyEdit)
.registerRequest(DeleteFile)
.registerRequest(CopyFile)
.registerRequest(MoveFile)
.registerRequest(ExistsFile)
.registerRequest(TreeFile)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)
.registerNotification(TextDidChange)
case class WebConnect(webActor: ActorRef)
}
/**
* An actor handling communications between a single client and the language
* server.
*
* @param clientId the internal client id.
* @param server the language server actor.
* @param server the language server actor ref.
* @param bufferRegistry a router that dispatches text editing requests
* @param capabilityRouter a router that dispatches capability requests
* @param requestTimeout a request timeout
*/
class ClientController(
val clientId: Client.Id,
@ -114,7 +76,7 @@ class ClientController(
log.warning("Received unknown message: {}", message)
override def receive: Receive = {
case ClientApi.WebConnect(webActor) =>
case JsonRpcServer.WebConnect(webActor) =>
context.system.eventStream
.publish(ClientConnected(Client(clientId, self)))
unstashAll()
@ -343,3 +305,34 @@ class ClientController(
}
}
object ClientController {
/**
* Creates a configuration object used to create a [[ClientController]].
*
* @param clientId the internal client id.
* @param server the language server actor ref.
* @param bufferRegistry a router that dispatches text editing requests
* @param capabilityRouter a router that dispatches capability requests
* @param requestTimeout a request timeout
* @return a configuration object
*/
def props(
clientId: UUID,
server: ActorRef,
bufferRegistry: ActorRef,
capabilityRouter: ActorRef,
requestTimeout: FiniteDuration = 10.seconds
): Props =
Props(
new ClientController(
clientId,
server,
bufferRegistry,
capabilityRouter,
requestTimeout
)
)
}

View File

@ -0,0 +1,38 @@
package org.enso.languageserver.protocol
import io.circe.generic.auto._
import org.enso.jsonrpc.Protocol
import org.enso.languageserver.capability.CapabilityApi.{
AcquireCapability,
ForceReleaseCapability,
GrantCapability,
ReleaseCapability
}
import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.text.TextApi._
object JsonRpc {
/**
* A description of supported JSON RPC messages.
*/
val protocol: Protocol = Protocol.empty
.registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability)
.registerRequest(WriteFile)
.registerRequest(ReadFile)
.registerRequest(CreateFile)
.registerRequest(OpenFile)
.registerRequest(CloseFile)
.registerRequest(SaveFile)
.registerRequest(ApplyEdit)
.registerRequest(DeleteFile)
.registerRequest(CopyFile)
.registerRequest(MoveFile)
.registerRequest(ExistsFile)
.registerRequest(TreeFile)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)
.registerNotification(TextDidChange)
}

View File

@ -0,0 +1,33 @@
package org.enso.languageserver.protocol
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import org.enso.jsonrpc.ClientControllerFactory
/**
* Language server client controller factory.
*
* @param server the language server actor ref
* @param bufferRegistry the buffer registry actor ref
* @param capabilityRouter the capability router actor ref
* @param system the actor system
*/
class ServerClientControllerFactory(
server: ActorRef,
bufferRegistry: ActorRef,
capabilityRouter: ActorRef
)(implicit system: ActorSystem)
extends ClientControllerFactory {
/**
* Creates a client controller actor.
*
* @param clientId the internal client id.
* @return
*/
override def createClientController(clientId: UUID): ActorRef =
system.actorOf(
ClientController.props(clientId, server, bufferRegistry, capabilityRouter)
)
}

View File

@ -1,6 +1,8 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.capability.CapabilityApi.AcquireCapability
import org.enso.languageserver.capability.CapabilityProtocol
import org.enso.languageserver.capability.CapabilityProtocol.{
@ -8,8 +10,6 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
CapabilityAcquisitionBadRequest
}
import org.enso.languageserver.data.{CapabilityRegistration, Client}
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import scala.concurrent.duration.FiniteDuration

View File

@ -1,24 +1,12 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.data.Client
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import org.enso.languageserver.text.TextApi.{
ApplyEdit,
FileNotOpenedError,
InvalidVersionError,
TextEditValidationError,
WriteDeniedError
}
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.text.TextProtocol.{
ApplyEditSuccess,
FileNotOpened,
TextEditInvalidVersion,
TextEditValidationFailed,
WriteDenied
}
import org.enso.languageserver.text.TextProtocol.{ApplyEdit => _, _}
import scala.concurrent.duration.FiniteDuration

View File

@ -1,9 +1,9 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.data.Client
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import org.enso.languageserver.text.TextApi.{CloseFile, FileNotOpenedError}
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.text.TextProtocol.{FileClosed, FileNotOpened}

View File

@ -1,15 +1,10 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.languageserver.data.Client
import org.enso.languageserver.filemanager.FileSystemFailureMapper
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc.{
Id,
Request,
ResponseError,
ResponseResult
}
import org.enso.languageserver.text.TextApi.OpenFile
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.text.TextProtocol.{

View File

@ -1,6 +1,8 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.capability.CapabilityApi.ReleaseCapability
import org.enso.languageserver.capability.CapabilityProtocol
import org.enso.languageserver.capability.CapabilityProtocol.{
@ -8,8 +10,6 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
CapabilityReleased
}
import org.enso.languageserver.data.{CapabilityRegistration, Client}
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import scala.concurrent.duration.FiniteDuration

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.requesthandler
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.data.Client
import org.enso.languageserver.filemanager.FileSystemFailureMapper
import org.enso.languageserver.jsonrpc.Errors.ServiceError
import org.enso.languageserver.jsonrpc._
import org.enso.languageserver.text.TextApi.{
FileNotOpenedError,
InvalidVersionError,
@ -12,13 +12,7 @@ import org.enso.languageserver.text.TextApi.{
WriteDeniedError
}
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.text.TextProtocol.{
FileNotOpened,
FileSaved,
SaveDenied,
SaveFailed,
SaveFileInvalidVersion
}
import org.enso.languageserver.text.TextProtocol._
import scala.concurrent.duration.FiniteDuration

View File

@ -2,13 +2,7 @@ package org.enso.languageserver.text
import org.enso.languageserver.data.CapabilityRegistration
import org.enso.languageserver.filemanager.Path
import org.enso.languageserver.jsonrpc.{
Error,
HasParams,
HasResult,
Method,
Unused
}
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.languageserver.text.editing.model.FileEdit
/**

View File

@ -0,0 +1,48 @@
package org.enso.languageserver.websocket
import java.nio.file.Files
import java.util.UUID
import akka.actor.Props
import cats.effect.IO
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.languageserver.{LanguageProtocol, LanguageServer}
import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator}
import org.enso.languageserver.filemanager.FileSystem
import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory}
import org.enso.languageserver.text.BufferRegistry
class BaseServerTest extends JsonRpcServerTestKit {
val testContentRoot = Files.createTempDirectory(null)
val testContentRootId = UUID.randomUUID()
val config = Config(Map(testContentRootId -> testContentRoot.toFile))
testContentRoot.toFile.deleteOnExit()
override def protocol: Protocol = JsonRpc.protocol
override def clientControllerFactory: ClientControllerFactory = {
val languageServer =
system.actorOf(
Props(new LanguageServer(config, new FileSystem[IO]))
)
languageServer ! LanguageProtocol.Initialize
val bufferRegistry =
system.actorOf(
BufferRegistry.props(languageServer)(Sha3_224VersionCalculator)
)
lazy val capabilityRouter =
system.actorOf(CapabilityRouter.props(bufferRegistry))
new ServerClientControllerFactory(
languageServer,
bufferRegistry,
capabilityRouter
)
}
}

View File

@ -6,7 +6,7 @@ import java.util.UUID
import io.circe.literal._
import org.apache.commons.io.FileUtils
class FileManagerTest extends WebSocketServerTest {
class FileManagerTest extends BaseServerTest {
"File Server" must {
"write textual content to a file" in {

View File

@ -5,7 +5,7 @@ import io.circe.literal._
import org.enso.languageserver.event.BufferClosed
import org.enso.languageserver.filemanager.Path
class TextOperationsTest extends WebSocketServerTest {
class TextOperationsTest extends BaseServerTest {
"text/openFile" must {
"fail opening a file if it does not exist" in {

View File

@ -1,21 +1,10 @@
package org.enso.runner
import java.io.File
import akka.actor.{ActorSystem, Props}
import akka.stream.{ActorMaterializer, SystemMaterializer}
import cats.effect.IO
import org.enso.interpreter.instrument.ReplDebuggerInstrument
import org.enso.languageserver.data.Config
import org.enso.languageserver.filemanager.FileSystem
import org.enso.languageserver.{
LanguageProtocol,
LanguageServer,
LanguageServerConfig,
MainModule,
WebSocketServer
MainModule
}
import org.enso.polyglot.LanguageInfo
import scala.concurrent.Await
import scala.concurrent.duration._
@ -47,6 +36,7 @@ object LanguageServerApp {
s"Started server at ${config.interface}:${config.port}, press enter to kill server"
)
StdIn.readLine()
binding.terminate(10.seconds)
}
}