From 09de3a27dcb3d19153b0b3c52ae58236e4a15726 Mon Sep 17 00:00:00 2001
From: Iain Collins
Date: Wed, 17 Nov 2021 18:05:12 +0000
Subject: [PATCH] Add check for newer release
This is a crude implementation to test release integration.
---
resources/installer.nsi | 2 +-
scripts/lib/build-options.js | 11 ++-
src/app/go.mod | 4 +-
src/app/go.sum | 4 +
src/app/main.go | 2 +
src/app/updater.go | 186 +++++++++++++++++++++++++++++++++++
src/web/public/launcher.html | 2 +-
7 files changed, 203 insertions(+), 8 deletions(-)
create mode 100644 src/app/updater.go
diff --git a/resources/installer.nsi b/resources/installer.nsi
index de66cca8..86b2a603 100644
--- a/resources/installer.nsi
+++ b/resources/installer.nsi
@@ -6,7 +6,7 @@
!define APP_NAME "ICARUS Terminal"
!define COMP_NAME "ICARUS"
-!define VERSION "00.00.00.00"
+!define VERSION "0.1.1.0"
!define COPYRIGHT "ICARUS"
!define DESCRIPTION "Application"
!define INSTALLER_NAME "..\dist\ICARUS Setup.exe"
diff --git a/scripts/lib/build-options.js b/scripts/lib/build-options.js
index 840e08f9..581df091 100644
--- a/scripts/lib/build-options.js
+++ b/scripts/lib/build-options.js
@@ -1,11 +1,14 @@
const path = require('path')
-const ROOT_DIR = path.join(__dirname, '..', '..')
+const PRODUCT_VERSION = '0.1.1.0'
+const APP_FILE_VERSION = PRODUCT_VERSION
+const SERVICE_FILE_VERSION = PRODUCT_VERSION
// Development builds are faster, larger and can contain debug routines
const DEVELOPMENT_BUILD = process.env.DEVELOPMENT || false
const DEBUG_CONSOLE = DEVELOPMENT_BUILD
+const ROOT_DIR = path.join(__dirname, '..', '..')
const BUILD_DIR = path.join(ROOT_DIR, 'build') // For intermediate build steps
const BIN_DIR = path.join(BUILD_DIR, 'bin') // For final binary build
const DIST_DIR = path.join(ROOT_DIR, 'dist') // For distributable build
@@ -16,8 +19,6 @@ const ASSETS_BUILD_DIR = path.join(BUILD_DIR, 'assets')
const INSTALLER_NSI = path.join(RESOURCES_DIR, 'installer.nsi') // Installer config
const INSTALLER_EXE = path.join(DIST_DIR, 'ICARUS Setup.exe') // Should match INSTALLER_NAME in .nsi
-const PRODUCT_VERSION = '0.0.0.1'
-
const APP_BINARY_NAME = 'ICARUS Terminal.exe'
const APP_UNOPTIMIZED_BUILD = path.join(BUILD_DIR, `~UNOPT_${safeBinaryName(APP_BINARY_NAME)}`)
const APP_OPTIMIZED_BUILD = path.join(BUILD_DIR, `~OPT_${safeBinaryName(APP_BINARY_NAME)}`)
@@ -28,7 +29,7 @@ const APP_VERSION_INFO = {
CompanyName: 'ICARUS',
ProductName: 'ICARUS Terminal',
FileDescription: 'ICARUS Terminal',
- FileVersion: '0.0.0.1',
+ FileVersion: APP_FILE_VERSION,
ProductVersion: PRODUCT_VERSION,
OriginalFilename: 'ICARUS Terminal.exe',
InternalName: 'ICARUS Terminal',
@@ -45,7 +46,7 @@ const SERVICE_VERSION_INFO = {
CompanyName: 'ICARUS',
ProductName: 'ICARUS Terminal Service',
FileDescription: 'ICARUS Terminal Service',
- FileVersion: '0.0.0.1',
+ FileVersion: SERVICE_FILE_VERSION,
ProductVersion: PRODUCT_VERSION,
OriginalFilename: 'ICARUS Service.exe',
InternalName: 'ICARUS Service',
diff --git a/src/app/go.mod b/src/app/go.mod
index e882a243..6250f159 100644
--- a/src/app/go.mod
+++ b/src/app/go.mod
@@ -1,4 +1,4 @@
-module iaincollins.com/m/v2
+module iaincollins.com/icarus-terminal/v2
go 1.17
@@ -6,8 +6,10 @@ require github.com/webview/webview v0.0.0-20210330151455-f540d88dde4e
require (
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect
+ github.com/gonutz/w32/v2 v2.2.2 // indirect
github.com/jchv/go-webview2 v0.0.0-20211023023319-977d8719321f // indirect
github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 // indirect
+ github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e // indirect
github.com/nvsoft/win v0.0.0-20160111051136-23d143e32c41 // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 // indirect
github.com/sqweek/dialog v0.0.0-20211002065838-9a201b55ab91 // indirect
diff --git a/src/app/go.sum b/src/app/go.sum
index 679e663a..9e7050d2 100644
--- a/src/app/go.sum
+++ b/src/app/go.sum
@@ -1,9 +1,13 @@
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I=
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
+github.com/gonutz/w32/v2 v2.2.2 h1:y6Y337TpuCXjYdFTq5p5NmcujEdAQiTB43kisovMk+0=
+github.com/gonutz/w32/v2 v2.2.2/go.mod h1:MgtHx0AScDVNKyB+kjyPder4xIi3XAcHS6LDDU2DmdE=
github.com/jchv/go-webview2 v0.0.0-20211023023319-977d8719321f h1:wF7bbDOcRcRlamAwWMNagyFVQubISAlisj6Ix8O8Hn0=
github.com/jchv/go-webview2 v0.0.0-20211023023319-977d8719321f/go.mod h1:7Q5nFip7HvzGJDYVfa22s/pb9T2X+XhEjLhylNf5dV8=
github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5 h1:pdFFlHXY9tZXmJz+tRSm1DzYEH4ebha7cffmm607bMU=
github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e h1:ZZCvgaRDZg1gC9/1xrsgaJzQUCQgniKtw0xjWywWAOE=
+github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e/go.mod h1:+rHyWac2R9oAZwFe1wGY2HBzFJJy++RHBg1cU23NkD8=
github.com/nvsoft/win v0.0.0-20160111051136-23d143e32c41 h1:s0qXnW0MxcRPYZpqbrITRo3tAbAdlQBPUCKf/akNMKg=
github.com/nvsoft/win v0.0.0-20160111051136-23d143e32c41/go.mod h1:bI2vvx1dagFt7tydvy947C0q6ET6k5MfIvWmmpLelpw=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
diff --git a/src/app/main.go b/src/app/main.go
index 07664f6c..1d4f7b66 100644
--- a/src/app/main.go
+++ b/src/app/main.go
@@ -48,6 +48,8 @@ var processGroup ProcessGroup
func main() {
startTime := time.Now()
+ CheckForUpdate()
+
_processGroup, err := NewProcessGroup()
if err != nil {
panic(err)
diff --git a/src/app/updater.go b/src/app/updater.go
new file mode 100644
index 00000000..1569d705
--- /dev/null
+++ b/src/app/updater.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "strings"
+ "encoding/json"
+ "github.com/jmoiron/jsonq"
+ "github.com/sqweek/dialog"
+ "github.com/gonutz/w32/v2"
+ "io/ioutil"
+ "net/http"
+ "time"
+ "regexp"
+ "os/exec"
+ "errors"
+ "io"
+ "os"
+)
+
+const LATEST_RELEASE_URL = "https://api.github.com/repos/iaincollins/icarus/releases/latest"
+
+type Release struct {
+ productVersion string
+ downloadUrl string
+}
+
+func CheckForUpdate() {
+ currentProductVersion := GetCurrentAppVersion()
+ release, releaseErr := GetLatestRelease(LATEST_RELEASE_URL)
+ if releaseErr != nil {
+ return
+ }
+
+ // If we are already running the latest release, do nothing
+ if (currentProductVersion == release.productVersion) {
+ return
+ }
+
+ ok := dialog.Message("%s", "A new version of ICARUS Terminal is available.\n\nWould you like to download the update?").Title("New version available").YesNo()
+ if (ok) {
+ downloadUrl := release.downloadUrl // In future may redirect to webpage instead
+ exec.Command("rundll32", "url.dll,FileProtocolHandler", downloadUrl).Start()
+
+ // This is disabled as we can't actually run the current installer this way
+ // because it requires escalated privilages. This could be addressed by
+ // using a different type of installer.
+ /*
+ downloadedFile, downloadErr := DownloadRelease(release)
+ if downloadErr != nil {
+ fmt.Println("Error downloading update", downloadErr.Error())
+ }
+
+ installerCmdInstance := exec.Command(downloadedFile)
+ installerCmdErr := installerCmdInstance.Start()
+ if installerCmdErr != nil {
+ fmt.Println("Error installing update", installerCmdErr.Error())
+ }
+ */
+ }
+
+ return
+}
+
+func GetCurrentAppVersion() string {
+ const path = "ICARUS Terminal.exe"
+
+ size := w32.GetFileVersionInfoSize(path)
+ if size <= 0 {
+ panic("GetFileVersionInfoSize failed")
+ }
+
+ info := make([]byte, size)
+ ok := w32.GetFileVersionInfo(path, info)
+ if !ok {
+ panic("GetFileVersionInfo failed")
+ }
+
+ /*
+ fixed, ok := w32.VerQueryValueRoot(info)
+ if !ok {
+ panic("VerQueryValueRoot failed")
+ }
+ version := fixed.FileVersion()
+ fileVersion := fmt.Sprintf(
+ "%d.%d.%d.%d",
+ version&0xFFFF000000000000>>48,
+ version&0x0000FFFF00000000>>32,
+ version&0x00000000FFFF0000>>16,
+ version&0x000000000000FFFF>>0,
+ )
+ */
+
+ translations, ok := w32.VerQueryValueTranslations(info)
+ if !ok {
+ panic("VerQueryValueTranslations failed")
+ }
+ if len(translations) == 0 {
+ panic("no translation found")
+ }
+ t := translations[0]
+
+ productVersion, ok := w32.VerQueryValueString(info, t, w32.ProductVersion)
+ if !ok {
+ panic("cannot get product version")
+ }
+
+ // Convert from version with build number (0.0.0.0) to semver version (0.0.0)
+ productVersion = regexp.MustCompile(`(\.[^\.]+)$`).ReplaceAllString(productVersion, ``)
+
+ return productVersion
+}
+
+func GetLatestRelease(releasesUrl string) (Release, error) {
+ release := Release{}
+
+ httpClient := http.Client{Timeout: time.Second * 5}
+
+ req, reqErr := http.NewRequest(http.MethodGet, releasesUrl, nil)
+ if reqErr != nil {
+ return release, reqErr
+ }
+
+ res, getErr := httpClient.Do(req)
+ if getErr != nil {
+ return release, getErr
+ }
+
+ if res.Body != nil {
+ defer res.Body.Close()
+ }
+
+ body, readErr := ioutil.ReadAll(res.Body)
+ if readErr != nil {
+ return release, readErr
+ }
+
+ // Hackery to convert the response into JSON that jsonq can parse
+ jsonObjectAsString := string(body)
+ // jsonObjectAsString = regexp.MustCompile(`^\[`).ReplaceAllString(jsonObjectAsString, `{"releases":[`)
+ // jsonObjectAsString = regexp.MustCompile(`\]$`).ReplaceAllString(jsonObjectAsString, `]}`)
+
+ // Use jsonq to access JSON
+ data := map[string]interface{}{}
+ dec := json.NewDecoder(strings.NewReader(jsonObjectAsString))
+ dec.Decode(&data)
+ jq := jsonq.NewQuery(data)
+
+ // Get properties from from JSON
+ // tag, _ := jq.String("releases", "0", "tag_name")
+ // productVersion := regexp.MustCompile(`^v`).ReplaceAllString(tag, ``)
+ // downloadUrl, _ := jq.String("releases", "0", "assets", "0", "browser_download_url")
+ tag, _ := jq.String("tag_name")
+ productVersion := regexp.MustCompile(`^v`).ReplaceAllString(tag, ``) // Converts tag (v0.0.0) to semver version (0.0.0) for easier comparion
+ downloadUrl, _ := jq.String("assets", "0", "browser_download_url")
+
+ if (downloadUrl == "") {
+ return release, errors.New("Could not get download URL")
+ }
+
+ release.productVersion = productVersion
+ release.downloadUrl = downloadUrl
+
+ return release, nil
+}
+
+func DownloadRelease(release Release) (string, error) {
+ pathToDownloadedFile := "ICARUS Update.exe"
+
+ // Get file to download
+ resp, err := http.Get(release.downloadUrl)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ // Create file
+ out, err := os.Create(pathToDownloadedFile)
+ if err != nil {
+ return "", err
+ }
+ defer out.Close()
+
+ // Write to file
+ _, err = io.Copy(out, resp.Body)
+
+ return pathToDownloadedFile, nil
+}
\ No newline at end of file
diff --git a/src/web/public/launcher.html b/src/web/public/launcher.html
index 9ccc85d9..2e08e17e 100644
--- a/src/web/public/launcher.html
+++ b/src/web/public/launcher.html
@@ -58,7 +58,7 @@ ICARUS
- Preview Build 0.0.0.1
+ Preview Build 0.1.1.0