diff --git a/ui/desktop/electron-app/.gitignore b/ui/desktop/electron-app/.gitignore index 9fde06842..0928c4d31 100644 --- a/ui/desktop/electron-app/.gitignore +++ b/ui/desktop/electron-app/.gitignore @@ -93,4 +93,7 @@ ember-dist/ ember-test/ # CLI -cli/ \ No newline at end of file +cli/ + +# Autoupdate +nextVersion/ diff --git a/ui/desktop/electron-app/config/cli.js b/ui/desktop/electron-app/config/cli.js index 1badbfbcc..8195701a9 100644 --- a/ui/desktop/electron-app/config/cli.js +++ b/ui/desktop/electron-app/config/cli.js @@ -10,7 +10,7 @@ const artifactDestination = path.resolve(__dirname, '..', 'cli'); const downloadArtifact = version => { const url = `https://releases.hashicorp.com/boundary/${version}/boundary_${version}_darwin_amd64.zip`; return new Promise((resolve, reject) => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boundary-artifact-')) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boundary-artifact-')); console.log('Create tmp artifact directory: ', tmpDir); const artifactFileName = url.split('/').pop(); diff --git a/ui/desktop/electron-app/config/forge.config.js b/ui/desktop/electron-app/config/forge.config.js index 4151023de..7cea2a94e 100644 --- a/ui/desktop/electron-app/config/forge.config.js +++ b/ui/desktop/electron-app/config/forge.config.js @@ -26,33 +26,33 @@ module.exports = { } }, packagerConfig: { - ignore: [ - "/ember-test(/|$)", - "/tests(/|$)" - ], - name: "Boundary", - appBundleId: "com.electron.boundary", + ignore: ['/ember-test(/|$)', '/tests(/|$)'], + name: 'Boundary', + appBundleId: 'com.electron.boundary', // TODO: where should the client version number come from? appVersion: `${formattedAppVersion}${formattedCLIVersion}\n`, - appCopyright: "Copyright © 2021 HashiCorp, Inc.", - icon: "./config/macos/icon.icns", + appCopyright: 'Copyright © 2021 HashiCorp, Inc.', + icon: './config/macos/icon.icns', osxSign: { identity: process.env.BOUNDARY_DESKTOP_SIGNING_IDENTITY, - "hardened-runtime": true, - entitlements: "./config/macos/entitlements.plist", - "entitlements-inherit": "./config/macos/entitlements.plist", - "signature-flags": "library" - } + 'hardened-runtime': true, + entitlements: './config/macos/entitlements.plist', + 'entitlements-inherit': './config/macos/entitlements.plist', + 'signature-flags': 'library', + }, }, makers: [ { - name: "@electron-forge/maker-dmg", + name: '@electron-forge/maker-dmg', config: { - title: "Boundary", - name: "boundary", - icon: "./config/macos/disk.icns", - background: "./config/macos/background.png" - } - } - ] -} + title: 'Boundary', + name: 'boundary', + icon: './config/macos/disk.icns', + background: './config/macos/background.png', + }, + }, + { + name: '@electron-forge/maker-zip', + }, + ], +}; diff --git a/ui/desktop/electron-app/package.json b/ui/desktop/electron-app/package.json index 4d6a6e9ac..fe26b126b 100644 --- a/ui/desktop/electron-app/package.json +++ b/ui/desktop/electron-app/package.json @@ -25,11 +25,13 @@ "electron-is-dev": "^1.2.0", "electron-squirrel-startup": "^1.0.0", "lookpath": "^1.1.0", + "node-html-parser": "^3.1.3", "shell-quote": "^1.7.2" }, "devDependencies": { "@electron-forge/cli": "6.0.0-beta.54", "@electron-forge/maker-dmg": "^6.0.0-beta.54", + "@electron-forge/maker-zip": "^6.0.0-beta.54", "decompress": "^4.2.1", "devtron": "^1.4.0", "electron": "12.0.2" diff --git a/ui/desktop/electron-app/src/app-updater.js b/ui/desktop/electron-app/src/app-updater.js new file mode 100644 index 000000000..568ed6807 --- /dev/null +++ b/ui/desktop/electron-app/src/app-updater.js @@ -0,0 +1,123 @@ +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { parse } = require('node-html-parser'); +const { autoUpdater, dialog } = require('electron'); + +const debug = process.env.DEBUG_APP_UPDATER +const releasesUrl = 'https://releases.hashicorp.com/boundary-desktop/'; +const currentVersion = '1.0.0-beta'; + +// Query releases url to find latest version +const findLatestVersion = (url) => { + return new Promise((resolve, reject) => { + let data = ''; + https.get(url, (response) => { + response.on('data', (chunk) => (data += chunk)); + response.on('end', () => { + const html = parse(data); + // Ignore first link as it navigates to root + const fullVersion = html.querySelectorAll('a')[1].innerHTML; + resolve(fullVersion.split('_')[1]); + }); + response.on('error', (err) => reject(err)); + }); + }); +}; + +// Create update config using downloaded archive +const createAppUpdaterConfig = (url, version, destination) => { + if (debug) console.log(`[app-updater] url: ${url}, destination: ${destination}`); + const json = { url, version}; + const config = JSON.stringify(json); + const configPath = `${destination}/feed.json`; + if (debug) console.log(`[app-updater] config: ${config}`); + fs.writeFileSync(configPath, config); + return configPath; +}; + +const downloadAndInstallUpdate = async (version) => { + const url = `${releasesUrl}boundary-desktop_${version}/boundary-desktop_${version}_darwin_amd64.zip`; + const destination = path.resolve(__dirname, '..', 'nextVersion'); + if (!fs.existsSync(destination)) fs.mkdirSync(destination); + + try { + let configPath; + if (debug) { + // Support hosted url and file paths + const location = process.env.LATEST_VERSION_LOCATION; + configPath = createAppUpdaterConfig( + location.match(/^https/i) ? location : `file://${location}`, + version, + destination + ); + } else { + configPath = createAppUpdaterConfig(url, version, destination); + } + + autoUpdater.on('update-downloaded', () => { + const dialogOpts = { + type: 'info', + buttons: ['Restart', 'Later'], + detail: + 'A new version has been downloaded. Restart the application to apply the updates.', + }; + dialog.showMessageBox(dialogOpts).then((returnValue) => { + if (returnValue.response === 0) autoUpdater.quitAndInstall(true, true); + }); + }); + + autoUpdater.on('error', (e) => { + dialog.showErrorBox('Could not update', e.message); + }); + + if (debug) console.log('[app-updater] config:', configPath); + autoUpdater.setFeedURL(`file://${configPath}`); + autoUpdater.checkForUpdates(); + } catch (e) { + dialog.showErrorBox('Could not update', e.message); + } +}; + +/** + * Configure inbuilt app updater to use a custom config file + * to download and install next available app version. + * TODO: download progress in dialog + * TODO: Read current version from app config +**/ +module.exports = { + run: async () => { + let latestVersion; + if (debug) { + latestVersion = process.env.LATEST_VERSION_TAG; + } else { + latestVersion = await findLatestVersion(releasesUrl); + } + + // Update not available - do nothing + if (latestVersion <= currentVersion) { + const dialogOpts = { + type: 'info', + icon: null, + detail: 'No updates available', + }; + + dialog.showMessageBox(dialogOpts); + return; + } + + // Update is available - prompt for download + const dialogOpts = { + type: 'info', + buttons: ['Download', 'Later'], + icon: null, + detail: 'A new version is available for download', + }; + + dialog.showMessageBox(dialogOpts).then((returnValue) => { + if (returnValue.response === 0) { + downloadAndInstallUpdate(latestVersion); + } + }); + }, +}; diff --git a/ui/desktop/electron-app/src/index.js b/ui/desktop/electron-app/src/index.js index 9dff06506..d2c8fe5cc 100644 --- a/ui/desktop/electron-app/src/index.js +++ b/ui/desktop/electron-app/src/index.js @@ -11,6 +11,7 @@ const { BrowserWindow, ipcMain, Menu, + MenuItem, } = require('electron'); require('./handlers.js'); @@ -18,7 +19,6 @@ const origin = require('./origin.js'); const { generateCSPHeader } = require('./content-security-policy.js'); const menu = require('./menu.js'); - const isDev = require('electron-is-dev'); // Register the custom file protocol @@ -101,11 +101,13 @@ app.on('ready', async () => { } } - // Configure menu in prod env - if (!isDev) { - const menuTemplate = Menu.buildFromTemplate(menu.generateMenuTemplate()); - Menu.setApplicationMenu(menuTemplate); + // Configure dev tools menu + const menuTemplate = Menu.buildFromTemplate(menu.generateMenuTemplate()); + if (isDev) { + const view = menuTemplate.getMenuItemById('view'); + view.submenu.append(new MenuItem({ role: 'toggleDevTools' })); } + Menu.setApplicationMenu(menuTemplate); mainWindow = new BrowserWindow({ width: 1280, diff --git a/ui/desktop/electron-app/src/menu.js b/ui/desktop/electron-app/src/menu.js index b4170ace7..2ba8eb6e7 100644 --- a/ui/desktop/electron-app/src/menu.js +++ b/ui/desktop/electron-app/src/menu.js @@ -1,4 +1,5 @@ const { shell } = require('electron'); +const appUpdater = require('./app-updater.js'); const generateMenuTemplate = () => { return [ @@ -7,6 +8,11 @@ const generateMenuTemplate = () => { label: 'Boundary', submenu: [ { role: 'about' }, + { + id: 'update', + label: 'Check for Updates', + click: async () => appUpdater.run(), + }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, @@ -44,6 +50,7 @@ const generateMenuTemplate = () => { }, // { role: 'viewMenu' } { + id: 'view', label: 'View', submenu: [ { role: 'reload' }, diff --git a/ui/desktop/electron-app/yarn.lock b/ui/desktop/electron-app/yarn.lock index 45dcdf0a4..07dd1d9d8 100644 --- a/ui/desktop/electron-app/yarn.lock +++ b/ui/desktop/electron-app/yarn.lock @@ -155,6 +155,16 @@ optionalDependencies: electron-installer-dmg "^3.0.0" +"@electron-forge/maker-zip@^6.0.0-beta.54": + version "6.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-6.0.0-beta.54.tgz#83d322afd912587fd233e87c30339d8150f29bed" + integrity sha512-wbJhK1rDOCZMTtKrjvavT8R+Yi+v/6axsnTXvzbkzzMQ0xADKNslTwzO6mmbBJea4oIbYmQ44DRAjI21TNyQ/A== + dependencies: + "@electron-forge/maker-base" "6.0.0-beta.54" + "@electron-forge/shared-types" "6.0.0-beta.54" + cross-zip "^3.0.0" + fs-extra "^9.0.1" + "@electron-forge/plugin-base@6.0.0-beta.54": version "6.0.0-beta.54" resolved "https://registry.yarnpkg.com/@electron-forge/plugin-base/-/plugin-base-6.0.0-beta.54.tgz#1847eeee00e9558d61ef9ff7c2d7c9f5dbf1109c" @@ -508,6 +518,11 @@ bluebird@^3.1.1, bluebird@^3.5.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + boolean@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.2.tgz#df1baa18b6a2b0e70840475e1d93ec8fe75b2570" @@ -791,6 +806,29 @@ cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" +cross-zip@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cross-zip/-/cross-zip-3.1.0.tgz#2b7d33f2a893bf83e232ccbabf4c6c706f6b313c" + integrity sha512-aX02l0SD3KE27pMl69gkxDdDM5D3u9Ic4Je+2b1B2fP0dWnlWWY6ns2Vk5DEgCXJRhL3GasSpicNQRNbDkq0+w== + dependencies: + rimraf "^3.0.0" + +css-select@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" + integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== + dependencies: + boolbase "^1.0.0" + css-what "^4.0.0" + domhandler "^4.0.0" + domutils "^2.4.3" + nth-check "^2.0.0" + +css-what@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" + integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -956,6 +994,36 @@ devtron@^1.4.0: highlight.js "^9.3.0" humanize-plus "^1.8.1" +dom-serializer@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43" + integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.4.3: + version "2.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.1.tgz#9b8e84b5d9f788499ae77506ea832e9b4f9aa1c0" + integrity sha512-hO1XwHMGAthA/1KL7c83oip/6UWo3FlUNIuWiWKltoiQ5oCOiqths8KknvY2jpOohUoUgnwa/+Rm7UpwpSbY/Q== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.1.0" + ds-store@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/ds-store/-/ds-store-0.1.6.tgz#d1024ef746ed0c13f0f7fec85c7e858e8c4b7ca7" @@ -1102,6 +1170,11 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + env-paths@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -1614,6 +1687,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + highlight.js@^10.4.1, highlight.js@^9.3.0: version "10.6.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.6.0.tgz#0073aa71d566906965ba6e1b7be7b2682f5e18b6" @@ -2277,6 +2355,14 @@ node-gyp@^7.1.0: tar "^6.0.2" which "^2.0.2" +node-html-parser@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-3.1.3.tgz#987d39bc8df27bc70cbe3b4e9a31fd84f7454dfb" + integrity sha512-pCE2I5UY5iOBnWdJQkbYZSk+fyq2zepw0nsELpHQjVFyCzOeZhkMhnvKqGceKgzWsWx7EG4KtMqsy9Eklf5Thw== + dependencies: + css-select "^3.1.2" + he "1.2.0" + node-pre-gyp@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" @@ -2369,6 +2455,13 @@ npmlog@^4.0.2, npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +nth-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" + integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + dependencies: + boolbase "^1.0.0" + nugget@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" @@ -2943,7 +3036,7 @@ rimraf@^2.6.1, rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==