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