diff --git a/ide-common/src/main/java/org/digma/intellij/plugin/settings/SettingsComponent.java b/ide-common/src/main/java/org/digma/intellij/plugin/settings/SettingsComponent.java index 3837f4db7..08b677634 100644 --- a/ide-common/src/main/java/org/digma/intellij/plugin/settings/SettingsComponent.java +++ b/ide-common/src/main/java/org/digma/intellij/plugin/settings/SettingsComponent.java @@ -12,6 +12,7 @@ import org.digma.intellij.plugin.analytics.BackendInfoHolder; import org.digma.intellij.plugin.auth.account.*; import org.digma.intellij.plugin.common.*; +import org.digma.intellij.plugin.docker.*; import org.digma.intellij.plugin.errorreporting.ErrorReporter; import org.digma.intellij.plugin.updates.ui.UIVersioningService; import org.jetbrains.annotations.*; @@ -382,7 +383,11 @@ private static JBLabel createBackendVersionLabel() { if (someProject != null) { var about = BackendInfoHolder.getInstance(someProject).getAbout(); if (about != null) { - backendVersionLabel.setText(about.getApplicationVersion()); + if (LocalInstallationFacade.getInstance().isLocalEngineInstalled()) { + backendVersionLabel.setText(about.getApplicationVersion() + " (" + DockerService.getInstance().getComposeFilePath() + ")"); + }else{ + backendVersionLabel.setText(about.getApplicationVersion()); + } } } return backendVersionLabel; diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/ComposeFileProvider.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/ComposeFileProvider.kt new file mode 100644 index 000000000..39fde9996 --- /dev/null +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/ComposeFileProvider.kt @@ -0,0 +1,272 @@ +package org.digma.intellij.plugin.docker + +import com.intellij.openapi.diagnostic.Logger +import org.digma.intellij.plugin.common.Retries +import org.digma.intellij.plugin.errorreporting.ErrorReporter +import org.digma.intellij.plugin.log.Log +import org.digma.intellij.plugin.paths.DigmaPathManager +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.URI +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import kotlin.io.path.deleteIfExists + +private const val COMPOSE_FILE_URL = "https://get.digma.ai/" +const val COMPOSE_FILE_NAME = "docker-compose.yml" +private const val RESOURCE_LOCATION = "docker-compose" +const val COMPOSE_FILE_DIR_NAME = "digma-docker" + +/** + * ComposeFileProvider is responsible for providing the docker-compose.yml file. + * the latest docker-compose.yml is bundled with the plugin in build time. + * on first request for the file It will unpack the file from the resource to the local file system if it does not exist. + * after unpacking the file it will be available for running docker operations and will not be unpacked again, unless it was deleted somehow. + * the file is saved to a persistence folder on user's machine and should not be deleted by the user. its is like an installation file + * of the plugin and usually users will not delete it. + * when upgrading the local engine the docker service will call downloadLatestComposeFile to download the latest compose file. the file will be downloaded + * and override the existing file, from now on the new file will be used for docker operations. + * if after upgrade the file is deleted somehow, the plugin will use the bundled file from the resource again. this may cause a downgrade of the local + * engine, but we don't expect that to happen. (TODO: an improvement to this case can be to track upgrades, mark a flag in persistence that there was + * an upgrade and if the file is deleted after upgrade, download the latest again. this may also cause an issue if the latest backend is not + * compatible with this plugin version or the docker images are different then what is currently running). + * This class also supports using a custom docker compose file for development purposes only. the custom file is downloaded from a custom url provided + * with a system property. engine upgrade will not work when using a custom docker compose file. + */ +class ComposeFileProvider { + + private val logger = Logger.getInstance(this::class.java) + + private val composeFile: File = File(COMPOSE_FILE_DIR, COMPOSE_FILE_NAME) + + //customComposeFileDownloaded is used to mark if the custom compose file was already downloaded in this IDE session. + // it needs to be downloaded only once per IDE session. after IDE restart, a new one will be downloaded and override the old one if it exists. + // it may be that a new session has another custom compose file url so need to override the old one. + private var customComposeFileDownloaded = false + private val customComposeFile: File = File(CUSTOM_COMPOSE_FILE_DIR, COMPOSE_FILE_NAME) + + + companion object { + val COMPOSE_FILE_DIR = File(DigmaPathManager.getLocalFilesDirectoryPath(), COMPOSE_FILE_DIR_NAME) + val CUSTOM_COMPOSE_FILE_DIR = File(System.getProperty("java.io.tmpdir"), COMPOSE_FILE_DIR_NAME) + + private fun usingCustomComposeFile(): Boolean { + return getCustomComposeFileUrl() != null + } + + /* + Using a custom compose file is for development purposes only and not for users. + After using it, it is necessary to remove the local engine and remove the property. + Then install local engine regularly. + Upgrade should not be invoked when using a custom compose file. it will not work, it will always use the same custom url. + */ + private fun getCustomComposeFileUrl(): String? { + return System.getProperty("org.digma.plugin.custom.docker-compose.url") + } + } + + + //this method should not be used to get the file itself, it is mainly for logging and debugging. + //it should not create the file or unpack it. + //use the method getComposeFile() to get the file for running docker operations + fun getComposeFilePath(): String { + if(usingCustomComposeFile()) { + return customComposeFile.absolutePath + } + return composeFile.absolutePath + } + + + + //this method should return a file, if the file does not exist, the docker operation will fail + fun getComposeFile(): File { + ensureComposeFileExists() + + if (usingCustomComposeFile()) { + return customComposeFile + } + + return composeFile + } + + + fun ensureComposeFileExists(): Boolean { + try { + if (usingCustomComposeFile()) { + Log.log(logger::info, "using custom compose file {}", getCustomComposeFileUrl()) + return ensureCustomComposeFileExists() + } + + if (composeFile.exists()) { + Log.log(logger::info, "compose file exists {}", composeFile) + return true + } + + Log.log(logger::info, "compose file does not exist, unpacking bundled file") + unpack() + return true + + } catch (e: Throwable) { + Log.warnWithException(logger, e, "could not ensure compose file exists") + ErrorReporter.getInstance().reportError("ComposeFileProvider.ensureComposeFileExists", e) + return false + } + } + + + private fun ensureCustomComposeFileExists(): Boolean { + + if (customComposeFileDownloaded && customComposeFile.exists()) { + return true + } + + return getCustomComposeFileUrl()?.let { url -> + CUSTOM_COMPOSE_FILE_DIR.mkdirs() + downloadAndCopyFile(URI(url).toURL(), customComposeFile) + customComposeFileDownloaded = customComposeFile.exists() + customComposeFile.exists() + } ?: false + + } + + + + private fun unpack() { + Log.log(logger::info, "unpacking docker-compose.yml") + + try { + ensureDirectoryExist() + + if (COMPOSE_FILE_DIR.exists()) { + copyComposeFileFromResource() + Log.log(logger::info, "docker-compose.yml unpacked to {}", COMPOSE_FILE_DIR) + } + } catch (e: Exception) { + ErrorReporter.getInstance().reportError("ComposeFileProvider.unpack", e) + Log.warnWithException(logger, e, "could not unpack docker-compose.yml") + } + } + + + private fun ensureDirectoryExist() { + if (!COMPOSE_FILE_DIR.exists()) { + if (!COMPOSE_FILE_DIR.mkdirs()) { + Log.log(logger::warn, "could not create directory for docker-compose.yml {}", COMPOSE_FILE_DIR) + ErrorReporter.getInstance().reportError( + null, "ComposeFileProvider.ensureDirectoryExist", + "ensureDirectoryExist,could not create directory for docker-compose.yml in $COMPOSE_FILE_DIR", + mapOf("error hint" to "could not create directory for docker-compose.yml in $COMPOSE_FILE_DIR") + ) + } + } + } + + + private fun copyComposeFileFromResource() { + val resourceLocation = "/$RESOURCE_LOCATION/$COMPOSE_FILE_NAME" + val inputStream = this::class.java.getResourceAsStream(resourceLocation) + if (inputStream == null) { + Log.log(logger::warn, "could not find file in resource for {}", resourceLocation) + ErrorReporter.getInstance().reportError( + null, "ComposeFileProvider.copyFileFromResource", + "could not extract docker-compose.yml from resource", mapOf( + "resource location" to resourceLocation + ) + ) + return + } + + FileOutputStream(composeFile).use { + Log.log(logger::info, "unpacking {} to {}", COMPOSE_FILE_NAME, composeFile) + com.intellij.openapi.util.io.StreamUtil.copy(inputStream, it) + } + + } + + + + //downloadLatestComposeFile is used only for upgrading the local engine + fun downloadLatestComposeFile(): Boolean { + + try { + //try to delete the current file, don't fail if delete fails + deleteFile() + } catch (e: Throwable) { + Log.warnWithException(logger, e, "could not delete compose file") + ErrorReporter.getInstance().reportError("ComposeFileProvider.downloadLatestComposeFile", e) + } + + try { + ensureDirectoryExist() + downloadAndCopyFile(URI(COMPOSE_FILE_URL).toURL(), composeFile) + return composeFile.exists() + } catch (e: Throwable) { + Log.warnWithException(logger, e, "could not download latest compose file") + ErrorReporter.getInstance().reportError("ComposeFileProvider.downloadLatestComposeFile", e) + return false + } + } + + + + private fun downloadAndCopyFile(url: URL, toFile: File) { + + val tempFile = kotlin.io.path.createTempFile("tempComposeFile", ".yml") + + try { + + Retries.simpleRetry({ + + Log.log(logger::info, "downloading {}", url) + + val connection: HttpURLConnection = url.openConnection() as HttpURLConnection + connection.connectTimeout = 10000 + connection.readTimeout = 5000 + connection.requestMethod = "GET" + val responseCode: Int = connection.getResponseCode() + + if (responseCode != HttpURLConnection.HTTP_OK) { + throw RuntimeException("could not download file from $url") + } else { + connection.inputStream.use { + Files.copy(it, tempFile, StandardCopyOption.REPLACE_EXISTING) + } + + Log.log(logger::info, "copying downloaded file {} to {}", tempFile, toFile) + try { + Files.move(tempFile, toFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) + } catch (e: Exception) { + //ATOMIC_MOVE is not always supported, so try again on exception + Files.move(tempFile, toFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + }, Throwable::class.java, 5000, 3) + + } catch (e: Exception) { + Log.log(logger::warn, "could not download file {}, {}", url, e) + + ErrorReporter.getInstance().reportError( + "ComposeFileProvider.downloadAndCopyFile", e, mapOf( + "url" to url.toString(), + "toFile" to toFile.toString() + ) + ) + + } finally { + tempFile.deleteIfExists() + } + } + + + fun deleteFile() { + Retries.simpleRetry({ + val file = if(usingCustomComposeFile()) customComposeFile else composeFile + Files.deleteIfExists(file.toPath()) + }, Throwable::class.java, 100, 5) + } + + + +} \ No newline at end of file diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DigmaInstallationDiscovery.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DigmaInstallationDiscovery.kt index d41a06d9f..2b6eb6b37 100644 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DigmaInstallationDiscovery.kt +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DigmaInstallationDiscovery.kt @@ -74,7 +74,7 @@ private fun isLocalEngineRunning(): Boolean { return false } - val projectName = COMPOSE_FILE_DIR + val projectName = COMPOSE_FILE_DIR_NAME val dockerCmd = getDockerCommand() @@ -104,7 +104,7 @@ private fun isAnyEngineRunning(): Boolean { return false } - val projectName = COMPOSE_FILE_DIR + val projectName = COMPOSE_FILE_DIR_NAME val dockerCmd = getDockerCommand() diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerComposeFileMigration.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerComposeFileMigration.kt new file mode 100644 index 000000000..e407ae9ef --- /dev/null +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerComposeFileMigration.kt @@ -0,0 +1,90 @@ +package org.digma.intellij.plugin.docker + +import com.intellij.openapi.diagnostic.Logger +import org.digma.intellij.plugin.common.findActiveProject +import org.digma.intellij.plugin.errorreporting.ErrorReporter +import org.digma.intellij.plugin.log.Log +import org.digma.intellij.plugin.persistence.PersistenceService +import org.digma.intellij.plugin.posthog.ActivityMonitor +import java.io.File + + +fun migrateDockerComposeFile(newDockerComposeFilePath: String, logger: Logger) { + + /* + in version 2.0.404 of the plugin we changed the location of the docker-conmpose.yml file. + from $TEMP/digma-docker/docker-compose.yml + to + ${DigmaPathManager.getLocalFilesDirectoryPath()}/digma-docker/docker-compose.yml + + the directory is the same name and so the docker project is the same project. + this migration will just move the old file to the new location and because its the same project name in docker nothing will change + and the local engine will continue as usual + + when this plugin version is installed we try to find the old compose file and just copy it to the new location. + after that local engine will continue to work as usual. + + if the file is not found the plugin will just use the new file that is bundled with this plugin version. the result is the same + as if this migration didn't happen because in the old way if the file from $TEMP/digma-docker/docker-compose.yml was deleted the + plugin would download the latest anyway. + + if the old file does not exist: + - if local engine is not installed, nothing to do. + + - if local engine is installed and not running: + the next time user starts the engine the new file that is bundled with this plugin version will be used. + this may be an update to the engine if the previous engine was older than the compose file that is bundled with this plugin version. + + - if local engine is installed and running: + the next time user will try to stop the engine the new file that is bundled with this plugin version will be used. + this may not succeed if the engine was installed with a much older version than what is bundled with this plugin version. + + if the steps above succeed local engine will continue to work as usual. + + */ + + // this is a one time operation, if it fails we don't want to try again. + // this code will run once after the installation of plugin version that contains this code. + + try { + //check if this is the first time this plugin version is running + val isFirstRunAfterPersistDockerCompose = PersistenceService.getInstance().isFirstRunAfterPersistDockerCompose() + if (isFirstRunAfterPersistDockerCompose) { + Log.log(logger::info, "first run after persist docker compose") + PersistenceService.getInstance().setIsFirstRunAfterPersistDockerComposeDone() + + if (!LocalInstallationFacade.getInstance().isLocalEngineInstalled()) { + Log.log(logger::info, "local engine not installed, nothing to do") + return + } + + val oldDockerComposeDir = File(System.getProperty("java.io.tmpdir"), COMPOSE_FILE_DIR_NAME) + val oldDockerComposeFile = File(oldDockerComposeDir, COMPOSE_FILE_NAME) + if (oldDockerComposeFile.exists()) { + val newDockerComposeFile = File(newDockerComposeFilePath) + Log.log(logger::info, "old compose file found, moving to new location {}", newDockerComposeFile) + oldDockerComposeFile.copyTo(newDockerComposeFile, overwrite = true) + //do not delete the old file, it may be used by other IDEs. worst case it will stay as zombie file in the user's temp directory + ////oldDockerComposeFile.delete() + Log.log(logger::info, "old compose file moved to new location {}", newDockerComposeFile) + } else { + Log.log(logger::info, "old compose file not found") + } + + findActiveProject()?.let { + ActivityMonitor.getInstance(it).registerCustomEvent( + "docker compose file migrated", mapOf( + "oldDockerComposeFileExists" to oldDockerComposeFile.exists(), + "newDockerComposeFile" to newDockerComposeFilePath + ) + ) + } + + } + } catch (e: Throwable) { + Log.warnWithException(logger, e, "error migrating docker compose file") + ErrorReporter.getInstance().reportError("DockerComposeFileMigration.migrateDockerComposeFile", e) + } +} + + diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerService.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerService.kt index 177c59ec7..4f8ad28d8 100644 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerService.kt +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerService.kt @@ -36,7 +36,7 @@ class DockerService { private val logger = Logger.getInstance(this::class.java) private val engine = Engine() - private val downloader = Downloader() + private val composeFileProvider = ComposeFileProvider() companion object { @@ -50,12 +50,13 @@ class DockerService { init { + migrateDockerComposeFile(composeFileProvider.getComposeFilePath(), logger) + } - if (PersistenceService.getInstance().isLocalEngineInstalled()) { - //this will happen on IDE start, - // DockerService is an application service so Downloader will be singleton per application - downloader.downloadComposeFile() - } + + //this method should not be used to get the file itself, it is mainly for logging and debugging. + fun getComposeFilePath(): String { + return composeFileProvider.getComposeFilePath() } @@ -106,18 +107,18 @@ class DockerService { var exitValue = "" try { - if (downloader.downloadComposeFile(true)) { + if (composeFileProvider.ensureComposeFileExists()) { val dockerComposeCmd = getDockerComposeCommand() if (dockerComposeCmd != null) { - exitValue = engine.up(project, downloader.composeFile, dockerComposeCmd) + exitValue = engine.up(project, composeFileProvider.getComposeFile(), dockerComposeCmd) if (exitValue != "0") { ActivityMonitor.getInstance(project).registerDigmaEngineEventError("installEngine", exitValue) Log.log(logger::warn, "error installing engine {}", exitValue) if (isDockerDaemonDownExitValue(exitValue)) { exitValue = doRetryFlowWhenDockerDaemonIsDown(project, exitValue) { - engine.up(project, downloader.composeFile, dockerComposeCmd) + engine.up(project, composeFileProvider.getComposeFile(), dockerComposeCmd) } } } @@ -166,9 +167,17 @@ class DockerService { //the engine should be up otherwise the upgrade would not be triggered. //ignore errors, we'll try to upgrade anyway after that. try { - val dockerComposeCmd = getDockerComposeCommand() - dockerComposeCmd?.let { - engine.down(project, downloader.composeFile, it, false) + if (composeFileProvider.ensureComposeFileExists()) { + val dockerComposeCmd = getDockerComposeCommand() + dockerComposeCmd?.let { + engine.down(project, composeFileProvider.getComposeFile(), it, false) + } + } else { + ActivityMonitor.getInstance(project).registerDigmaEngineEventError( + "upgradeEngine", + "Failed to stop engine before upgrade because compose file not found" + ) + Log.log(logger::warn, "Failed to download compose file") } } catch (e: Throwable) { ErrorReporter.getInstance().reportError(project, "DockerService.upgradeEngine", e) @@ -178,11 +187,11 @@ class DockerService { var exitValue = "" try { - if (downloader.downloadComposeFile(true)) { + if (composeFileProvider.downloadLatestComposeFile()) { val dockerComposeCmd = getDockerComposeCommand() if (dockerComposeCmd != null) { - exitValue = engine.up(project, downloader.composeFile, dockerComposeCmd) + exitValue = engine.up(project, composeFileProvider.getComposeFile(), dockerComposeCmd) //in upgrade there is no need to check if daemon is down because upgrade will not be triggered if the engine is not running. if (exitValue != "0") { ActivityMonitor.getInstance(project).registerDigmaEngineEventError("upgradeEngine", exitValue) @@ -196,7 +205,7 @@ class DockerService { notifyResult(NO_DOCKER_COMPOSE_COMMAND, resultTask) } } else { - ActivityMonitor.getInstance(project).registerDigmaEngineEventError("upgradeEngine", "Failed to download compose file") + ActivityMonitor.getInstance(project).registerDigmaEngineEventError("upgradeEngine", "Failed to download latest compose file") Log.log(logger::warn, "Failed to download compose file") notifyResult("Failed to download compose file", resultTask) } @@ -224,13 +233,13 @@ class DockerService { var exitValue = "" try { - if (downloader.downloadComposeFile()) { + if (composeFileProvider.ensureComposeFileExists()) { val dockerComposeCmd = getDockerComposeCommand() if (dockerComposeCmd != null) { - exitValue = engine.stop(project, downloader.composeFile, dockerComposeCmd) + exitValue = engine.stop(project, composeFileProvider.getComposeFile(), dockerComposeCmd) if (exitValue != "0") { ActivityMonitor.getInstance(project).registerDigmaEngineEventError("stopEngine", exitValue) Log.log(logger::warn, "error stopping engine {}", exitValue) @@ -270,7 +279,7 @@ class DockerService { var exitValue = "" try { - if (downloader.downloadComposeFile()) { + if (composeFileProvider.ensureComposeFileExists()) { val dockerComposeCmd = getDockerComposeCommand() if (dockerComposeCmd != null) { @@ -282,20 +291,20 @@ class DockerService { //so running down and then up solves it. //in any case it's better to stop before start because we don't know the state of the engine, maybe its // partially up and start will fail. - engine.down(project, downloader.composeFile, dockerComposeCmd, false) + engine.down(project, composeFileProvider.getComposeFile(), dockerComposeCmd, false) Thread.sleep(2000) } catch (e: Exception) { ErrorReporter.getInstance().reportError(project, "DockerService.startEngine", e) Log.warnWithException(logger, e, "Failed to stop docker engine {}", e) } - exitValue = engine.start(project, downloader.composeFile, dockerComposeCmd) + exitValue = engine.start(project, composeFileProvider.getComposeFile(), dockerComposeCmd) if (exitValue != "0") { ActivityMonitor.getInstance(project).registerDigmaEngineEventError("startEngine", exitValue) Log.log(logger::warn, "error starting engine {}", exitValue) if (isDockerDaemonDownExitValue(exitValue)) { exitValue = doRetryFlowWhenDockerDaemonIsDown(project, exitValue) { - engine.start(project, downloader.composeFile, dockerComposeCmd) + engine.start(project, composeFileProvider.getComposeFile(), dockerComposeCmd) } } } @@ -345,11 +354,11 @@ class DockerService { var exitValue = "" try { - if (downloader.downloadComposeFile()) { + if (composeFileProvider.ensureComposeFileExists()) { val dockerComposeCmd = getDockerComposeCommand() if (dockerComposeCmd != null) { - exitValue = engine.remove(project, downloader.composeFile, dockerComposeCmd) + exitValue = engine.remove(project, composeFileProvider.getComposeFile(), dockerComposeCmd) if (exitValue != "0") { ActivityMonitor.getInstance(project).registerDigmaEngineEventError("removeEngine", exitValue) Log.log(logger::warn, "error uninstalling engine {}", exitValue) @@ -363,7 +372,7 @@ class DockerService { try { //always delete file here, it's an uninstallation - downloader.deleteFile() + composeFileProvider.deleteFile() } catch (e: Exception) { Log.log(logger::warn, "Failed to delete compose file") ActivityMonitor.getInstance(project).registerDigmaEngineEventError("removeEngine", "failed to delete compose file: $e") diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerServiceStarter.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerServiceStarter.kt index 36221a885..9dc690479 100644 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerServiceStarter.kt +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/DockerServiceStarter.kt @@ -3,9 +3,10 @@ package org.digma.intellij.plugin.docker import com.intellij.openapi.project.Project import org.digma.intellij.plugin.startup.DigmaProjectActivity -class DockerServiceStarter: DigmaProjectActivity() { - //initialize DockerService as early as possible so it will download docker compose file early +class DockerServiceStarter : DigmaProjectActivity() { + override fun executeProjectStartup(project: Project) { + //initialize the docker service as early as possible DockerService.getInstance() } } \ No newline at end of file diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/Downloader.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/Downloader.kt deleted file mode 100644 index 2e367fbad..000000000 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/docker/Downloader.kt +++ /dev/null @@ -1,171 +0,0 @@ -package org.digma.intellij.plugin.docker - -import com.intellij.openapi.diagnostic.Logger -import org.digma.intellij.plugin.common.Retries -import org.digma.intellij.plugin.errorreporting.ErrorReporter -import org.digma.intellij.plugin.log.Log -import java.io.File -import java.io.FileOutputStream -import java.net.HttpURLConnection -import java.net.URL -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import kotlin.io.path.deleteIfExists - -private const val COMPOSE_FILE_URL = "https://get.digma.ai/" -private const val COMPOSE_FILE_NAME = "docker-compose.yml" -const val COMPOSE_FILE_DIR = "digma-docker" -private const val RESOURCE_LOCATION = "docker-compose" - -class Downloader { - - private val logger = Logger.getInstance(this::class.java) - - private val downloadDir: File = File(System.getProperty("java.io.tmpdir"), COMPOSE_FILE_DIR) - val composeFile: File = File(downloadDir, COMPOSE_FILE_NAME) - - - private fun unpackAndDownloadLatestNow() { - unpack() - downloadNow() - } - - - private fun unpack() { - Log.log(logger::info, "unpacking docker-compose.yml") - - try { - ensureDirectoryExist() - - if (downloadDir.exists()) { - copyFileFromResource() - Log.log(logger::info, "docker-compose.yml unpacked to {}", downloadDir) - } - } catch (e: Exception) { - ErrorReporter.getInstance().reportError("Downloader.unpack", e) - Log.warnWithException(logger, e, "could not unpack docker-compose.yml.") - } - } - - - private fun ensureDirectoryExist() { - if (!downloadDir.exists()) { - if (!downloadDir.mkdirs()) { - Log.log(logger::warn, "could not create directory for docker-compose.yml {}", downloadDir) - ErrorReporter.getInstance().reportError( - null, "Downloader.ensureDirectoryExist", - "ensureDirectoryExist,could not create directory for docker-compose.yml in $downloadDir", - mapOf("error hint" to "could not create directory for docker-compose.yml in $downloadDir") - ) - } - } - } - - - private fun copyFileFromResource() { - val inputStream = this::class.java.getResourceAsStream("/$RESOURCE_LOCATION/$COMPOSE_FILE_NAME") - if (inputStream == null) { - Log.log(logger::warn, "could not find file in resource for {}", COMPOSE_FILE_NAME) - return - } - - FileOutputStream(composeFile).use { - Log.log(logger::info, "unpacking {} to {}", COMPOSE_FILE_NAME, composeFile) - com.intellij.openapi.util.io.StreamUtil.copy(inputStream, it) - } - - } - - - private fun downloadNow(url: String = COMPOSE_FILE_URL) { - ensureDirectoryExist() - Log.log(logger::info, "trying to download latest compose file") - downloadAndCopyFile(URL(url), composeFile) - } - - private fun downloadAndCopyFile(url: URL, toFile: File) { - - val tempFile = kotlin.io.path.createTempFile("tempComposeFile", ".yml") - - try { - - Retries.simpleRetry({ - - Log.log(logger::info, "downloading {}", url) - - val connection: HttpURLConnection = url.openConnection() as HttpURLConnection - connection.connectTimeout = 10000 - connection.readTimeout = 5000 - connection.requestMethod = "GET" - val responseCode: Int = connection.getResponseCode() - - if (responseCode != HttpURLConnection.HTTP_OK) { - ErrorReporter.getInstance().reportError( - null, "Downloader.downloadAndCopyFile", - "download from $url", - mapOf("responseCode" to responseCode.toString()) - ) - throw RuntimeException("could not download file from $url") - } else { - connection.inputStream.use { - Files.copy(it, tempFile, StandardCopyOption.REPLACE_EXISTING) - } - - Log.log(logger::info, "copying downloaded file {} to {}", tempFile, toFile) - try { - Files.move(tempFile, toFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) - } catch (e: Exception) { - //ATOMIC_MOVE is not always supported so try again on exception - Files.move(tempFile, toFile.toPath(), StandardCopyOption.REPLACE_EXISTING) - } - } - }, Throwable::class.java, 5000, 3) - - } catch (e: Exception) { - ErrorReporter.getInstance().reportError("Downloader.downloadAndCopyFile", e) - Log.log(logger::warn, "could not download file {}, {}", url, e) - } finally { - tempFile.deleteIfExists() - } - } - - - fun downloadComposeFile(forceDownloadLatest: Boolean = false): Boolean { - - val customDownloadUrl: String? = System.getProperty("org.digma.plugin.custom.docker-compose.url") - if (customDownloadUrl != null) { - Log.log(logger::warn, "downloading compose file from {}", customDownloadUrl) - unpack() - downloadNow(customDownloadUrl) - return true - } - - if (composeFile.exists() && !forceDownloadLatest) { - Log.log(logger::warn, "compose file already exists {}", composeFile) - return true - } - - if (composeFile.exists() && forceDownloadLatest) { - Log.log(logger::warn, "compose file already exists but forcing download latest {}", composeFile) - downloadNow() - return true - } - - //compose file does not exist. probably first install or the file was deleted. - // in case the file was deleted this will actually upgrade the backend to latest compose file - unpackAndDownloadLatestNow() - - return composeFile.exists() - - } - - - fun deleteFile() { - Retries.simpleRetry(Runnable { - val dir = composeFile.parentFile - Files.deleteIfExists(composeFile.toPath()) - Files.deleteIfExists(dir.toPath()) - }, Throwable::class.java, 100, 5) - } - -} \ No newline at end of file diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceData.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceData.kt index 72fe6727e..95cd4573b 100644 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceData.kt +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceData.kt @@ -98,6 +98,7 @@ internal data class PersistenceData( var engagementScorePersistenceFileFixed: Boolean = false, var currentUiVersion: String? = null, - var latestDownloadedUiVersion: String? = null + var latestDownloadedUiVersion: String? = null, + var isFirstRunAfterPersistDockerCompose: Boolean = true ) \ No newline at end of file diff --git a/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceService.kt b/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceService.kt index dc5c9d362..9ae1d5a84 100644 --- a/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceService.kt +++ b/ide-common/src/main/kotlin/org/digma/intellij/plugin/persistence/PersistenceService.kt @@ -443,4 +443,11 @@ class PersistenceService { state.latestDownloadedUiVersion = uiVersion } + fun isFirstRunAfterPersistDockerCompose(): Boolean { + return state.isFirstRunAfterPersistDockerCompose + } + fun setIsFirstRunAfterPersistDockerComposeDone() { + state.isFirstRunAfterPersistDockerCompose = false + } + } diff --git a/src/main/kotlin/org/digma/intellij/plugin/ui/common/UpdateBackendAction.kt b/src/main/kotlin/org/digma/intellij/plugin/ui/common/UpdateBackendAction.kt index 6ab883da5..ac1bbb08c 100644 --- a/src/main/kotlin/org/digma/intellij/plugin/ui/common/UpdateBackendAction.kt +++ b/src/main/kotlin/org/digma/intellij/plugin/ui/common/UpdateBackendAction.kt @@ -3,11 +3,15 @@ package org.digma.intellij.plugin.ui.common import com.intellij.codeInsight.hint.HintManager import com.intellij.ide.BrowserUtil import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.ui.awt.RelativePoint import com.intellij.util.ui.JBUI.Borders.empty +import org.digma.intellij.plugin.analytics.AnalyticsService +import org.digma.intellij.plugin.analytics.BackendConnectionMonitor import org.digma.intellij.plugin.docker.LocalInstallationFacade import org.digma.intellij.plugin.errorreporting.ErrorReporter +import org.digma.intellij.plugin.log.Log import org.digma.intellij.plugin.model.rest.version.BackendDeploymentType import org.digma.intellij.plugin.service.EditorService import org.digma.intellij.plugin.ui.common.Links.DIGMA_DOCKER_APP_URL @@ -22,9 +26,12 @@ const val UPDATE_GUIDE_HELM_NAME = "upgrade_helm.md" class UpdateBackendAction { + private val logger = Logger.getInstance(this::class.java) fun updateBackend(project: Project, backendDeploymentType: BackendDeploymentType, sourceComponent: JComponent?) { + Log.log(logger::info, "updateBackend invoked, backendDeploymentType {}, [t:{}]", backendDeploymentType, Thread.currentThread().name) + when (backendDeploymentType) { BackendDeploymentType.Helm -> { @@ -45,14 +52,17 @@ class UpdateBackendAction { HintManager.getInstance() .showHint(upgradePopupLabel, RelativePoint.getNorthWestOf(it), HintManager.HIDE_BY_ESCAPE, 5000) } + Log.log(logger::info, "calling upgrade backend for local engine. [t:{}]", Thread.currentThread().name) service().upgradeEngine(project) { exitValue -> if (exitValue != "0") { + Log.log(logger::warn, "error upgrading local engine , exitValue {}. [t:{}]", exitValue, Thread.currentThread().name) ErrorReporter.getInstance().reportError( "UpdateBackendAction.upgradeLocalEngine", "failed to upgrade local engine", mapOf("exitValue" to exitValue) ) } + tryToUpdateConnectionStatusSoon(project) } } else { EditorService.getInstance(project) @@ -72,4 +82,34 @@ class UpdateBackendAction { } + //call some api to refresh the connection status as soon as possible + private fun tryToUpdateConnectionStatusSoon(project: Project) { + Log.log(logger::info, "trying to update connection status soon [t:{}]", Thread.currentThread().name) + repeat(24) { count -> + if (BackendConnectionMonitor.getInstance(project).isConnectionOk()) { + return@repeat + } + + try { + Log.log(logger::info, "waiting for connection {} [t:{}]", count, Thread.currentThread().name) + Thread.sleep(1000) + } catch (e: InterruptedException) { + //ignore + } + + try { + AnalyticsService.getInstance(project).environments + } catch (e: Throwable) { + //ignore + } + } + + if (!BackendConnectionMonitor.getInstance(project).isConnectionOk()) { + Log.log(logger::warn, "connection status is ok. [t:{}]", Thread.currentThread().name) + } else { + Log.log(logger::warn, "connection status is not ok. [t:{}]", Thread.currentThread().name) + } + } + + } \ No newline at end of file