diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 777cda4d..c1e46610 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: if: ${{ !matrix.os.run-integration }} run: "./gradlew check --no-daemon" - name: Test build update4j config - if: ${{ !matrix.os.run-integration }} + if: ${{ matrix.os.run-integration }} run: | ./gradlew getDependencySources ./gradlew :app:mergeLocalLibs @@ -84,6 +84,7 @@ jobs: CC_TEST_REPORTER_ID: ${{secrets.codecovToken}} JACOCO_SOURCE_PATH: | ${{github.workspace}}/app/src/main/kotlin \ + ${{github.workspace}}/bootstrap/src/main/kotlin \ ${{github.workspace}}/lib/configuration/src/main/kotlin \ ${{github.workspace}}/lib/helper/src/main/kotlin \ ${{github.workspace}}/lib/jsonhelper/src/main/kotlin \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 239bb35b..4fd043da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,14 +38,14 @@ jobs: - uses: olegtarasov/get-tag@v2 id: tagName - name: Gradle package - run: "./gradlew packageApp" + run: "./gradlew bootstrap:packageApp" env: RELEASE_VERSION: ${{ steps.tagName.outputs.tag }} - name: Update binary uses: actions/upload-artifact@v2 with: name: ${{ matrix.os.artifact }} - path: "./app/${{ matrix.os.path }}" + path: "./bootstrap/${{ matrix.os.path }}" if-no-files-found: error - name: Build update4j config and jar if: ${{ matrix.os.name == 'ubuntu-latest' }} diff --git a/app/src/main/kotlin/insulator/views/configurations/ListClusterView.kt b/app/src/main/kotlin/insulator/views/configurations/ListClusterView.kt index 96280678..bc741fe3 100644 --- a/app/src/main/kotlin/insulator/views/configurations/ListClusterView.kt +++ b/app/src/main/kotlin/insulator/views/configurations/ListClusterView.kt @@ -120,7 +120,8 @@ class ListClusterView @Inject constructor( it.height = 500.0 it.resizableProperty().value = false } - checkVersion() + // Enable ONLY when the bootstrap need to be updated + // checkVersion() super.onDock() title = "Clusters" } diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle new file mode 100644 index 00000000..ce6d9742 --- /dev/null +++ b/bootstrap/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'insulator.application' + id 'insulator.jpackage' +} + +compileKotlin { kotlinOptions.jvmTarget = "1.8" } +compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } + +jar { manifest { attributes('Main-Class': 'insulator.BootstrapKt') } } + +application { mainClassName ='insulator.BootstrapKt' } + +dependencies { + implementation(group: 'org.update4j', name: 'update4j', version: "1.5.6") +} \ No newline at end of file diff --git a/bootstrap/src/main/kotlin/insulator/Bootstrap.kt b/bootstrap/src/main/kotlin/insulator/Bootstrap.kt new file mode 100644 index 00000000..520f35aa --- /dev/null +++ b/bootstrap/src/main/kotlin/insulator/Bootstrap.kt @@ -0,0 +1,79 @@ +package insulator + +import org.update4j.Archive +import org.update4j.Configuration +import org.update4j.UpdateOptions +import org.update4j.service.UpdateHandler +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.InputStream +import java.io.InputStreamReader +import java.net.SocketTimeoutException +import java.net.URL +import java.net.UnknownHostException +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import javax.swing.JFrame +import javax.swing.JOptionPane +import javax.swing.WindowConstants.DISPOSE_ON_CLOSE +import kotlin.system.exitProcess + +private val view = BootstrapViewManager(JFrame("Bootstrap").apply { defaultCloseOperation = DISPOSE_ON_CLOSE }) + +fun main(args: Array) { + tryLoadLocalConfig() + .let { (left, right) -> left?.let { Result.failure(it) } ?: Result.success(right!!) } + .mapCatching { config -> + if (config.requiresUpdate()) { + val result = config.update(UpdateOptions.archive(updatePath).updateHandler(InsulatorUpdateHandler())) + result.exception?.let { throw it } ?: Archive.read(updatePath).install() + } + config + } + .mapCatching { it.launch() } + .fold({ Unit }, { handleErrors(it) }) +} + +private fun handleErrors(exception: Throwable) { + val errorMessage = when (exception) { + is UnknownHostException -> Triple("Unable to check for updates. Check your internet connection and retry", "Download error", JOptionPane.WARNING_MESSAGE) + is SocketTimeoutException -> Triple("Unable to complete the download. Check your internet connection and retry", "Timeout error", JOptionPane.WARNING_MESSAGE) + is FileNotFoundException -> Triple("Unable to find the remote configuration file.", "Download error", JOptionPane.ERROR_MESSAGE) + else -> Triple("Unexpected error: $exception.", "Unexpected error", JOptionPane.ERROR_MESSAGE) + } + view.showMessageDialog(errorMessage.first, errorMessage.second, errorMessage.third) + exitProcess(-1) +} + +fun saveConfig(stream: InputStream): InputStream { + if (!File(localPath).exists()) File(localPath).mkdirs() + Files.copy( + stream, + Paths.get(localConfigFile), + StandardCopyOption.REPLACE_EXISTING + ) + return FileInputStream(localConfigFile) +} + +fun tryLoadLocalConfig(): Pair = + URL(configPath).runCatching { openConnection().getInputStream() } + .fold( + { Result.success(it) }, + { error -> + with(File(localConfigFile)) { + if (exists()) Result.success(inputStream()) + else Result.failure(error) + } + } + ) + .mapCatching { stream -> saveConfig(stream) } + .mapCatching { stream -> InputStreamReader(stream).use { Configuration.read(it) } } + .fold({ Pair(null, it) }, { Pair(it, null) }) + +class InsulatorUpdateHandler : UpdateHandler { + override fun startDownloads() = view.showUpdateView() + override fun updateDownloadProgress(frac: Float) = view.updateDownloadProgress(frac) + override fun doneDownloads() = view.closeUpdateView() +} diff --git a/bootstrap/src/main/kotlin/insulator/BootstrapViewManager.kt b/bootstrap/src/main/kotlin/insulator/BootstrapViewManager.kt new file mode 100644 index 00000000..954ebf49 --- /dev/null +++ b/bootstrap/src/main/kotlin/insulator/BootstrapViewManager.kt @@ -0,0 +1,24 @@ +package insulator + +import java.awt.event.WindowEvent +import javax.swing.JFrame +import javax.swing.JOptionPane + +class BootstrapViewManager(private val frame: JFrame) { + + private var updateView: UpdateView? = null + + fun updateDownloadProgress(fraction: Float) { + updateView?.updateDownloadProgress(fraction) + } + + fun showMessageDialog(message: String, title: String, iconId: Int) = JOptionPane.showMessageDialog(frame, message, title, iconId) + + fun closeUpdateView() { + frame.dispatchEvent(WindowEvent(frame, WindowEvent.WINDOW_CLOSING)) + } + + fun showUpdateView() { + updateView = UpdateView(frame) + } +} diff --git a/bootstrap/src/main/kotlin/insulator/Constants.kt b/bootstrap/src/main/kotlin/insulator/Constants.kt new file mode 100644 index 00000000..945ad908 --- /dev/null +++ b/bootstrap/src/main/kotlin/insulator/Constants.kt @@ -0,0 +1,18 @@ +package insulator + +import java.nio.file.Path + +const val appName = "Insulator" +val localPath: String by lazy { + with(System.getProperty("os.name")) { + when { + contains("nux") -> "${System.getProperty("user.home")!!}/.config/$appName/" + contains("win") -> "${System.getenv("LOCALAPPDATA")}/$appName" + else -> "${System.getProperty("user.home")!!}/Library/Application Support/$appName/" + } + } +} + +val localConfigFile = "${localPath}insulator-update.xml" +val updatePath: Path = Path.of(System.getProperty("java.io.tmpdir") ?: "", "update.zip") +const val configPath = "https://github.com/andrea-vinci/Insulator/releases/latest/download/insulator-update.xml" diff --git a/bootstrap/src/main/kotlin/insulator/UpdateView.kt b/bootstrap/src/main/kotlin/insulator/UpdateView.kt new file mode 100644 index 00000000..3bba79d1 --- /dev/null +++ b/bootstrap/src/main/kotlin/insulator/UpdateView.kt @@ -0,0 +1,68 @@ +package insulator + +import java.awt.Component +import java.awt.Dimension +import java.awt.Image +import java.awt.RenderingHints +import java.awt.Toolkit +import java.awt.image.BufferedImage +import javax.imageio.ImageIO +import javax.swing.BorderFactory +import javax.swing.BoxLayout +import javax.swing.ImageIcon +import javax.swing.JFrame +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JProgressBar +import kotlin.math.floor + +class UpdateView(private val frame: JFrame) { + private val progressBar = JProgressBar().apply { isStringPainted = true } + + init { + with(frame) { + add( + JPanel().apply { + add(insulatorIcon()) + add(progressBar) + + layout = BoxLayout(this, BoxLayout.PAGE_AXIS) + border = BorderFactory.createEmptyBorder(10, 20, 20, 20) + } + ) + fixSize(300, 130) + center() + isVisible = true + } + } + + fun updateDownloadProgress(frac: Float) = + with(progressBar) { + value = floor(frac.toDouble() * 100).toInt() + if (value >= 98) frame.isVisible = false + } + + private fun insulatorIcon() = + ImageIO.read(this.javaClass.getResource("/icon.png")) + .let { JLabel(ImageIcon(getScaledImage(it, 60, 60))).apply { text = "Insulator auto-update" } } + .also { it.alignmentX = Component.CENTER_ALIGNMENT } + + private fun getScaledImage(srcImg: Image, width: Int, height: Int) = + BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB).apply { + with(createGraphics()) { + setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) + drawImage(srcImg, 0, 0, width, height, null) + dispose() + } + } + + private fun JFrame.center() = apply { + val screen = Toolkit.getDefaultToolkit().screenSize + setLocation(screen.width / 2 - size.width / 2, screen.height / 2 - size.height / 2) + } + + private fun JFrame.fixSize(width: Int, height: Int) = this.apply { + setSize(width, height) + maximumSize = Dimension(width, height) + } +} diff --git a/bootstrap/src/main/resources/icon.png b/bootstrap/src/main/resources/icon.png new file mode 100644 index 00000000..f1aebf34 Binary files /dev/null and b/bootstrap/src/main/resources/icon.png differ diff --git a/buildSrc/src/main/groovy/insulator.application.gradle b/buildSrc/src/main/groovy/insulator.application.gradle index 5d6deb24..afa8eda0 100644 --- a/buildSrc/src/main/groovy/insulator.application.gradle +++ b/buildSrc/src/main/groovy/insulator.application.gradle @@ -2,5 +2,6 @@ plugins { id 'application' id 'distribution' id 'insulator.update4j' + id 'insulator.base' id 'com.github.johnrengelman.shadow' } \ No newline at end of file diff --git a/buildSrc/src/main/groovy/insulator.base.gradle b/buildSrc/src/main/groovy/insulator.base.gradle new file mode 100644 index 00000000..db06b084 --- /dev/null +++ b/buildSrc/src/main/groovy/insulator.base.gradle @@ -0,0 +1,23 @@ +plugins{ + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.kotlin.kapt' +} + +repositories { + jcenter() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://dl.bintray.com/arrow-kt/arrow-kt/" } + maven { url "https://packages.confluent.io/maven/" } + maven { url "https://kotlin.bintray.com/kotlinx/" } + maven { url "https://repository.mulesoft.org/nexus/content/repositories/public/" } +} + +compileKotlin { kotlinOptions.jvmTarget = "1.8" } +compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } + +dependencies { + // Kotlin + implementation platform('org.jetbrains.kotlin:kotlin-bom') + implementation(group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8') +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/insulator.common.gradle b/buildSrc/src/main/groovy/insulator.common.gradle index 875da90d..454a4551 100644 --- a/buildSrc/src/main/groovy/insulator.common.gradle +++ b/buildSrc/src/main/groovy/insulator.common.gradle @@ -1,34 +1,17 @@ plugins { id 'idea' id 'jacoco' - id 'org.jetbrains.kotlin.jvm' - id 'org.jetbrains.kotlin.kapt' + id 'insulator.base' id 'org.jlleitschuh.gradle.ktlint' id 'com.adarshr.test-logger' } -repositories { - jcenter() - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - maven { url "https://dl.bintray.com/arrow-kt/arrow-kt/" } - maven { url "https://packages.confluent.io/maven/" } - maven { url "https://kotlin.bintray.com/kotlinx/" } - maven { url "https://repository.mulesoft.org/nexus/content/repositories/public/" } -} - -compileKotlin { kotlinOptions.jvmTarget = "1.8" } -compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } - tasks.withType(Test) { useJUnitPlatform() } def arrow_version = "0.11.0" def kotest_version = "4.3.0" dependencies { - // Kotlin - implementation platform('org.jetbrains.kotlin:kotlin-bom') - implementation(group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8') // Arrow implementation(group: 'io.arrow-kt', name: 'arrow-annotations', version: "$arrow_version") diff --git a/buildSrc/src/main/groovy/insulator.jpackage.gradle b/buildSrc/src/main/groovy/insulator.jpackage.gradle index a9dce68b..eb3936a5 100644 --- a/buildSrc/src/main/groovy/insulator.jpackage.gradle +++ b/buildSrc/src/main/groovy/insulator.jpackage.gradle @@ -4,7 +4,7 @@ task dist(type: Copy) { group = 'distribution' dependsOn 'distZip' - def zipFile = file("${buildDir}/distributions/app.zip") + def zipFile = file("${buildDir}/distributions/${project.name}.zip") def outputDir = file("${buildDir}/distributions") from zipTree(zipFile) @@ -17,7 +17,7 @@ task packageApp(type: Exec) { // build command def appVersion = "${System.getenv().get("RELEASE_VERSION") ?: "0.0.0"}" - def command = ['jpackage', '--input', './build/distributions/app/lib/', '--main-jar', 'app.jar', '-d', '.', + def command = ['jpackage', '--input', "./build/distributions/${project.name}/lib/", '--main-jar', "${project.name}.jar", '-d', '.', '--name', 'Insulator', '--java-options', "'--enable-preview'", '--app-version', appVersion] // customization for each OS diff --git a/buildSrc/src/main/groovy/insulator.update4j.gradle b/buildSrc/src/main/groovy/insulator.update4j.gradle index 31918ec1..46a6f6bf 100644 --- a/buildSrc/src/main/groovy/insulator.update4j.gradle +++ b/buildSrc/src/main/groovy/insulator.update4j.gradle @@ -1,8 +1,3 @@ -plugins { - id 'insulator.common' -} - - task mergeLocalLibs(type: Jar) { group = 'distribution' if (!project.tasks.names.contains("dist")) { diff --git a/scripts/constants.py b/scripts/constants.py index b7ff7053..e46df95e 100644 --- a/scripts/constants.py +++ b/scripts/constants.py @@ -2,9 +2,9 @@ - - - + + + @@ -16,21 +16,21 @@ manual_dependencies = { "kotlinx-serialization-runtime-jvm-1.0-M1-1.4.0-rc-218.jar": '', # javafx for mac - "javafx-swing-15.0.1-mac.jar": '', - "javafx-graphics-15.0.1-mac.jar": '', - "javafx-base-15.0.1-mac.jar": '', - "javafx-controls-15.0.1-mac.jar": '', - "javafx-fxml-15.0.1-mac.jar": '', + "javafx-swing-15.0.1-mac.jar": '', + "javafx-graphics-15.0.1-mac.jar": '', + "javafx-base-15.0.1-mac.jar": '', + "javafx-controls-15.0.1-mac.jar": '', + "javafx-fxml-15.0.1-mac.jar": '', # javafx for linux - "javafx-swing-15.0.1-linux.jar": '', - "javafx-graphics-15.0.1-linux.jar": '', - "javafx-base-15.0.1-linux.jar": '', - "javafx-controls-15.0.1-linux.jar": '', - "javafx-fxml-15.0.1-linux.jar": '', + "javafx-swing-15.0.1-linux.jar": '', + "javafx-graphics-15.0.1-linux.jar": '', + "javafx-base-15.0.1-linux.jar": '', + "javafx-controls-15.0.1-linux.jar": '', + "javafx-fxml-15.0.1-linux.jar": '', # javafx for windows - "javafx-swing-15.0.1-win.jar": '', - "javafx-graphics-15.0.1-win.jar": '', - "javafx-base-15.0.1-win.jar": '', - "javafx-controls-15.0.1-win.jar": '', - "javafx-fxml-15.0.1-win.jar": '', + "javafx-swing-15.0.1-win.jar": '', + "javafx-graphics-15.0.1-win.jar": '', + "javafx-base-15.0.1-win.jar": '', + "javafx-controls-15.0.1-win.jar": '', + "javafx-fxml-15.0.1-win.jar": '', } diff --git a/settings.gradle b/settings.gradle index 915eb36a..eb9e9702 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'insulator' include( "app", + "bootstrap", "lib:jsonhelper", "lib:helper", "lib:configuration",