From 4864d2623f5485c849cffcbb895b182b07b229c1 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 19 Mar 2020 13:47:08 +0300 Subject: [PATCH] Refactor FileManager file commands (#609) * refactor: create Config.scala * WIP FileSystemHandler * doc: update FileSystem list * add: zio FileSystem * update: FileSystemHandler runAsync * add: config timeouts * rename FileSystemHandler to FileSystemManager * add: ZioExec * add: FileManager router * fix: FileManager return FileWiteResult * update: FileSystemApi interface * refactor: FileSystem with Zio * impl: FileManager * impl: cleanup LanguageServer * impl: ReadFileHandler * impl: CreateFileHandler * impl: DeleteFileHandler * impl: CopyFileHandler * impl: MoveFileHandler * impl: ExistsFileHandler * impl: TreeFileHandler * fix: filemanager tests * misc: cleanup * fix: BufferRegistry tests * doc: add misc * doc: misc * feat: add ZioExec parameter to FileManager * feat: FileManager uses FileSystemApi interface * feat: FileSystem has blocking semantics * feat: FileManager props --- build.sbt | 17 +- .../enso/languageserver/LanguageServer.scala | 103 +------- .../org/enso/languageserver/MainModule.scala | 26 +- .../org/enso/languageserver/data/Config.scala | 40 +++ .../languageserver/data/Environment.scala | 23 -- .../org/enso/languageserver/effect/Exec.scala | 116 +++++++++ .../enso/languageserver/effect/package.scala | 9 + .../filemanager/FileManager.scala | 149 +++++++++++ .../filemanager/FileSystem.scala | 246 +++++++----------- .../filemanager/FileSystemApi.scala | 22 +- .../protocol/ClientController.scala | 228 +--------------- .../ServerClientControllerFactory.scala | 6 +- .../requesthandler/file/CopyFileHandler.scala | 60 +++++ .../file/CreateFileHandler.scala | 60 +++++ .../file/DeleteFileHandler.scala | 60 +++++ .../file/ExistsFileHandler.scala | 60 +++++ .../requesthandler/file/MoveFileHandler.scala | 60 +++++ .../requesthandler/file/ReadFileHandler.scala | 60 +++++ .../requesthandler/file/TreeFileHandler.scala | 60 +++++ .../file/WriteFileHandler.scala | 60 +++++ .../runtime/RuntimeConnector.scala | 1 - .../filemanager/FileSystemSpec.scala | 10 +- .../websocket/BaseServerTest.scala | 31 ++- 23 files changed, 977 insertions(+), 530 deletions(-) create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/data/Config.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/effect/Exec.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/effect/package.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManager.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CopyFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CreateFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/DeleteFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ExistsFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/MoveFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ReadFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/TreeFileHandler.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/WriteFileHandler.scala diff --git a/build.sbt b/build.sbt index 9f906f2a445..49a0f3fac73 100644 --- a/build.sbt +++ b/build.sbt @@ -438,17 +438,16 @@ 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", + "ch.qos.logback" % "logback-classic" % "1.2.3", "io.circe" %% "circe-generic-extras" % "0.12.2", - "io.circe" %% "circe-literal" % circeVersion, - "org.typelevel" %% "cats-core" % "2.0.0", - "org.typelevel" %% "cats-effect" % "2.0.0", - "org.bouncycastle" % "bcpkix-jdk15on" % "1.64", - "commons-io" % "commons-io" % "2.6", + "io.circe" %% "circe-literal" % circeVersion, + "org.bouncycastle" % "bcpkix-jdk15on" % "1.64", + "dev.zio" %% "zio" % "1.0.0-RC18-2", akkaTestkit % Test, - "org.scalatest" %% "scalatest" % "3.2.0-M2" % Test, - "org.scalacheck" %% "scalacheck" % "1.14.0" % Test, - "org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided" + "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") diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala b/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala index f9ec4de951b..477b73ad938 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala @@ -1,19 +1,12 @@ package org.enso.languageserver import akka.actor.{Actor, ActorLogging, Stash} -import cats.effect.IO import org.enso.languageserver.data._ import org.enso.languageserver.event.{ ClientConnected, ClientDisconnected, ClientEvent } -import org.enso.languageserver.filemanager.FileManagerProtocol._ -import org.enso.languageserver.filemanager.{ - DirectoryTree, - FileSystemApi, - FileSystemObject -} object LanguageProtocol { @@ -27,14 +20,14 @@ object LanguageProtocol { * * @param config the configuration used by this Language Server. */ -class LanguageServer(config: Config, fs: FileSystemApi[IO]) +class LanguageServer(config: Config) extends Actor with Stash with ActorLogging { import LanguageProtocol._ override def preStart(): Unit = { - context.system.eventStream.subscribe(self, classOf[ClientEvent]) + context.system.eventStream.subscribe(self, classOf[ClientEvent]): Unit } override def receive: Receive = { @@ -58,97 +51,5 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO]) case ClientDisconnected(clientId) => log.info("Client disconnected [{}].", clientId) context.become(initialized(config, env.removeClient(clientId))) - - case WriteFile(path, content) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - _ <- fs.write(path.toFile(rootPath), content).unsafeRunSync() - } yield () - - sender ! WriteFileResult(result) - - case ReadFile(path) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - content <- fs.read(path.toFile(rootPath)).unsafeRunSync() - } yield content - - sender ! ReadFileResult(result) - - case CreateFile(FileSystemObject.File(name, path)) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - _ <- fs.createFile(path.toFile(rootPath, name)).unsafeRunSync() - } yield () - - sender ! CreateFileResult(result) - - case CreateFile(FileSystemObject.Directory(name, path)) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - _ <- fs.createDirectory(path.toFile(rootPath, name)).unsafeRunSync() - } yield () - - sender ! CreateFileResult(result) - - case DeleteFile(path) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - _ <- fs.delete(path.toFile(rootPath)).unsafeRunSync() - } yield () - - sender ! DeleteFileResult(result) - - case CopyFile(from, to) => - val result = - for { - rootPathFrom <- config.findContentRoot(from.rootId) - rootPathTo <- config.findContentRoot(to.rootId) - _ <- fs - .copy(from.toFile(rootPathFrom), to.toFile(rootPathTo)) - .unsafeRunSync() - } yield () - - sender ! CopyFileResult(result) - - case MoveFile(from, to) => - val result = - for { - rootPathFrom <- config.findContentRoot(from.rootId) - rootPathTo <- config.findContentRoot(to.rootId) - _ <- fs - .move(from.toFile(rootPathFrom), to.toFile(rootPathTo)) - .unsafeRunSync() - } yield () - - sender ! MoveFileResult(result) - - case ExistsFile(path) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - exists <- fs.exists(path.toFile(rootPath)).unsafeRunSync() - } yield exists - - sender ! ExistsFileResult(result) - - case TreeFile(path, depth) => - val result = - for { - rootPath <- config.findContentRoot(path.rootId) - directory <- fs.tree(path.toFile(rootPath), depth).unsafeRunSync() - } yield DirectoryTree.fromDirectoryEntry(rootPath, path, directory) - - sender ! TreeFileResult(result) } - /* Note [Usage of unsafe methods] - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - It invokes side-effecting function, all exceptions are caught and - explicitly returned as left side of disjunction. - */ } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala index ac98cd5bfd8..ecd39ea6a43 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala @@ -5,15 +5,16 @@ import java.net.URI import akka.actor.{ActorSystem, Props} import akka.stream.SystemMaterializer -import cats.effect.IO import org.enso.jsonrpc.JsonRpcServer import org.enso.languageserver.capability.CapabilityRouter import org.enso.languageserver.data.{ Config, ContentBasedVersioning, + FileManagerConfig, Sha3_224VersionCalculator } -import org.enso.languageserver.filemanager.{FileSystem, FileSystemApi} +import org.enso.languageserver.effect.ZioExec +import org.enso.languageserver.filemanager.{FileManager, FileSystem} import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory} import org.enso.languageserver.runtime.RuntimeConnector import org.enso.languageserver.text.BufferRegistry @@ -21,6 +22,8 @@ import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo} import org.graalvm.polyglot.Context import org.graalvm.polyglot.io.MessageEndpoint +import scala.concurrent.duration._ + /** * A main module containing all components of th server. * @@ -29,10 +32,13 @@ import org.graalvm.polyglot.io.MessageEndpoint class MainModule(serverConfig: LanguageServerConfig) { lazy val languageServerConfig = Config( - Map(serverConfig.contentRootUuid -> new File(serverConfig.contentRootPath)) + Map(serverConfig.contentRootUuid -> new File(serverConfig.contentRootPath)), + FileManagerConfig(timeout = 3.seconds) ) - lazy val fileSystem: FileSystemApi[IO] = new FileSystem[IO] + val zioExec = ZioExec(zio.Runtime.default) + + lazy val fileSystem: FileSystem = new FileSystem implicit val versionCalculator: ContentBasedVersioning = Sha3_224VersionCalculator @@ -43,12 +49,17 @@ class MainModule(serverConfig: LanguageServerConfig) { lazy val languageServer = system.actorOf( - Props(new LanguageServer(languageServerConfig, fileSystem)), + Props(new LanguageServer(languageServerConfig)), "server" ) + lazy val fileManager = system.actorOf( + FileManager.pool(languageServerConfig, fileSystem, zioExec), + "file-manager" + ) + lazy val bufferRegistry = - system.actorOf(BufferRegistry.props(languageServer), "buffer-registry") + system.actorOf(BufferRegistry.props(fileManager), "buffer-registry") lazy val capabilityRouter = system.actorOf(CapabilityRouter.props(bufferRegistry), "capability-router") @@ -76,7 +87,8 @@ class MainModule(serverConfig: LanguageServerConfig) { lazy val clientControllerFactory = new ServerClientControllerFactory( languageServer, bufferRegistry, - capabilityRouter + capabilityRouter, + fileManager ) lazy val server = diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/data/Config.scala b/engine/language-server/src/main/scala/org/enso/languageserver/data/Config.scala new file mode 100644 index 00000000000..0668cd4cab8 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/data/Config.scala @@ -0,0 +1,40 @@ +package org.enso.languageserver.data + +import java.io.File +import java.util.UUID + +import org.enso.languageserver.filemanager.{ + ContentRootNotFound, + FileSystemFailure +} + +import scala.concurrent.duration.FiniteDuration + +case class FileManagerConfig(timeout: FiniteDuration, parallelism: Int) + +object FileManagerConfig { + + def apply(timeout: FiniteDuration): FileManagerConfig = + FileManagerConfig( + timeout = timeout, + parallelism = Runtime.getRuntime().availableProcessors() + ) +} + +/** + * The config of the running Language Server instance. + * + * @param contentRoots a mapping between content root id and absolute path to + * the content root + */ +case class Config( + contentRoots: Map[UUID, File], + fileManager: FileManagerConfig +) { + + def findContentRoot(rootId: UUID): Either[FileSystemFailure, File] = + contentRoots + .get(rootId) + .toRight(ContentRootNotFound) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/data/Environment.scala b/engine/language-server/src/main/scala/org/enso/languageserver/data/Environment.scala index a5415ed3862..d1092260067 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/data/Environment.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/data/Environment.scala @@ -1,28 +1,5 @@ package org.enso.languageserver.data -import java.io.File -import java.util.UUID - -import org.enso.languageserver.filemanager.{ - ContentRootNotFound, - FileSystemFailure -} - -/** - * The config of the running Language Server instance. - * - * @param contentRoots a mapping between content root id and absolute path to - * the content root - */ -case class Config(contentRoots: Map[UUID, File] = Map.empty) { - - def findContentRoot(rootId: UUID): Either[FileSystemFailure, File] = - contentRoots - .get(rootId) - .toRight(ContentRootNotFound) - -} - /** * The state of the running Language Server instance. * diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/effect/Exec.scala b/engine/language-server/src/main/scala/org/enso/languageserver/effect/Exec.scala new file mode 100644 index 00000000000..60ce72a9d55 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/effect/Exec.scala @@ -0,0 +1,116 @@ +package org.enso.languageserver.effect + +import java.util.concurrent.{ExecutionException, TimeoutException} + +import zio._ + +import scala.concurrent.{Future, Promise} +import scala.concurrent.duration.FiniteDuration + +/** + * Abstract entity that executes effects `F`. + */ +trait Exec[-F[_, _]] { + + /** + * Execute Zio effect. + * + * @param op effect to execute + * @return a future containing either a failure or a result + */ + def exec[E, A](op: F[E, A]): Future[Either[E, A]] + + /** + * Execute Zio effect with timeout. + * + * @param timeout execution timeout + * @param op effect to execute + * @return a future + */ + def execTimed[E, A]( + timeout: FiniteDuration, + op: ZIO[ZEnv, E, A] + ): Future[Either[E, A]] +} + +/** + * Executor of [[ZIO]] effects. + * + * @param runtime zio runtime + */ +case class ZioExec(runtime: Runtime[ZEnv]) extends Exec[ZioExec.IO] { + + /** + * Execute Zio effect. + * + * @param op effect to execute + * @return a future containing either a failure or a result + */ + override def exec[E, A](op: ZIO[ZEnv, E, A]): Future[Either[E, A]] = { + val promise = Promise[Either[E, A]] + runtime.unsafeRunAsync(op) { + _.fold( + ZioExec.completeFailure(promise, _), + ZioExec.completeSuccess(promise, _) + ) + } + promise.future + } + + /** + * Execute Zio effect with timeout. + * + * @param timeout execution timeout + * @param op effect to execute + * @return a future. On timeout future is failed with [[TimeoutException]]. + * Otherwise future contains either a failure or a result. + */ + override def execTimed[E, A]( + timeout: FiniteDuration, + op: ZIO[ZEnv, E, A] + ): Future[Either[E, A]] = { + val promise = Promise[Either[E, A]] + runtime.unsafeRunAsync( + op.disconnect.timeout(zio.duration.Duration.fromScala(timeout)) + ) { + _.fold( + ZioExec.completeFailure(promise, _), + _.fold(promise.failure(ZioExec.timeoutFailure))( + a => promise.success(Right(a)) + ) + ) + } + promise.future + } +} + +object ZioExec { + + type IO[+E, +A] = ZIO[ZEnv, E, A] + + object ZioExecutionException extends ExecutionException + + private def completeSuccess[E, A]( + promise: Promise[Either[E, A]], + result: A + ): Unit = + promise.success(Right(result)) + + private def completeFailure[E, A]( + promise: Promise[Either[E, A]], + cause: Cause[E] + ): Unit = + cause.failureOption match { + case Some(e) => + promise.success(Left(e)) + case None => + val error = cause.defects.headOption.getOrElse(executionFailure) + promise.failure(error) + } + + private val executionFailure: Throwable = + new ExecutionException("ZIO execution failed", ZioExecutionException) + + private val timeoutFailure: Throwable = + new TimeoutException() +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/effect/package.scala b/engine/language-server/src/main/scala/org/enso/languageserver/effect/package.scala new file mode 100644 index 00000000000..0733f6ba339 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/effect/package.scala @@ -0,0 +1,9 @@ +package org.enso.languageserver + +import zio._ +import zio.blocking.Blocking + +package object effect { + + type BlockingIO[+E, +A] = ZIO[Blocking, E, A] +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManager.scala new file mode 100644 index 00000000000..ceb0001b2f6 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManager.scala @@ -0,0 +1,149 @@ +package org.enso.languageserver.filemanager + +import akka.actor.{Actor, Props} +import akka.routing.SmallestMailboxPool +import akka.pattern.pipe +import org.enso.languageserver.effect._ +import org.enso.languageserver.data.Config +import zio._ + +/** + * Handles the [[FileManagerProtocol]] messages, executes the [[FileSystem]] + * effects and forms the responses. + * + * @param config configuration + * @param fs an instance of a [[FileSyste]] that creates the effects + * @param exec effects executor + */ +class FileManager( + config: Config, + fs: FileSystemApi[BlockingIO], + exec: Exec[BlockingIO] +) extends Actor { + + import context.dispatcher + + override def receive: Receive = { + case FileManagerProtocol.WriteFile(path, content) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + _ <- fs.write(path.toFile(rootPath), content) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.WriteFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.ReadFile(path) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + content <- fs.read(path.toFile(rootPath)) + } yield content + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.ReadFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.CreateFile(FileSystemObject.File(name, path)) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + _ <- fs.createFile(path.toFile(rootPath, name)) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.CreateFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.CreateFile( + FileSystemObject.Directory(name, path) + ) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + _ <- fs.createDirectory(path.toFile(rootPath, name)) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.CreateFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.DeleteFile(path) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + _ <- fs.delete(path.toFile(rootPath)) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.DeleteFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.CopyFile(from, to) => + val result = + for { + rootPathFrom <- IO.fromEither(config.findContentRoot(from.rootId)) + rootPathTo <- IO.fromEither(config.findContentRoot(to.rootId)) + _ <- fs.copy(from.toFile(rootPathFrom), to.toFile(rootPathTo)) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.CopyFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.MoveFile(from, to) => + val result = + for { + rootPathFrom <- IO.fromEither(config.findContentRoot(from.rootId)) + rootPathTo <- IO.fromEither(config.findContentRoot(to.rootId)) + _ <- fs.move(from.toFile(rootPathFrom), to.toFile(rootPathTo)) + } yield () + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.MoveFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.ExistsFile(path) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + exists <- fs.exists(path.toFile(rootPath)) + } yield exists + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.ExistsFileResult) + .pipeTo(sender()) + () + + case FileManagerProtocol.TreeFile(path, depth) => + val result = + for { + rootPath <- IO.fromEither(config.findContentRoot(path.rootId)) + directory <- fs.tree(path.toFile(rootPath), depth) + } yield DirectoryTree.fromDirectoryEntry(rootPath, path, directory) + exec + .execTimed(config.fileManager.timeout, result) + .map(FileManagerProtocol.TreeFileResult) + .pipeTo(sender()) + () + } +} + +object FileManager { + + def props(config: Config, fs: FileSystem, exec: Exec[BlockingIO]): Props = + Props(new FileManager(config, fs, exec)) + + def pool(config: Config, fs: FileSystem, exec: Exec[BlockingIO]): Props = + SmallestMailboxPool(config.fileManager.parallelism) + .props(props(config, fs, exec)) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala index dc4b7500c7a..9c0a74e6b41 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala @@ -1,12 +1,12 @@ package org.enso.languageserver.filemanager -import java.io.{File, FileNotFoundException, IOException} +import java.io.{File, FileNotFoundException} import java.nio.file._ -import cats.data.EitherT -import cats.effect.Sync -import cats.implicits._ import org.apache.commons.io.{FileExistsException, FileUtils} +import org.enso.languageserver.effect.BlockingIO +import zio._ +import zio.blocking.effectBlocking import scala.collection.mutable @@ -15,7 +15,7 @@ import scala.collection.mutable * * @tparam F represents target monad */ -class FileSystem[F[_]: Sync] extends FileSystemApi[F] { +class FileSystem extends FileSystemApi[BlockingIO] { import FileSystemApi._ @@ -23,20 +23,15 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * Writes textual content to a file. * * @param file path to the file - * @param content a textual content of the file + * @param content a textual content of the file * @return either FileSystemFailure or Unit */ override def write( file: File, content: String - ): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - FileUtils.write(file, content, "UTF-8") - } - .leftMap(errorHandling) - } + ): BlockingIO[FileSystemFailure, Unit] = + effectBlocking(FileUtils.write(file, content, "UTF-8")) + .mapError(errorHandling) /** * Reads the contents of a textual file. @@ -44,14 +39,9 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * @param file path to the file * @return either [[FileSystemFailure]] or the content of a file as a String */ - override def read(file: File): F[Either[FileSystemFailure, String]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - FileUtils.readFileToString(file, "UTF-8") - } - .leftMap(errorHandling) - } + override def read(file: File): BlockingIO[FileSystemFailure, String] = + effectBlocking(FileUtils.readFileToString(file, "UTF-8")) + .mapError(errorHandling) /** * Deletes the specified file or directory recursively. @@ -59,18 +49,14 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * @param file path to the file or directory * @return either [[FileSystemFailure]] or Unit */ - def delete(file: File): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - if (file.isDirectory) { - FileUtils.deleteDirectory(file) - } else { - Files.delete(file.toPath) - } - } - .leftMap(errorHandling) - } + def delete(file: File): BlockingIO[FileSystemFailure, Unit] = + effectBlocking({ + if (file.isDirectory) { + FileUtils.deleteDirectory(file) + } else { + Files.delete(file.toPath) + } + }).mapError(errorHandling) /** * Creates an empty file with parent directory. @@ -78,25 +64,15 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * @param file path to the file * @return */ - override def createFile(file: File): F[Either[FileSystemFailure, Unit]] = { - val op = - for { - _ <- EitherT { createDirectory(file.getParentFile) } - _ <- EitherT { createEmptyFile(file) } - } yield () + override def createFile(file: File): BlockingIO[FileSystemFailure, Unit] = + for { + _ <- createDirectory(file.getParentFile) + _ <- createEmptyFile(file) + } yield () - op.value - } - - private def createEmptyFile(file: File): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - file.createNewFile() - } - .leftMap(errorHandling) - .map(_ => ()) - } + private def createEmptyFile(file: File): BlockingIO[FileSystemFailure, Unit] = + effectBlocking(file.createNewFile(): Unit) + .mapError(errorHandling) /** * Creates a directory, including any necessary but nonexistent parent @@ -107,14 +83,9 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { */ override def createDirectory( file: File - ): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - FileUtils.forceMkdir(file) - } - .leftMap(errorHandling) - } + ): BlockingIO[FileSystemFailure, Unit] = + effectBlocking(FileUtils.forceMkdir(file)) + .mapError(errorHandling) /** * Copy a file or directory recursively. @@ -125,25 +96,19 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * be a directory. * @return either [[FileSystemFailure]] or Unit */ - override def copy( - from: File, - to: File - ): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - if (from.isDirectory && to.isFile) { - Left(FileExists) - } else { - Either - .catchOnly[IOException] { - if (from.isFile && to.isDirectory) { - FileUtils.copyFileToDirectory(from, to) - } else if (from.isDirectory) { - FileUtils.copyDirectory(from, to) - } else { - FileUtils.copyFile(from, to) - } - } - }.leftMap(errorHandling) + override def copy(from: File, to: File): BlockingIO[FileSystemFailure, Unit] = + if (from.isDirectory && to.isFile) { + IO.fail(FileExists) + } else { + effectBlocking({ + if (from.isFile && to.isDirectory) { + FileUtils.copyFileToDirectory(from, to) + } else if (from.isDirectory) { + FileUtils.copyDirectory(from, to) + } else { + FileUtils.copyFile(from, to) + } + }).mapError(errorHandling) } /** @@ -153,24 +118,17 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * @param to a path to the destination * @return either [[FileSystemFailure]] or Unit */ - override def move( - from: File, - to: File - ): F[Either[FileSystemFailure, Unit]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - if (to.isDirectory) { - val createDestDir = false - FileUtils.moveToDirectory(from, to, createDestDir) - } else if (from.isDirectory) { - FileUtils.moveDirectory(from, to) - } else { - FileUtils.moveFile(from, to) - } - } - .leftMap(errorHandling) - } + override def move(from: File, to: File): BlockingIO[FileSystemFailure, Unit] = + effectBlocking({ + if (to.isDirectory) { + val createDestDir = false + FileUtils.moveToDirectory(from, to, createDestDir) + } else if (from.isDirectory) { + FileUtils.moveDirectory(from, to) + } else { + FileUtils.moveFile(from, to) + } + }).mapError(errorHandling) /** * Checks if the specified file exists. @@ -178,40 +136,31 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { * @param file path to the file or directory * @return either [[FileSystemFailure]] or file existence flag */ - override def exists(file: File): F[Either[FileSystemFailure, Boolean]] = - Sync[F].delay { - Either - .catchOnly[IOException] { - Files.exists(file.toPath) - } - .leftMap(errorHandling) - } + override def exists(file: File): BlockingIO[FileSystemFailure, Boolean] = + effectBlocking(Files.exists(file.toPath)) + .mapError(errorHandling) - override def list(path: File): F[Either[FileSystemFailure, Vector[Entry]]] = - Sync[F].delay { - if (path.exists) { - if (path.isDirectory) { - Either - .catchOnly[IOException] { - FileSystem - .list(path.toPath) - .map { - case SymbolicLinkEntry(path, _) => - FileSystem.readSymbolicLink(path) - case entry => entry - } + override def list(path: File): BlockingIO[FileSystemFailure, Vector[Entry]] = + if (path.exists) { + if (path.isDirectory) { + effectBlocking({ + FileSystem + .list(path.toPath) + .map { + case SymbolicLinkEntry(path, _) => + FileSystem.readSymbolicLink(path) + case entry => entry } - .leftMap(errorHandling) - } else { - Left(NotDirectory) - } + }).mapError(errorHandling) } else { - Left(FileNotFound) + IO.fail(NotDirectory) } + } else { + IO.fail(FileNotFound) } /** - * Returns contents of a given path. + * Returns tree of a given path. * * @param path to the file system object * @param depth maximum depth of a directory tree @@ -220,34 +169,30 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] { override def tree( path: File, depth: Option[Int] - ): F[Either[FileSystemFailure, DirectoryEntry]] = { - Sync[F].delay { - val limit = FileSystem.Depth(depth) - if (path.exists && limit.canGoDeeper) { - if (path.isDirectory) { - Either - .catchOnly[IOException] { - val directory = DirectoryEntry.empty(path.toPath) - FileSystem.readDirectoryEntry( - directory, - limit.goDeeper, - Vector(), - mutable.Queue().appendAll(FileSystem.list(path.toPath)), - mutable.Queue() - ) - directory - } - .leftMap(errorHandling) - } else { - Left(NotDirectory) - } + ): BlockingIO[FileSystemFailure, DirectoryEntry] = { + val limit = FileSystem.Depth(depth) + if (path.exists && limit.canGoDeeper) { + if (path.isDirectory) { + effectBlocking({ + val directory = DirectoryEntry.empty(path.toPath) + FileSystem.readDirectoryEntry( + directory, + limit.goDeeper, + Vector(), + mutable.Queue().appendAll(FileSystem.list(path.toPath)), + mutable.Queue() + ) + directory + }).mapError(errorHandling) } else { - Left(FileNotFound) + IO.fail(NotDirectory) } + } else { + IO.fail(FileNotFound) } } - private val errorHandling: IOException => FileSystemFailure = { + private val errorHandling: Throwable => FileSystemFailure = { case _: FileNotFoundException => FileNotFound case _: NoSuchFileException => FileNotFound case _: FileExistsException => FileExists @@ -308,6 +253,9 @@ object FileSystem { visited: Vector[SymbolicLinkEntry] ) + /** + * Read an entry without following the symlinks. + */ private def readEntry(path: Path): Entry = { if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { FileEntry(path) @@ -325,6 +273,9 @@ object FileSystem { } } + /** + * Read the target of a symlink. + */ private def readSymbolicLink(path: Path): Entry = { if (Files.isRegularFile(path)) { FileEntry(path) @@ -339,6 +290,7 @@ object FileSystem { * Returns the entries of the provided path. Symlinks are not resolved. * * @param path to the directory + * @return list of entries */ private def list(path: Path): Vector[Entry] = { def accumulator(acc: Vector[Entry], path: Path): Vector[Entry] = diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala index 571f1e14683..33d5a366b79 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala @@ -10,7 +10,7 @@ import scala.collection.mutable.ArrayBuffer * * @tparam F represents target monad */ -trait FileSystemApi[F[_]] { +trait FileSystemApi[F[_, _]] { import FileSystemApi._ @@ -24,7 +24,7 @@ trait FileSystemApi[F[_]] { def write( file: File, content: String - ): F[Either[FileSystemFailure, Unit]] + ): F[FileSystemFailure, Unit] /** * Reads the contents of a textual file. @@ -32,7 +32,7 @@ trait FileSystemApi[F[_]] { * @param file path to the file * @return either [[FileSystemFailure]] or the content of a file as a String */ - def read(file: File): F[Either[FileSystemFailure, String]] + def read(file: File): F[FileSystemFailure, String] /** * Deletes the specified file or directory recursively. @@ -40,7 +40,7 @@ trait FileSystemApi[F[_]] { * @param file path to the file or directory * @return either [[FileSystemFailure]] or Unit */ - def delete(file: File): F[Either[FileSystemFailure, Unit]] + def delete(file: File): F[FileSystemFailure, Unit] /** * Creates an empty file with parent directory. @@ -48,7 +48,7 @@ trait FileSystemApi[F[_]] { * @param file path to the file * @return */ - def createFile(file: File): F[Either[FileSystemFailure, Unit]] + def createFile(file: File): F[FileSystemFailure, Unit] /** * Creates a directory, including any necessary but nonexistent parent @@ -57,7 +57,7 @@ trait FileSystemApi[F[_]] { * @param file path to the file * @return */ - def createDirectory(file: File): F[Either[FileSystemFailure, Unit]] + def createDirectory(file: File): F[FileSystemFailure, Unit] /** * Copy a file or directory recursively @@ -69,7 +69,7 @@ trait FileSystemApi[F[_]] { def copy( from: File, to: File - ): F[Either[FileSystemFailure, Unit]] + ): F[FileSystemFailure, Unit] /** * Move a file or directory recursively @@ -81,7 +81,7 @@ trait FileSystemApi[F[_]] { def move( from: File, to: File - ): F[Either[FileSystemFailure, Unit]] + ): F[FileSystemFailure, Unit] /** * Checks if the specified file exists. @@ -89,7 +89,7 @@ trait FileSystemApi[F[_]] { * @param file path to the file or directory * @return either [[FileSystemFailure]] or file existence flag */ - def exists(file: File): F[Either[FileSystemFailure, Boolean]] + def exists(file: File): F[FileSystemFailure, Boolean] /** * List contents of a given path. @@ -97,7 +97,7 @@ trait FileSystemApi[F[_]] { * @param path to the file system object * @return either [[FileSystemFailure]] or list of entries */ - def list(path: File): F[Either[FileSystemFailure, Vector[Entry]]] + def list(path: File): F[FileSystemFailure, Vector[Entry]] /** * Returns contents of a given path. @@ -109,7 +109,7 @@ trait FileSystemApi[F[_]] { def tree( path: File, depth: Option[Int] - ): F[Either[FileSystemFailure, DirectoryEntry]] + ): F[FileSystemFailure, DirectoryEntry] } object FileSystemApi { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala index 2df2e1dc9a0..dce961f776f 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala @@ -3,9 +3,7 @@ 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, @@ -17,20 +15,11 @@ import org.enso.languageserver.capability.CapabilityProtocol import org.enso.languageserver.data.Client import org.enso.languageserver.event.{ClientConnected, ClientDisconnected} import org.enso.languageserver.filemanager.FileManagerApi._ -import org.enso.languageserver.filemanager.FileManagerProtocol.{ - CreateFileResult, - WriteFileResult -} -import org.enso.languageserver.filemanager.{ - FileManagerProtocol, - FileSystemFailureMapper -} 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} /** * An actor handling communications between a single client and the language @@ -47,13 +36,12 @@ class ClientController( val server: ActorRef, val bufferRegistry: ActorRef, val capabilityRouter: ActorRef, + val fileManager: ActorRef, requestTimeout: FiniteDuration = 10.seconds ) extends Actor with Stash with ActorLogging { - import context.dispatcher - implicit val timeout = Timeout(requestTimeout) private val client = Client(clientId, self) @@ -69,7 +57,15 @@ class ClientController( .props(bufferRegistry, requestTimeout, client), ApplyEdit -> ApplyEditHandler .props(bufferRegistry, requestTimeout, client), - SaveFile -> SaveFileHandler.props(bufferRegistry, requestTimeout, client) + SaveFile -> SaveFileHandler.props(bufferRegistry, requestTimeout, client), + WriteFile -> file.WriteFileHandler.props(requestTimeout, fileManager), + ReadFile -> file.ReadFileHandler.props(requestTimeout, fileManager), + CreateFile -> file.CreateFileHandler.props(requestTimeout, fileManager), + DeleteFile -> file.DeleteFileHandler.props(requestTimeout, fileManager), + CopyFile -> file.CopyFileHandler.props(requestTimeout, fileManager), + MoveFile -> file.MoveFileHandler.props(requestTimeout, fileManager), + ExistsFile -> file.ExistsFileHandler.props(requestTimeout, fileManager), + TreeFile -> file.TreeFileHandler.props(requestTimeout, fileManager) ) override def unhandled(message: Any): Unit = @@ -101,209 +97,7 @@ class ClientController( case r @ Request(method, _, _) if (requestHandlers.contains(method)) => val handler = context.actorOf(requestHandlers(method)) handler.forward(r) - - case Request(WriteFile, id, params: WriteFile.Params) => - writeFile(webActor, id, params) - - case Request(ReadFile, id, params: ReadFile.Params) => - readFile(webActor, id, params) - - case Request(CreateFile, id, params: CreateFile.Params) => - createFile(webActor, id, params) - - case Request(DeleteFile, id, params: DeleteFile.Params) => - deleteFile(webActor, id, params) - - case Request(CopyFile, id, params: CopyFile.Params) => - copyFile(webActor, id, params) - - case Request(MoveFile, id, params: MoveFile.Params) => - moveFile(webActor, id, params) - - case Request(ExistsFile, id, params: ExistsFile.Params) => - existsFile(webActor, id, params) - - case Request(TreeFile, id, params: TreeFile.Params) => - treeFile(webActor, id, params) } - - private def readFile( - webActor: ActorRef, - id: Id, - params: ReadFile.Params - ): Unit = { - (server ? FileManagerProtocol.ReadFile(params.path)).onComplete { - case Success( - FileManagerProtocol.ReadFileResult(Right(content: String)) - ) => - webActor ! ResponseResult(ReadFile, id, ReadFile.Result(content)) - - case Success(FileManagerProtocol.ReadFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occurred during reading a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def writeFile( - webActor: ActorRef, - id: Id, - params: WriteFile.Params - ): Unit = { - (server ? FileManagerProtocol.WriteFile(params.path, params.contents)) - .onComplete { - case Success(WriteFileResult(Right(()))) => - webActor ! ResponseResult(WriteFile, id, Unused) - - case Success(WriteFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occurred during writing to a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def createFile( - webActor: ActorRef, - id: Id, - params: CreateFile.Params - ): Unit = { - (server ? FileManagerProtocol.CreateFile(params.`object`)) - .onComplete { - case Success(CreateFileResult(Right(()))) => - webActor ! ResponseResult(CreateFile, id, Unused) - - case Success(CreateFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occurred during creating a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def deleteFile( - webActor: ActorRef, - id: Id, - params: DeleteFile.Params - ): Unit = { - (server ? FileManagerProtocol.DeleteFile(params.path)) - .onComplete { - case Success(FileManagerProtocol.DeleteFileResult(Right(()))) => - webActor ! ResponseResult(DeleteFile, id, Unused) - - case Success(FileManagerProtocol.DeleteFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occurred during deleting a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def copyFile( - webActor: ActorRef, - id: Id, - params: CopyFile.Params - ): Unit = { - (server ? FileManagerProtocol.CopyFile(params.from, params.to)) - .onComplete { - case Success(FileManagerProtocol.CopyFileResult(Right(()))) => - webActor ! ResponseResult(CopyFile, id, Unused) - - case Success(FileManagerProtocol.CopyFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occured during copying a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def moveFile( - webActor: ActorRef, - id: Id, - params: MoveFile.Params - ): Unit = { - (server ? FileManagerProtocol.MoveFile(params.from, params.to)) - .onComplete { - case Success(FileManagerProtocol.MoveFileResult(Right(()))) => - webActor ! ResponseResult(MoveFile, id, Unused) - - case Success(FileManagerProtocol.MoveFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occured during moving a file", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def existsFile( - webActor: ActorRef, - id: Id, - params: ExistsFile.Params - ): Unit = { - (server ? FileManagerProtocol.ExistsFile(params.path)) - .onComplete { - case Success(FileManagerProtocol.ExistsFileResult(Right(exists))) => - webActor ! ResponseResult(ExistsFile, id, ExistsFile.Result(exists)) - - case Success(FileManagerProtocol.ExistsFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occurred during exists file command", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - - private def treeFile( - webActor: ActorRef, - id: Id, - params: TreeFile.Params - ): Unit = { - (server ? FileManagerProtocol.TreeFile(params.path, params.depth)) - .onComplete { - case Success(FileManagerProtocol.TreeFileResult(Right(tree))) => - webActor ! ResponseResult(TreeFile, id, TreeFile.Result(tree)) - - case Success(FileManagerProtocol.TreeFileResult(Left(failure))) => - webActor ! ResponseError( - Some(id), - FileSystemFailureMapper.mapFailure(failure) - ) - - case Failure(th) => - log.error("An exception occured during a tree operation", th) - webActor ! ResponseError(Some(id), ServiceError) - } - } - } object ClientController { @@ -323,6 +117,7 @@ object ClientController { server: ActorRef, bufferRegistry: ActorRef, capabilityRouter: ActorRef, + fileManager: ActorRef, requestTimeout: FiniteDuration = 10.seconds ): Props = Props( @@ -331,6 +126,7 @@ object ClientController { server, bufferRegistry, capabilityRouter, + fileManager, requestTimeout ) ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala index 64c4c65863d..3a280414ea6 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala @@ -16,7 +16,8 @@ import org.enso.jsonrpc.ClientControllerFactory class ServerClientControllerFactory( server: ActorRef, bufferRegistry: ActorRef, - capabilityRouter: ActorRef + capabilityRouter: ActorRef, + fileManager: ActorRef )(implicit system: ActorSystem) extends ClientControllerFactory { @@ -28,6 +29,7 @@ class ServerClientControllerFactory( */ override def createClientController(clientId: UUID): ActorRef = system.actorOf( - ClientController.props(clientId, server, bufferRegistry, capabilityRouter) + ClientController + .props(clientId, server, bufferRegistry, capabilityRouter, fileManager) ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CopyFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CopyFileHandler.scala new file mode 100644 index 00000000000..fec3dfb891f --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CopyFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.CopyFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class CopyFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(CopyFile, id, params: CopyFile.Params) => + fileManager ! FileManagerProtocol.CopyFile(params.from, params.to) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $CopyFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.CopyFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.CopyFileResult(Right(())) => + replyTo ! ResponseResult(CopyFile, id, Unused) + context.stop(self) + } +} + +object CopyFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new CopyFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CreateFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CreateFileHandler.scala new file mode 100644 index 00000000000..2d956b99ac9 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/CreateFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.CreateFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class CreateFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(CreateFile, id, params: CreateFile.Params) => + fileManager ! FileManagerProtocol.CreateFile(params.`object`) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $CreateFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.CreateFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.CreateFileResult(Right(())) => + replyTo ! ResponseResult(CreateFile, id, Unused) + context.stop(self) + } +} + +object CreateFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new CreateFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/DeleteFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/DeleteFileHandler.scala new file mode 100644 index 00000000000..73aecdf1f52 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/DeleteFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.DeleteFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class DeleteFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(DeleteFile, id, params: DeleteFile.Params) => + fileManager ! FileManagerProtocol.DeleteFile(params.path) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $DeleteFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.DeleteFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.DeleteFileResult(Right(())) => + replyTo ! ResponseResult(DeleteFile, id, Unused) + context.stop(self) + } +} + +object DeleteFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new DeleteFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ExistsFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ExistsFileHandler.scala new file mode 100644 index 00000000000..9ccc7b3e5e9 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ExistsFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.ExistsFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class ExistsFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(ExistsFile, id, params: ExistsFile.Params) => + fileManager ! FileManagerProtocol.ExistsFile(params.path) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $ExistsFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.ExistsFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.ExistsFileResult(Right(result)) => + replyTo ! ResponseResult(ExistsFile, id, ExistsFile.Result(result)) + context.stop(self) + } +} + +object ExistsFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new ExistsFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/MoveFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/MoveFileHandler.scala new file mode 100644 index 00000000000..9c87f9b6734 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/MoveFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.MoveFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class MoveFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(MoveFile, id, params: MoveFile.Params) => + fileManager ! FileManagerProtocol.MoveFile(params.from, params.to) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $MoveFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.MoveFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.MoveFileResult(Right(())) => + replyTo ! ResponseResult(MoveFile, id, Unused) + context.stop(self) + } +} + +object MoveFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new MoveFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ReadFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ReadFileHandler.scala new file mode 100644 index 00000000000..f016ccb4df2 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/ReadFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.ReadFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class ReadFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(ReadFile, id, params: ReadFile.Params) => + fileManager ! FileManagerProtocol.ReadFile(params.path) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $ReadFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.ReadFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.ReadFileResult(Right(content)) => + replyTo ! ResponseResult(ReadFile, id, ReadFile.Result(content)) + context.stop(self) + } +} + +object ReadFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new ReadFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/TreeFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/TreeFileHandler.scala new file mode 100644 index 00000000000..bbcc95aa517 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/TreeFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.TreeFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class TreeFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(TreeFile, id, params: TreeFile.Params) => + fileManager ! FileManagerProtocol.TreeFile(params.path, params.depth) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $TreeFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.TreeFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.TreeFileResult(Right(result)) => + replyTo ! ResponseResult(TreeFile, id, TreeFile.Result(result)) + context.stop(self) + } +} + +object TreeFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new TreeFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/WriteFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/WriteFileHandler.scala new file mode 100644 index 00000000000..e87755885c8 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/file/WriteFileHandler.scala @@ -0,0 +1,60 @@ +package org.enso.languageserver.requesthandler.file + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.{ + FileManagerProtocol, + FileSystemFailureMapper +} +import org.enso.languageserver.filemanager.FileManagerApi.WriteFile +import org.enso.languageserver.requesthandler.RequestTimeout + +import scala.concurrent.duration.FiniteDuration + +class WriteFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef) + extends Actor + with ActorLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(WriteFile, id, params: WriteFile.Params) => + fileManager ! FileManagerProtocol.WriteFile(params.path, params.contents) + context.system.scheduler + .scheduleOnce(requestTimeout, self, RequestTimeout) + context.become(responseStage(id, sender())) + } + + private def responseStage(id: Id, replyTo: ActorRef): Receive = { + case Status.Failure(ex) => + log.error(s"Failure during $WriteFile operation:", ex) + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case RequestTimeout => + log.error(s"Request $id timed out") + replyTo ! ResponseError(Some(id), ServiceError) + context.stop(self) + + case FileManagerProtocol.WriteFileResult(Left(failure)) => + replyTo ! ResponseError( + Some(id), + FileSystemFailureMapper.mapFailure(failure) + ) + context.stop(self) + + case FileManagerProtocol.WriteFileResult(Right(())) => + replyTo ! ResponseResult(WriteFile, id, Unused) + context.stop(self) + } +} + +object WriteFileHandler { + + def props(timeout: FiniteDuration, fileManager: ActorRef): Props = + Props(new WriteFileHandler(timeout, fileManager)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala index f00dd6705e3..33a4dcdda01 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/RuntimeConnector.scala @@ -1,7 +1,6 @@ package org.enso.languageserver.runtime import java.nio.ByteBuffer -import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import org.enso.languageserver.runtime.RuntimeConnector.Destroy diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala index 5e2356004eb..150366f1f33 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala @@ -2,12 +2,14 @@ package org.enso.languageserver.filemanager import java.nio.file.{Files, Path, Paths} -import cats.effect.IO +import org.enso.languageserver.effect.ZioExec import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.io.Source import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Await +import scala.concurrent.duration._ class FileSystemSpec extends AnyFlatSpec with Matchers { @@ -538,8 +540,12 @@ class FileSystemSpec extends AnyFlatSpec with Matchers { val testDir = testDirPath.toFile testDir.deleteOnExit() - val objectUnderTest = new FileSystem[IO] + val objectUnderTest = new FileSystem } + implicit final class UnsafeRunZio[E, A](io: zio.ZIO[zio.ZEnv, E, A]) { + def unsafeRunSync(): Either[E, A] = + Await.result(ZioExec(zio.Runtime.default).exec(io), 3.seconds) + } } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala index 3613c9cac0e..e91ed3f291b 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala @@ -4,44 +4,53 @@ 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.effect.ZioExec import org.enso.languageserver.capability.CapabilityRouter -import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator} -import org.enso.languageserver.filemanager.FileSystem +import org.enso.languageserver.data.{ + Config, + FileManagerConfig, + Sha3_224VersionCalculator +} +import org.enso.languageserver.filemanager.{FileManager, FileSystem} import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory} import org.enso.languageserver.text.BufferRegistry +import scala.concurrent.duration._ + class BaseServerTest extends JsonRpcServerTestKit { val testContentRoot = Files.createTempDirectory(null) val testContentRootId = UUID.randomUUID() - val config = Config(Map(testContentRootId -> testContentRoot.toFile)) + val config = Config( + Map(testContentRootId -> testContentRoot.toFile), + FileManagerConfig(timeout = 3.seconds) + ) testContentRoot.toFile.deleteOnExit() override def protocol: Protocol = JsonRpc.protocol override def clientControllerFactory: ClientControllerFactory = { - val languageServer = - system.actorOf( - Props(new LanguageServer(config, new FileSystem[IO])) - ) + val zioExec = ZioExec(zio.Runtime.default) + val languageServer = system.actorOf(Props(new LanguageServer(config))) languageServer ! LanguageProtocol.Initialize + val fileManager = + system.actorOf(FileManager.props(config, new FileSystem, zioExec)) val bufferRegistry = system.actorOf( - BufferRegistry.props(languageServer)(Sha3_224VersionCalculator) + BufferRegistry.props(fileManager)(Sha3_224VersionCalculator) ) - lazy val capabilityRouter = system.actorOf(CapabilityRouter.props(bufferRegistry)) new ServerClientControllerFactory( languageServer, bufferRegistry, - capabilityRouter + capabilityRouter, + fileManager ) }