diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index b93ee883c24..d10df415ec3 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -145,7 +145,7 @@ jobs: Remove-Item -Path "packages/insomnia/dist/win-unpacked/tosign" -Recurse -Force Remove-Item -Path "packages/insomnia/dist/win-unpacked/signed" -Recurse -Force - # re-packages the now code-signed electron-builder contents into a squirrel installer + # re-packages the now code-signed electron-builder contents into the installer - name: Package dist app (Windows only) if: runner.os == 'Windows' shell: bash @@ -244,6 +244,7 @@ jobs: name: ${{ runner.os }}-${{ runner.arch }}-artifacts path: | packages/insomnia/dist/*.exe + packages/insomnia/dist/*.yml packages/insomnia/dist/squirrel-windows/* packages/insomnia/dist/*.zip packages/insomnia/dist/*.dmg diff --git a/.github/workflows/release-recurring.yml b/.github/workflows/release-recurring.yml index e9f2e513d97..d964b066be4 100644 --- a/.github/workflows/release-recurring.yml +++ b/.github/workflows/release-recurring.yml @@ -88,3 +88,5 @@ jobs: packages/insomnia/dist/*.exe packages/insomnia/dist/*.tar.gz packages/insomnia/dist/*.zip + packages/insomnia/dist/*.yml + packages/insomnia/dist/squirrel-windows/*.exe diff --git a/package-lock.json b/package-lock.json index 73cd37b4974..7d6d711cbf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11547,6 +11547,70 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz", + "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.6.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -15960,7 +16024,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/less": { @@ -16162,6 +16225,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -21716,6 +21785,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -23884,6 +23959,7 @@ "dompurify": "^3.2.5", "electron-context-menu": "^3.6.1", "electron-log": "^4.4.8", + "electron-updater": "^6.6.2", "fastq": "^1.19.1", "graphql": "^16.10.0", "graphql-ws": "^5.16.2", diff --git a/packages/insomnia/customSign.js b/packages/insomnia/customSign.js index c14d82f8c5d..d545ecae949 100644 --- a/packages/insomnia/customSign.js +++ b/packages/insomnia/customSign.js @@ -5,12 +5,11 @@ const execAsync = util.promisify(exec); // adapted from https://www.electron.build/win.html#how-do-delegate-code-signing // It was possible code-sign installer after packaging, but some files are only available -// through hooking into the signing step of electron-builder while the final squirrel installer is being built -// This makes it possible to sign the Update.exe and stub of Insomnia.exe that end up in C:\Users\\AppData\Local\insomnia +// through hooking into the signing step of electron-builder while the final nsis installer is being built +// This makes it possible to sign the Update.exe and stub of Insomnia.exe that end up in the installation folder exports.default = async function (configuration) { - // skip signing if not windows squirrel - if (configuration.options.target.length === 0 || configuration.options.target[0].target !== 'squirrel') { - console.log('[customSign] Skipping signing because target is not windows squirrel.'); + if (configuration.options.target.length === 0) { + console.log('[customSign] Skipping signing because target is empty'); return; } diff --git a/packages/insomnia/electron-builder.config.js b/packages/insomnia/electron-builder.config.js index a913c3b7656..f2430751d6a 100644 --- a/packages/insomnia/electron-builder.config.js +++ b/packages/insomnia/electron-builder.config.js @@ -81,13 +81,37 @@ const config = { win: { target: [ { - target: 'squirrel', + target: 'nsis', + arch: ['x64'], }, + { + target: 'squirrel', + } ], signtoolOptions: { sign: './customSign.js', signingHashAlgorithms: ['sha256'], // avoid duplicate signing hook calls https://github.com/electron-userland/electron-builder/issues/3995#issuecomment-505725704 }, + publish: { + provider: 'generic', + url: 'http://localhost/', + }, + generateUpdatesFilesForAllChannels: true, + }, + nsis: { + artifactName: `${BINARY_PREFIX}-nsis-\${version}.\${ext}`, + include: './scripts/nsisInstall.nsh', + oneClick: false, + selectPerMachineByDefault: true, + allowToChangeInstallationDirectory: true, + installerIcon: './build/icon.ico', + installerSidebar: './src/icons/nsis-sidebar.bmp', + uninstallerSidebar: './src/icons/nsis-sidebar.bmp', + uninstallerIcon: './build/icon.ico', + createDesktopShortcut: true, + createStartMenuShortcut: true, + shortcutName: 'Insomnia', + deleteAppDataOnUninstall: false, }, squirrelWindows: { artifactName: `${BINARY_PREFIX}-\${version}.\${ext}`, @@ -139,16 +163,16 @@ const config = { }, }; -const { - env: { BUILD_TARGETS }, - platform, -} = process; -const targets = BUILD_TARGETS?.split(','); -if (platform && targets) { - console.log('overriding build targets to: ', targets); - const PLATFORM_MAP = { darwin: 'mac', linux: 'linux', win32: 'win' }; - config[PLATFORM_MAP[platform]].target = config[PLATFORM_MAP[platform]].target.filter(({ target }) => - targets.includes(target), - ); -} +// const { +// env: { BUILD_TARGETS }, +// platform, +// } = process; +// const targets = BUILD_TARGETS?.split(','); +// if (platform && targets) { +// console.log('overriding build targets to: ', targets); +// const PLATFORM_MAP = { darwin: 'mac', linux: 'linux', win32: 'win' }; +// config[PLATFORM_MAP[platform]].target = config[PLATFORM_MAP[platform]].target.filter(({ target }) => +// targets.includes(target), +// ); +// } module.exports = config; diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index d587275b2a3..4d8e094a63c 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -23,7 +23,7 @@ "lint": "eslint . --ext .js,.ts,.tsx --cache", "package": "npm run build && cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js", "package:windows:unpacked": "npm run build && cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js --dir", - "package:windows:dist": "cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js --win squirrel --prepackaged ./dist/win-unpacked", + "package:windows:dist": "cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js --win squirrel nsis --prepackaged ./dist/win-unpacked", "start": "npx -y concurrently -n browser,main --kill-others \"npm run start:dev-server\" \"npm run start:electron\"", "start:dev-server": "vite dev", "start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron --inspect=5858 .", @@ -55,6 +55,7 @@ "dompurify": "^3.2.5", "electron-context-menu": "^3.6.1", "electron-log": "^4.4.8", + "electron-updater": "^6.6.2", "fastq": "^1.19.1", "graphql": "^16.10.0", "graphql-ws": "^5.16.2", diff --git a/packages/insomnia/scripts/nsisInstall.nsh b/packages/insomnia/scripts/nsisInstall.nsh new file mode 100644 index 00000000000..26cb6cf0a5e --- /dev/null +++ b/packages/insomnia/scripts/nsisInstall.nsh @@ -0,0 +1,24 @@ +!macro customInit + ${ifNot} ${isUpdated} + StrCpy $0 "$PROFILE\AppData\Local\insomnia\Update.exe" + StrCpy $1 "$PROFILE\AppData\Local\insomnia\.dead" + IfFileExists $1 skip_uninstall + IfFileExists $0 0 skip_uninstall + MessageBox MB_YESNO "Existing Insomnia installation found, which must be uninstalled first.$\n$\nClick 'No' to exit this installer so you can uninstall yourself.$\n$\nClick 'Yes' to allow this installer to uninstall for you (your existing Insomnia data will be preserved)." IDYES do_uninstall IDNO exit_installer + do_uninstall: + nsExec::Exec '"$0" --uninstall -s' + Goto skip_uninstall + exit_installer: + Quit + skip_uninstall: + ${endIf} +!macroend + +!macro customInstall + SetOutPath "$INSTDIR" + DetailPrint "Creating installer-info.json..." + + FileOpen $0 "$INSTDIR\installer-info.json" w + FileWrite $0 '{"installer": "nsis"}' + FileClose $0 +!macroend diff --git a/packages/insomnia/src/icons/nsis-sidebar.bmp b/packages/insomnia/src/icons/nsis-sidebar.bmp new file mode 100644 index 00000000000..1fccc7f8e30 Binary files /dev/null and b/packages/insomnia/src/icons/nsis-sidebar.bmp differ diff --git a/packages/insomnia/src/main/nsisUpdate.ts b/packages/insomnia/src/main/nsisUpdate.ts new file mode 100644 index 00000000000..23bd7bc22d5 --- /dev/null +++ b/packages/insomnia/src/main/nsisUpdate.ts @@ -0,0 +1,81 @@ +import { dialog } from 'electron'; +import { autoUpdater } from 'electron-updater'; + +import { CHECK_FOR_UPDATES_INTERVAL } from '../common/constants'; +import { delay } from '../common/misc'; +import * as models from '../models/index'; +import { ipcMainOn } from './ipc/electron'; +import { _sendUpdateStatus, isUpdateSupported } from './updates'; + +export const initNsusUpdater = async () => { + autoUpdater.on('error', error => { + console.warn(`[updater] Error: ${error.message}`); + _sendUpdateStatus('Update Error'); + }); + autoUpdater.on('update-not-available', () => { + console.log('[updater] Not Available'); + _sendUpdateStatus('Up to Date'); + }); + autoUpdater.on('update-available', () => { + console.log('[updater] NSIS Update Available'); + _sendUpdateStatus('Downloading...'); + }); + autoUpdater.on('update-downloaded', async ({ releaseNotes, releaseName }) => { + console.log(`[updater] Downloaded ${releaseName}`); + console.log(`[updater] Downloaded ${releaseNotes}`); + _sendUpdateStatus('Performing backup...'); + _sendUpdateStatus('Updated (Restart Required)'); + + dialog + .showMessageBox({ + type: 'info', + buttons: ['Restart', 'Later'], + title: 'Application Update', + message: releaseName || '', + detail: 'A new version of Insomnia has been downloaded. Restart the application to apply the updates.', + }) + .then(returnValue => { + if (returnValue.response === 0) { + autoUpdater.quitAndInstall(); + } + }); + }); + const settings = await models.settings.get(); + const updateSupported = isUpdateSupported(); + + // perhaps disable this method of upgrading just incase it trigger before backup is complete + // on app start + if (updateSupported) { + if (settings.updateAutomatically) { + _checkForUpdates(); + } + // on an interval (3h) + setInterval(async () => { + const settings = await models.settings.get(); + if (settings.updateAutomatically) { + _checkForUpdates(); + } + }, CHECK_FOR_UPDATES_INTERVAL); + } + // on check now button pushed + ipcMainOn('manualUpdateCheck', async () => { + console.log('[updater] Manual NSIS update check'); + + _sendUpdateStatus('Checking'); + await delay(300); // Pacing + _checkForUpdates(); + }); +}; + +const _checkForUpdates = async () => { + try { + console.log(`[updater] Checking for NSIS updates`); + const settings = await models.settings.get(); + // set auto-update channel + autoUpdater.channel = settings.updateChannel; + autoUpdater.checkForUpdates(); + } catch (err) { + console.warn('[updater] Failed to check for NSIS updates:', err.message); + _sendUpdateStatus('Update Error'); + } +}; diff --git a/packages/insomnia/src/main/updates.ts b/packages/insomnia/src/main/updates.ts index 2eba4e8f46d..d8b3cd0df45 100644 --- a/packages/insomnia/src/main/updates.ts +++ b/packages/insomnia/src/main/updates.ts @@ -1,10 +1,13 @@ import { autoUpdater, BrowserWindow, dialog } from 'electron'; +import { promises as fsPromise } from 'fs'; +import path from 'path'; import { CHECK_FOR_UPDATES_INTERVAL, getAppId, getAppVersion, isDevelopment, UpdateURL } from '../common/constants'; import { delay } from '../common/misc'; import * as models from '../models/index'; import { invariant } from '../utils/invariant'; import { ipcMainOn } from './ipc/electron'; +import { initNsusUpdater } from './nsisUpdate'; export type UpdateStatus = | 'Update Error' @@ -16,7 +19,7 @@ export type UpdateStatus = | 'Updates Not Supported' | 'Check Now'; -const isUpdateSupported = () => { +export const isUpdateSupported = () => { if (process.platform === 'linux') { console.log('[updater] Not supported on this platform', process.platform); return false; @@ -45,13 +48,40 @@ const getUpdateUrl = (updateChannel: string): string | null => { return fullUrl.toString(); }; -const _sendUpdateStatus = (status: UpdateStatus) => { +export const _sendUpdateStatus = (status: UpdateStatus) => { for (const window of BrowserWindow.getAllWindows()) { window.webContents.send('updaterStatus', status); } }; +const isNsisInstaller = async () => { + if (process.platform !== 'win32') { + return false; + } + try { + const installDir = path.dirname(process.execPath); + // we inject this file(nsisInstall.nsh) during the NSIS build process to indicate the installer type + const flagFilePath = path.join(installDir, 'installer-info.json'); + + const content = await fsPromise.readFile(flagFilePath, 'utf-8'); + const json = JSON.parse(content); + console.log('installer type', json.installer); + return json.installer === 'nsis'; + } catch (err) { + console.warn('Failed to read installer-info.json:', err); + return false; + } +}; + export const init = async () => { + // use different update logic for windows nsis installer + if (process.platform === 'win32') { + const isNsis = await isNsisInstaller(); + if (isNsis) { + initNsusUpdater(); + return; + } + } autoUpdater.on('error', error => { console.warn(`[updater] Error: ${error.message}`); _sendUpdateStatus('Update Error');