diff --git a/arc-scripting/src/main/kotlin/ScriptHotReload.kt b/arc-scripting/src/main/kotlin/ScriptHotReload.kt index 81ecc0d4..0fb52bb9 100644 --- a/arc-scripting/src/main/kotlin/ScriptHotReload.kt +++ b/arc-scripting/src/main/kotlin/ScriptHotReload.kt @@ -20,6 +20,7 @@ import java.io.IOException import java.nio.file.FileSystems import java.nio.file.StandardWatchEventKinds.* import java.nio.file.WatchService +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors.newCachedThreadPool import java.util.concurrent.Executors.newFixedThreadPool import java.util.concurrent.atomic.AtomicBoolean @@ -35,6 +36,8 @@ class ScriptHotReload( ) : Closeable { private val log = LoggerFactory.getLogger(this.javaClass) + private val agentToFileMappings = ConcurrentHashMap>() + private val fileWatcher = lazy { try { val watchService = FileSystems.getDefault().newWatchService() @@ -52,24 +55,42 @@ class ScriptHotReload( when (event) { is FileEvent.Created -> { if (event.file.isDirectory) return@watch - scriptAgentLoader.loadAgents(event.file) + val loadedAgents = scriptAgentLoader.loadAgents(event.file) scriptFunctionLoader.loadFunctions(event.file) + removeRemovedAgents(loadedAgents) } is FileEvent.Modified -> { if (event.file.isDirectory) return@watch - scriptAgentLoader.loadAgents(event.file) + val loadedAgents = scriptAgentLoader.loadAgents(event.file) scriptFunctionLoader.loadFunctions(event.file) + removeRemovedAgents(loadedAgents) } is FileEvent.Deleted -> { - // TODO + if (event.file.isDirectory) return@watch + removeRemovedAgents(mapOf(event.file to emptySet())) } } }.onFailure { logError(it) } } } + /** + * Removes agents that were removed from the script files. + */ + private fun removeRemovedAgents(loadedAgents: Map>) { + loadedAgents.forEach { (file, agents) -> + val previousAgents = agentToFileMappings[file] ?: emptySet() + val removedAgents = previousAgents - agents + if (removedAgents.isNotEmpty()) { + log.info("Removing agents ${removedAgents.joinToString()} from file ${file.name}") + removedAgents.forEach { scriptAgentLoader.removeAgent(it) } + } + agentToFileMappings[file] = agents + } + } + private fun logError(ex: Exception) { log.error("Unexpected Error while hot-loading Agent scripts!", ex) } diff --git a/arc-scripting/src/main/kotlin/agents/ScriptingAgentLoader.kt b/arc-scripting/src/main/kotlin/agents/ScriptingAgentLoader.kt index 577892e3..d74f1c20 100644 --- a/arc-scripting/src/main/kotlin/agents/ScriptingAgentLoader.kt +++ b/arc-scripting/src/main/kotlin/agents/ScriptingAgentLoader.kt @@ -12,6 +12,7 @@ import org.eclipse.lmos.arc.agents.events.EventPublisher import org.eclipse.lmos.arc.core.Failure import org.eclipse.lmos.arc.core.Result import org.eclipse.lmos.arc.core.Success +import org.eclipse.lmos.arc.core.map import org.eclipse.lmos.arc.core.onFailure import org.eclipse.lmos.arc.scripting.ScriptFailedException import org.slf4j.LoggerFactory @@ -38,15 +39,17 @@ class ScriptingAgentLoader( /** * Loads the agents defined in an Agent DSL script. + * @return a Result containing the names of the loaded agents or an error if the script failed. */ - fun loadAgent(agentScript: String): Result { + fun loadAgent(agentScript: String): Result, ScriptFailedException> { val context = BasicAgentDefinitionContext(agentFactory) val result = agentScriptEngine.eval(agentScript, context) if (result is Success && context.agents.isNotEmpty()) { log.info("Discovered the following agents (scripting): ${context.agents.joinToString { it.name }}") agents.putAll(context.agents.associateBy { it.name }) + return result.map { context.agents.map { it.name }.toSet() } } - return result + return result.map { emptySet() } } fun loadCompiledAgent(compiledAgentScript: CompiledAgentLoader) { @@ -60,18 +63,24 @@ class ScriptingAgentLoader( /** * Loads the agents defined in a list of Agent DSL script files. + * @return a Map containing the file and the names of the loaded agents. */ - fun loadAgents(vararg files: File) { + fun loadAgents(vararg files: File): Map> = buildMap { files .asSequence() .filter { it.name.endsWith(".agent.kts") } - .map { it.name to it.readText() } - .forEach { (name, script) -> + .map { it to it.readText() } + .forEach { (file, script) -> + val name = file.name val result = loadAgent(script).onFailure { log.warn("Failed to load agents from script: $name!", it) } when (result) { - is Success -> eventPublisher?.publish(AgentLoadedEvent(name)) + is Success -> { + put(file, result.value) + eventPublisher?.publish(AgentLoadedEvent(name)) + } + is Failure -> eventPublisher?.publish( AgentLoadedEvent( name, @@ -88,4 +97,12 @@ class ScriptingAgentLoader( fun loadAgentsFromFolder(folder: File) { folder.walk().filter { it.isFile }.forEach { loadAgents(it) } } + + /** + * Removes the agent with the given name. + */ + fun removeAgent(name: String): Boolean { + val agent = agents.remove(name) + return agent != null + } }