From 8e889b1b34a95b9b4bcf11c56f7b795e83f998b1 Mon Sep 17 00:00:00 2001 From: Aaron Feledy Date: Tue, 11 Jun 2024 14:23:52 -0400 Subject: [PATCH] Add status service --- .../landointellijplugin/LandoExec.kt | 13 +- ...ener.kt => LandoProjectManagerListener.kt} | 10 +- ...viceListener.kt => LandoStatusListener.kt} | 2 +- .../services/LandoAppService.kt | 121 -------------- .../services/LandoProjectService.kt | 136 +++++++++++++--- .../services/LandoStatusService.kt | 152 ++++++++++++++++++ .../resources/messages/LandoBundle.properties | 3 +- 7 files changed, 279 insertions(+), 158 deletions(-) rename src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/{LandoManagerListener.kt => LandoProjectManagerListener.kt} (65%) rename src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/{LandoAppServiceListener.kt => LandoStatusListener.kt} (70%) delete mode 100644 src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoAppService.kt create mode 100644 src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoStatusService.kt diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/LandoExec.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/LandoExec.kt index e182970..bb13da3 100644 --- a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/LandoExec.kt +++ b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/LandoExec.kt @@ -1,9 +1,10 @@ package com.github.aaronfeledy.landointellijplugin -import com.github.aaronfeledy.landointellijplugin.services.LandoAppService +import com.github.aaronfeledy.landointellijplugin.services.LandoProjectService import com.intellij.execution.process.ProcessHandler import com.intellij.execution.ui.ConsoleViewContentType import com.github.aaronfeledy.landointellijplugin.ui.console.JeditermConsoleView +import com.intellij.openapi.project.Project import java.io.File import java.io.OutputStream @@ -20,6 +21,8 @@ class LandoExec(private val command: String) { private var attachToConsole: Boolean = true + var project: Project? = null + /** * Sets the directory for the Lando command. * @param path The directory path. @@ -60,9 +63,11 @@ class LandoExec(private val command: String) { // Get the LandoAppService instance val appService = LandoAppService.getInstance() - // If the directory is not set, use the root directory of the Lando application - if (directory.isEmpty()) { - directory = appService.appRoot?.path!! + // Default to the project root as working directory if none is set + if (directory.isEmpty() && project != null) { + directory = LandoProjectService.getInstance(project!!).appRoot?.path!! + } else if (directory.isEmpty()) { + directory = System.getProperty("user.dir") } // Set the directory for the process builder processBuilder.directory(File(directory)) diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoManagerListener.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoProjectManagerListener.kt similarity index 65% rename from src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoManagerListener.kt rename to src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoProjectManagerListener.kt index 3fc2004..2fc4041 100644 --- a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoManagerListener.kt +++ b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoProjectManagerListener.kt @@ -1,13 +1,11 @@ package com.github.aaronfeledy.landointellijplugin.listeners -import com.intellij.openapi.components.service +import com.github.aaronfeledy.landointellijplugin.services.LandoProjectService import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManagerListener -import com.github.aaronfeledy.landointellijplugin.services.LandoProjectService - -internal class LandoManagerListener : ProjectManagerListener { +class LandoProjectManagerListener : ProjectManagerListener { override fun projectOpened(project: Project) { - project.service() + LandoProjectService.getInstance(project) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoAppServiceListener.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoStatusListener.kt similarity index 70% rename from src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoAppServiceListener.kt rename to src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoStatusListener.kt index 6400d63..b49b0d9 100644 --- a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoAppServiceListener.kt +++ b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/listeners/LandoStatusListener.kt @@ -1,5 +1,5 @@ package com.github.aaronfeledy.landointellijplugin.listeners -interface LandoAppServiceListener { +interface LandoStatusListener { fun statusChanged() } diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoAppService.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoAppService.kt deleted file mode 100644 index 2383b38..0000000 --- a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoAppService.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.github.aaronfeledy.landointellijplugin.services - -import com.github.aaronfeledy.landointellijplugin.LandoExec -import com.github.aaronfeledy.landointellijplugin.ServiceData -import com.github.aaronfeledy.landointellijplugin.listeners.LandoAppServiceListener -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.intellij.execution.process.ProcessHandler -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.ConcurrencyUtil -import com.intellij.util.messages.Topic -import java.util.* -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit - -// Logger instance for the LandoAppService class -val logger = Logger.getInstance(LandoAppService::class.java) - -/** - * Service class for managing Lando applications. - * This class is responsible for fetching and maintaining the status of Lando applications. - */ -@Service -class LandoAppService() : Disposable { - // Instance of the LandoProjectService - val thisProject: LandoProjectService = - LandoProjectService.getInstance(ProjectManager.getInstance().openProjects[0]) - - // ScheduledExecutorService for periodically checking the status of the Lando application - private val statusWatcher: ScheduledExecutorService = ConcurrencyUtil.newSingleScheduledThreadExecutor("Lando Status Watcher") - .apply { scheduleWithFixedDelay({ fetchStatus() }, 0, 2000, TimeUnit.MILLISECONDS) } - - // Root directory of the Lando application - var appRoot: VirtualFile? = thisProject.projectDir - - // Map for storing the status of the services in the Lando application - var services: MutableMap = HashMap() - - // Indicate whether the Lando application has been started - var started: Boolean = false - set(value) { - field = value - notifyListeners() - } - get() { - return services.isNotEmpty() - } - - init { - if (appRoot == null) { - logger.debug("Could not find project root directory") - } - } - - /** - * Fetches the status of the Lando application. - * @return ProcessHandler for the process fetching the status. - */ - private fun fetchLandoAppStatus(): ProcessHandler { - logger.debug("Fetching Lando app status...") - return LandoExec("list").fetchJson().run() - } - - /** - * Fetches the information of the services in the Lando application. - * @return ProcessHandler for the process fetching the service information. - */ - private fun fetchLandoServiceInfo(): ProcessHandler { - logger.debug("Fetching Lando service info...") - return LandoExec("info").fetchJson().run() - } - - /** - * Checks the status of the Lando application and updates the services map. - */ - private fun fetchStatus() { - logger.debug("Checking Lando status...") - val serviceInfoHandler = fetchLandoServiceInfo() - serviceInfoHandler.waitFor(30000) - if (!serviceInfoHandler.isProcessTerminated) { - serviceInfoHandler.destroyProcess() - logger.warn("Timeout while fetching Lando service info") - } - val serviceInfoJson = serviceInfoHandler.processInput.toString() - val serviceData = parseServiceData(serviceInfoJson) - - services = serviceData.associateBy { it.service }.toMutableMap() - } - - /** - * Parses the JSON data of the service information into a list of ServiceData objects. - * @param jsonData JSON data of the service information. - * @return List of ServiceData objects. - */ - private fun parseServiceData(jsonData: String): List { - val gson = Gson() - val serviceListType = object : TypeToken>() {}.type - return gson.fromJson(jsonData, serviceListType) - } - - private fun notifyListeners() { - ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC) - } - - /** - * Disposes the LandoAppService, shutting down the status watcher. - */ - override fun dispose() { - statusWatcher.shutdown() - } - - companion object { - val TOPIC = Topic.create("LandoAppServiceTopic", LandoAppServiceListener::class.java) - fun getInstance(): LandoAppService = ApplicationManager.getApplication().getService(LandoAppService::class.java) - } -} diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoProjectService.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoProjectService.kt index 343afc4..024a3bf 100644 --- a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoProjectService.kt +++ b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoProjectService.kt @@ -1,29 +1,53 @@ package com.github.aaronfeledy.landointellijplugin.services -import com.intellij.openapi.project.Project +import com.github.aaronfeledy.landointellijplugin.LandoExec +import com.github.aaronfeledy.landointellijplugin.ServiceData +import com.github.aaronfeledy.landointellijplugin.listeners.LandoProjectManagerListener +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.intellij.execution.process.ProcessHandler +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.ConcurrencyUtil +import com.intellij.util.messages.Topic +import java.util.* +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit -// The name of the Lando configuration file -const val landoFileName = ".lando.yml" +val logger = Logger.getInstance(LandoProjectService::class.java) /** - * Service for managing Lando project-specific data and operations. - * - * This service is scoped at the Project level in the IntelliJ Platform, meaning a separate instance is created for each open Project. - * It's responsible for managing the Lando configuration file (.lando.yml) and providing utility methods related to the Lando project. - * - * @property project The IntelliJ Project this service is associated with. + * Service class for managing Lando applications. + * This class is responsible for fetching and maintaining the status of Lando applications. + * @param project The IntelliJ Project this service is associated with. */ @Service(Service.Level.PROJECT) -class LandoProjectService(val project: Project) { +class LandoProjectService(val project: Project) : Disposable { + companion object { + const val LANDOFILE_NAME = ".lando.yml" + + val TOPIC = Topic.create("LandoServiceTopic", LandoProjectManagerListener::class.java) + + /** + * Retrieves the [LandoProjectService] instance associated with the given Project. + * + * @param project The IntelliJ Project this service is associated with. + * @return The [LandoProjectService] instance associated with the given Project. + */ + @JvmStatic + fun getInstance(project: Project): LandoProjectService = project.getService(LandoProjectService::class.java) + } + // The root directory of the project. It's where we expect to find the Lando configuration file. var projectDir: VirtualFile? = project.guessProjectDir() // The Lando configuration file in the project directory. It's presence determines whether the project uses Lando. - var landoFile: VirtualFile? = projectDir?.findChild(landoFileName) + var landoFile: VirtualFile? = projectDir?.findChild(LANDOFILE_NAME) /** * Checks if the current project uses Lando. @@ -33,21 +57,83 @@ class LandoProjectService(val project: Project) { * * @return True if the project uses Lando, false otherwise. */ - fun usesLando(): Boolean { + fun projectUsesLando(): Boolean { return landoFile?.exists()!! } - companion object { - /** - * Retrieves the LandoProjectService instance associated with the given Project. - * - * This static method is a convenience for getting the LandoProjectService instance without having to manually fetch it from the ServiceManager. - * It's used throughout the codebase whenever access to the LandoProjectService is needed. - * - * @param project The Project for which to retrieve the LandoProjectService. - * @return The LandoProjectService instance associated with the given Project. - */ - @JvmStatic - fun getInstance(project: Project): LandoProjectService = project.service() + // ScheduledExecutorService for periodically checking the status of the Lando application + private val statusWatcher: ScheduledExecutorService = ConcurrencyUtil.newSingleScheduledThreadExecutor("Lando Status Watcher") + .apply { scheduleWithFixedDelay({ fetchInfo() }, 0, 2000, TimeUnit.MILLISECONDS) } + + // Root directory of the Lando application + var appRoot: VirtualFile? = this.projectDir + + // Map for storing the status of the services in the Lando application + var services: MutableMap = HashMap() + + // Indicate whether the Lando application has been started + var started: Boolean = false + set(value) { + field = value + notifyListeners() + } + get() { + return services.isNotEmpty() + } + + init { + if (appRoot == null) { + logger.debug("Could not find project root directory") + } + } + + /** + * Fetches the information of the services in the Lando application. + * @return [ProcessHandler] for the process fetching the service information. + */ + private fun fetchLandoServiceInfo(): ProcessHandler { + logger.debug("Fetching Lando service info...") + val exec = LandoExec("info") + exec.project = project + return exec.fetchJson().run() + } + + /** + * Checks the status of the Lando application and updates the services map. + */ + private fun fetchInfo() { + logger.debug("Checking Lando status...") + val serviceInfoHandler = fetchLandoServiceInfo() + serviceInfoHandler.waitFor(30000) + if (!serviceInfoHandler.isProcessTerminated) { + serviceInfoHandler.destroyProcess() + logger.warn("Timeout while fetching Lando service info") + } + val serviceInfoJson = serviceInfoHandler.processInput.toString() + val serviceData = parseServiceData(serviceInfoJson) + + services = serviceData.associateBy { it.service }.toMutableMap() + } + + /** + * Parses the JSON data of the service information into a list of ServiceData objects. + * @param jsonData JSON data of the service information. + * @return List of ServiceData objects. + */ + private fun parseServiceData(jsonData: String): List { + val gson = Gson() + val serviceListType = object : TypeToken>() {}.type + return gson.fromJson(jsonData, serviceListType) + } + + private fun notifyListeners() { + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC) + } + + /** + * Disposes the LandoAppService, shutting down the status watcher. + */ + override fun dispose() { + statusWatcher.shutdown() } } diff --git a/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoStatusService.kt b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoStatusService.kt new file mode 100644 index 0000000..b9ce1a6 --- /dev/null +++ b/src/main/kotlin/com/github/aaronfeledy/landointellijplugin/services/LandoStatusService.kt @@ -0,0 +1,152 @@ +package com.github.aaronfeledy.landointellijplugin.services + +import com.github.aaronfeledy.landointellijplugin.LandoExec +import com.github.aaronfeledy.landointellijplugin.listeners.LandoStatusListener +import com.github.aaronfeledy.landointellijplugin.ui.console.JeditermConsoleView +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.ProjectManager +import com.intellij.util.messages.Topic +import com.pty4j.PtyProcessBuilder +import kotlinx.coroutines.sync.Mutex +import java.util.* + +@Service() +class LandoStatusService() : Disposable { + + private val ttl = 5000 + + private var lastUpdated = 0L + + private val mutex: Mutex = Mutex() + + val appsCache = mutableMapOf() + + var consoleView: JeditermConsoleView? = null + + /** + * Fetches the status of Lando apps. + * + * @return ProcessHandler for the process fetching the status. + */ + fun fetchLandoAppStatus(): Boolean { + if (cacheIsValid()) { + return true + } + if (!mutex.tryLock("fetchLandoAppStatus")) { + for (i in 0..LandoExec.PROCESS_TIMEOUT step 1000) { + if (cacheIsValid()) { + return true + } + Thread.sleep(1000) + } + logger.warn("Timeout waiting for fetchLandoAppStatus mutex to be released.") + return false + } + + try { + logger.debug("Fetching Lando app status...") + + // Execute `lando list` + val landoListCmd = + arrayOf("lando", "list") //, "--format", "json") + val processBuilder = PtyProcessBuilder(landoListCmd) + processBuilder.setRedirectErrorStream(true) + processBuilder.setConsole(true) + + val process = processBuilder.start() + + consoleView?.connectToProcess(process) + + val scanner = Scanner(process.inputStream).useDelimiter("\\n") + + while (scanner.hasNext()) { + val line = scanner.next() + consoleView?.output((line + "\n").toByteArray()) + } + +// val reader = BufferedReader(InputStreamReader(process.inputStream)) +// +// val stdout = StringBuilder() +// val stderr = StringBuilder() +// reader.forEachLine { line -> +// consoleView?.output(line.toByteArray()) + +// // Check if the line is JSON data +// if (line.startsWith("{")) { +// stdout.append(line) +// } else { +// stderr.append(line).append("\n") +// consoleView?.output((line + "\r\n").toByteArray()) // Ensure each line of stderr is output to consoleView +// } +// } + process.waitFor() + + // Handle stdout (JSON data) +// val jsonData = stdout.toString() +// if (jsonData.isNotEmpty()) { +// appsCache.putAll(parseLandoAppStatus(jsonData)) +// lastUpdated = System.currentTimeMillis() +// // Notify listeners about status change +// ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).statusChanged() +// // Send output to JeditermConsoleView +// consoleView?.output(jsonData.toByteArray()) +// } + + // Handle stderr (status updates) + //updateTerminalPanel(stderr.toString()) + + } catch (e: Exception) { + logger.error("Error fetching Lando app status", e) + } finally { + mutex.unlock() + } + + return appsCache.isNotEmpty() + } + + /** + * Checks if the cache is still valid. + * @return True if the cache is still valid, false otherwise. + */ + private fun cacheIsValid(): Boolean { + return appsCache.isNotEmpty() && System.currentTimeMillis() - lastUpdated < ttl + } + + /** + * Parses the JSON data of the Lando app status into a map. + * @param jsonData JSON data of the Lando app status. + * @return Map of the Lando app status. + */ + private fun parseLandoAppStatus(jsonData: String): Map { + val gson = Gson() + val statusType = object : TypeToken>() {}.type + return gson.fromJson(jsonData, statusType) + } + + override fun dispose() { + consoleView?.dispose() + } + + private fun updateTerminalPanel(errorOutput: String) { + // TODO: This service shouldn't be responsible for updating the terminal panel + val project = ProjectManager.getInstance().openProjects.firstOrNull() ?: return + + ApplicationManager.getApplication().invokeLater { + consoleView?.output(errorOutput.toByteArray()) + } + } + + companion object { + val TOPIC = Topic.create("LandoStatusTopic", LandoStatusListener::class.java) + + val logger = Logger.getInstance(LandoStatusService::class.java) + + fun getInstance(): LandoStatusService = + ApplicationManager.getApplication().getService(LandoStatusService::class.java) + } +} \ No newline at end of file diff --git a/src/main/resources/messages/LandoBundle.properties b/src/main/resources/messages/LandoBundle.properties index 37d18f4..98739fc 100644 --- a/src/main/resources/messages/LandoBundle.properties +++ b/src/main/resources/messages/LandoBundle.properties @@ -1,6 +1,7 @@ name=Lando -tab.title.lando=Lando +stripe.lando.title=Lando +tab.lando.title=Console filetype.lando.landofile.name.display=Landofile filetype.lando.landofile.description=Lando app configuration file.