Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage binaries in the background #75

Merged
merged 6 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/68.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The plugin now manages binaries in the background to prevent UI freeze.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.metalbear.mirrord
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.notification.NotificationType
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.system.CpuArch
Expand All @@ -14,10 +13,8 @@ import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

private const val CLI_BINARY = "mirrord"
private const val VERSION_ENDPOINT = "https://version.mirrord.dev/v1/version"
Expand All @@ -26,8 +23,125 @@ private const val DOWNLOAD_ENDPOINT = "https://github.com/metalbear-co/mirrord/r
/**
* For dynamically fetching and storing mirrord binary.
*/
class MirrordBinaryManager(private val service: MirrordProjectService) {
class MirrordBinary(val command: String) {
class MirrordBinaryManager(val service: MirrordProjectService) {
@Volatile
private var latestSupportedVersion: String? = null

private val downloadTaskRunning: AtomicBoolean = AtomicBoolean(false)

init {
VersionCheckTask(this, null).queue()
}

/**
* Background task for checking the latest supported version of the mirrord binary.
*/
private class VersionCheckTask(private val manager: MirrordBinaryManager, val product: String?) : Task.Backgroundable(manager.service.project, "mirrord", true) {
override fun run(indicator: ProgressIndicator) {
val pluginVersion = if (
System.getenv("CI_BUILD_PLUGIN") == "true" ||
System.getenv("PLUGIN_TESTING_ENVIRONMENT") == "true"
) {
"test"
} else {
VERSION ?: "unknown"
}

indicator.text = "mirrord is checking the latest supported binary version..."

val url = StringBuilder(VERSION_ENDPOINT)
.append("?source=3")
.append("&version=")
.append(URLEncoder.encode(pluginVersion, Charset.defaultCharset()))
.append("&platform=")
.append(URLEncoder.encode(SystemInfo.OS_NAME, Charset.defaultCharset()))
.toString()

val client = HttpClient.newHttpClient()
val builder = HttpRequest
.newBuilder(URI(url))
.timeout(Duration.ofSeconds(10L))
.GET()

product?.let { builder.header("user-agent", it) }

val response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString())
manager.latestSupportedVersion = response.body()
}

override fun onThrowable(error: Throwable) {
MirrordLogger.logger.debug("binary version check failed", error)
manager.service.notifier.notifyRichError(
"failed to check the latest supported version of the mirrord binary"
)
}
}

/**
* Background task for downloading the selected version of the mirrord binary.
*/
private class BinaryDownloadTask(private val manager: MirrordBinaryManager, val version: String) : Task.Backgroundable(manager.service.project, "mirrord", true) {
override fun run(indicator: ProgressIndicator) {
val url = if (SystemInfo.isMac) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_mac_universal"
} else if (SystemInfo.isLinux || SystemInfo.isWindows) {
if (CpuArch.isArm64()) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_linux_aarch64"
} else if (CpuArch.isIntel64()) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_linux_x86_64"
} else {
throw RuntimeException("Unsupported architecture: " + CpuArch.CURRENT.name)
}
} else {
throw RuntimeException("Unsupported platform: " + SystemInfo.OS_NAME)
}

indicator.text = "mirrord is downloading binary version $version..."
indicator.fraction = 0.0

val connection = URI(url).toURL().openConnection()
connection.connect()
val size = connection.contentLength
val stream = connection.getInputStream()

val bytes = ByteArray(size)
var bytesRead = 0
while (bytesRead < size) {
val toRead = minOf(4096, size - bytesRead)
val readNow = stream.read(bytes, bytesRead, toRead)
if (readNow == -1) {
break
}
bytesRead += readNow
indicator.fraction = bytesRead.toDouble() / size.toDouble()
}

stream.close()

val destination = MirrordPathManager.getPath(CLI_BINARY, true)
Files.createDirectories(destination.parent)
Files.write(destination, bytes)
destination.toFile().setExecutable(true)
}

override fun onThrowable(error: Throwable) {
MirrordLogger.logger.debug("binary dowload failed", error)
manager.service.notifier.notifyRichError("failed to download mirrord binary version $version")
}

override fun onFinished() {
manager.downloadTaskRunning.set(false)
}

override fun onSuccess() {
manager.service.notifier.notifySimple(
"downloaded mirrord binary version $version",
NotificationType.INFORMATION
)
}
}

private class MirrordBinary(val command: String) {
val version: String

init {
Expand Down Expand Up @@ -64,7 +178,7 @@ class MirrordBinaryManager(private val service: MirrordProjectService) {
}

/**
* @return the local installation of mirrord
* @return the local installation of mirrord, either in `PATH` or in plugin storage
*/
private fun getLocalBinary(requiredVersion: String?, wslDistribution: WSLDistribution?): MirrordBinary? {
try {
Expand All @@ -90,154 +204,46 @@ class MirrordBinaryManager(private val service: MirrordProjectService) {
return null
}

private fun getLatestSupportedVersion(product: String, timeout: Duration): String {
val environment = CompletableFuture<String>()
val versionCheckTask = object : Task.Backgroundable(service.project, "mirrord", true) {
override fun run(indicator: ProgressIndicator) {
indicator.text = "mirrord is checking the latest supported version..."

val testing = System.getenv("CI_BUILD_PLUGIN") == "true" ||
System.getenv("PLUGIN_TESTING_ENVIRONMENT") == "true"
val version = if (testing) {
"test"
} else {
VERSION ?: "unknown"
}

val url = StringBuilder(VERSION_ENDPOINT)
.append("?source=3")
.append("&version=")
.append(URLEncoder.encode(version, Charset.defaultCharset()))
.append("&platform=")
.append(URLEncoder.encode(SystemInfo.OS_NAME, Charset.defaultCharset()))
.toString()

val client = HttpClient.newHttpClient()
val request = HttpRequest
.newBuilder(URI(url))
.header("user-agent", product)
.timeout(timeout)
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())

environment.complete(response.body())
}

override fun onThrowable(error: Throwable) {
MirrordLogger.logger.debug(error)
}
}

ProgressManager.getInstance().run(versionCheckTask)

return environment.get(timeout.seconds, TimeUnit.SECONDS)
}

private fun downloadBinary(destination: Path, version: String) {
val url = if (SystemInfo.isMac) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_mac_universal"
} else if (SystemInfo.isLinux || SystemInfo.isWindows) {
if (CpuArch.isArm64()) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_linux_aarch64"
} else if (CpuArch.isIntel64()) {
"$DOWNLOAD_ENDPOINT/$version/mirrord_linux_x86_64"
} else {
throw RuntimeException("Unsupported architecture: " + CpuArch.CURRENT.name)
}
} else {
throw RuntimeException("Unsupported platform: " + SystemInfo.OS_NAME)
}

val environment = CompletableFuture<ByteArray>()
val versionCheckTask = object : Task.Backgroundable(service.project, "mirrord", true) {
override fun run(indicator: ProgressIndicator) {
indicator.text = "mirrord is downloading version $version..."
indicator.fraction = 0.0

val connection = URI(url).toURL().openConnection()
connection.connect()
val size = connection.contentLength
val stream = connection.getInputStream()

val bytes = ByteArray(size)
var bytesRead = 0
while (bytesRead < size) {
val toRead = minOf(4096, size - bytesRead)
val readNow = stream.read(bytes, bytesRead, toRead)
if (readNow == -1) {
break
}
bytesRead += readNow
indicator.fraction = bytesRead.toDouble() / size.toDouble()
}

stream.close()
environment.complete(bytes)
}

override fun onThrowable(error: Throwable) {
MirrordLogger.logger.debug(error)
}
}

ProgressManager.getInstance().run(versionCheckTask)

val bytes = try {
environment.get()
} catch (e: Exception) {
throw RuntimeException("failed to download mirrord version $version", e)
}

Files.createDirectories(destination.parent)
Files.write(destination, bytes)
destination.toFile().setExecutable(true)
}

/**
* Fetches mirrord binary.
* Downloads and stores it in the plugin directory if necessary (local version is missing or outdated).
* Finds a local installation of the mirrord binary.
* Schedules a binary version check to be executed in the background.
* If the mirrord binary is not found, schedules a download to be executed in the background.
*
* @return the path to the binary
*
* @throws RuntimeException
* @throws RuntimeException if the binary is not found
*/
fun getBinary(product: String, wslDistribution: WSLDistribution?): String {
val staleBinary = this.getLocalBinary(null, wslDistribution)

val timeout = if (staleBinary == null) 10L else 1L
val latestVersion = try {
this.getLatestSupportedVersion(product, Duration.ofSeconds(timeout))
} catch (e: Exception) {
MirrordLogger.logger.debug("failed to check the latest supported version of the mirrord binary", e)
null
}
VersionCheckTask(this, product).queue()

latestVersion?.let { version ->
latestSupportedVersion?.let { version ->
getLocalBinary(version, wslDistribution)?.let { return it.command }

try {
val destinationPath = MirrordPathManager.getPath(CLI_BINARY, true)
downloadBinary(destinationPath, version)
return MirrordBinary(destinationPath.toString()).command
} catch (e: Exception) {
MirrordLogger.logger.debug("failed to download the mirrord binary", e)
if (!downloadTaskRunning.compareAndExchange(false, true)) {
BinaryDownloadTask(this, version).queue()
}
}

staleBinary?.let {
this.getLocalBinary(null, wslDistribution)?.let {
var message = "using a local installation with version ${it.version}"
latestSupportedVersion?.let { version ->
message += ", latest supported version is $version"
}

service
.notifier
.notification(
"failed to download the mirrord binary, using a local installation with version ${it.version}",
NotificationType.WARNING
)
.notification(message, NotificationType.WARNING)
.withDontShowAgain(MirrordSettingsState.NotificationId.POSSIBLY_OUTDATED_BINARY_USED)
.fire()

return it.command
}

throw RuntimeException("no local installation found and download failed")
val message = if (downloadTaskRunning.get()) {
"no local installation found, downloading in the background"
} else {
"no local installation found, mirrord needs network access to download binary"
}
throw RuntimeException(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class MirrordExecManager(private val service: MirrordProjectService) {
val path = try {
service.binaryManager.getBinary(product, wslDistribution)
} catch (e: Exception) {
service.notifier.notifyRichError("failed to fetch mirrord binary: ${e.message}")
service.notifier.notifyRichError("failed to found mirrord binary: ${e.message}")
Razz4780 marked this conversation as resolved.
Show resolved Hide resolved
return null
}
wslDistribution?.let {
Expand Down