package wow.doge.mygame.subsystems.scriptsystem import javax.script.ScriptEngine import javax.script.ScriptEngineManager import javax.script.ScriptException import scala.concurrent.duration._ import ammonite.main.Defaults import ammonite.runtime.Storage.Folder import ammonite.util.Res import ammonite.util.Res.Failure import cats.effect.Resource import cats.effect.concurrent.Deferred import cats.syntax.either._ import cats.syntax.flatMap._ import com.softwaremill.macwire._ import com.softwaremill.tagging._ import groovy.util.GroovyScriptEngine import io.odin.Logger import monix.bio.IO import monix.bio.Task import monix.bio.UIO import monix.catnap.ConcurrentQueue import monix.reactive.Observable import wow.doge.mygame.implicits._ import monix.{eval => me} import groovy.util.ResourceException import monix.catnap.MVar trait Requestable[A] { protected def queue: ConcurrentQueue[Task, A] def request[T]( compileRequest: Deferred[Task, T] => A )(implicit timeout: FiniteDuration) = for { d <- Deferred[Task, T] req = compileRequest(d) _ <- queue.offer(req) res <- d.get.timeout(timeout).map(_.get) } yield res } class ScriptCompiler private ( _queue: ConcurrentQueue[Task, ScriptCompiler.Command] ) extends Requestable[ScriptCompiler.Command] { override protected def queue = _queue // def tell(item: Command) = queue.offer(item) } object ScriptCompiler { sealed trait State case object Idle extends State case object Active extends State /** * script representation */ sealed trait ScriptTag type ScriptObject = Any @@ ScriptTag sealed trait KotlinEngineTag type KotlinScriptEngine = ScriptEngine @@ KotlinEngineTag sealed trait Error final case class AmmoniteFailure(error: Res.Failure) extends Error final case class AmmoniteException(error: Res.Exception) extends Error final case class ScriptExceptionError(error: ScriptException) extends Error final case class ResourceExceptionError(error: ResourceException) extends Error final case class GroovyScriptExceptionError( error: groovy.util.ScriptException ) extends Error final case class SomeError(reason: String) extends Error sealed trait Command final case class Get( path: os.Path, result: Deferred[Task, ScriptResult], force: Boolean ) extends Command final case class GetData(result: Deferred[Task, Data]) final case class ObservableData(result: Deferred[Task, Observable[Data]]) extends Command // extends Command // final case class CompileAll(paths: Seq[os.Path]) extends Command type ScriptsMap = Map[os.Path, Any] type ScriptResult = Either[Error, Any] sealed trait ScriptType case object ScalaType extends ScriptType case object KotlinType extends ScriptType case object GroovyType extends ScriptType val defaultScalaRunner = ammonite .Main( storageBackend = new Folder( // os.pwd / "target" Defaults.ammoniteHome, isRepl = false ) ) val defaultKotlinRunner: KotlinScriptEngine = { val manager = new ScriptEngineManager() val engine = manager.getEngineByExtension("main.kts") engine.taggedWith[KotlinEngineTag] } val defaultGroovyRunner: GroovyScriptEngine = new GroovyScriptEngine(os.pwd.toString) case class Data(scriptsMap: ScriptsMap) class SourceMaker( queue: ConcurrentQueue[Task, Command], worker: ScriptCompilerWorker ) { import com.softwaremill.quicklens._ def get = for { dataVar <- MVar[Task].of(Data(Map.empty)) obs <- Task.deferAction(implicit s => Task( Observable .repeatEvalF(queue.poll) .scanEval0(me.Task.pure((Active: State) -> Data(Map.empty))) { case state -> data -> command => val nextState: IO[Error, (State, Data)] = state match { case Idle => IO.pure(Idle -> data) case Active => command match { case Get(path, result, force) => def getAndUpdate = worker .request( ScriptCompilerWorker.CompileAny(path, _) )( 20.seconds ) .flatTap(result.complete) .hideErrors .rethrow .flatMap(res => UIO(pprint.log(res)) >> UIO.pure( data .modify(_.scriptsMap) .using(_ + (path -> res)) ) ) for { nextData <- if (force) getAndUpdate else data.scriptsMap.get(path) match { case Some(e) => result .complete(e.asRight[Error]) .hideErrors >> UIO.pure(data) case None => getAndUpdate } } yield Active -> nextData case ObservableData(result) => result .complete(Observable.repeatEvalF(dataVar.take)) .hideErrors >> IO.pure(Active -> data) } } nextState .flatTap { case (_, data) => dataVar.put(data).hideErrors } .tapError(err => UIO(pprint.log(err.toString))) .attempt // .mapFilter(_.toOption) .map(_.getOrElse(state -> data)) .toTask } ) ) } yield obs } class ScriptCompileFns( val scalaRunner: ammonite.Main, val kotlinRunner: KotlinScriptEngine, val groovyRunner: GroovyScriptEngine ) { def runScala(path: os.Path): Either[Error, Any] = scalaRunner .runScript(path, Seq.empty) ._1 match { case e @ Res.Exception(t, msg) => Left(AmmoniteException(e)) case f @ Failure(msg) => Left(AmmoniteFailure(f)) case Res.Success(obj) => Right(obj) case _ => Left(SomeError("Failed to run script")) } def runKotlin(path: os.Path): Either[Error, Any] = Either .catchNonFatal(kotlinRunner.eval(os.read(path))) .leftMap { case ex: ScriptException => ScriptExceptionError(ex) } def runGroovy(path: os.Path): Either[Error, Any] = Either .catchNonFatal(groovyRunner.run(path.relativeTo(os.pwd).toString, "")) .leftMap { case ex: ResourceException => ResourceExceptionError(ex) case ex: groovy.util.ScriptException => GroovyScriptExceptionError(ex) } def ensureReturnedObjectNotNull(scriptObject: Any): Either[Error, Any] = Either.fromOption(Option(scriptObject), SomeError("unknown object")) } class ScriptCompileSource( fns: ScriptCompileFns, logger: Logger[Task], queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest] ) { import fns._ val source = Task.deferAction(implicit s => Task( Observable .repeatEvalF(queue.poll) .doOnNextF(el => logger.debug(s"Got $el")) .mapParallelUnorderedF(4) { case ScriptCompilerWorker.CompileAny(path, result) => for { mbRes <- Task( runScala(path) .flatMap(ensureReturnedObjectNotNull) // .map(_.taggedWith[ScriptTag]) ) _ <- result.complete(mbRes) } yield mbRes } ) ) } // override private val final class ScriptCompilerWorker( logger: Logger[Task], _queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest] ) extends Requestable[ScriptCompilerWorker.CompileRequest] { override def queue = _queue } object ScriptCompilerWorker { sealed trait CompileRequest final case class CompileAny( path: os.Path, result: Deferred[Task, ScriptResult] ) extends CompileRequest def apply( logger: Logger[Task], scalaRunner: ammonite.Main = defaultScalaRunner, kotlinRunner: KotlinScriptEngine = defaultKotlinRunner, groovyRunner: GroovyScriptEngine = defaultGroovyRunner ) = { val acquire = for { queue <- ConcurrentQueue[Task].bounded[CompileRequest](10) fns <- UIO.pure(wire[ScriptCompileFns]) worker = wire[ScriptCompilerWorker] fib <- wire[ScriptCompileSource].source.flatMap(_.completedL.toIO.start) // resource = Concurrent[Task].background( // wire[ScriptCompileSource].source.flatMap(_.completedL.toIO) // ) } yield worker -> fib Resource .make(acquire) { case worker -> fib => fib.cancel } .map(_._1) } } def apply(logger: Logger[Task]) = { def acquire(worker: ScriptCompilerWorker) = for { queue <- ConcurrentQueue.bounded[Task, Command](10) fib <- wire[SourceMaker].get.flatMap(_.completedL.toIO.start) } yield new ScriptCompiler(queue) -> fib ScriptCompilerWorker(logger) .flatMap(worker => Resource.make(acquire(worker)) { case (_, fib) => fib.cancel } ) .map(_._1) } }