forked from nova/jmonkey-test
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
9.7 KiB
310 lines
9.7 KiB
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)
|
|
}
|
|
|
|
}
|