2024-08-21 02:01:29 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-10-17 23:34:02 +02:00
import { dialog , ipcMain , Notification } from "electron" ;
2024-08-06 20:05:26 +02:00
import { autoUpdater } from "electron-updater" ;
2024-09-19 20:04:18 +02:00
import { readFileSync } from "fs" ;
import path from "path" ;
import YAML from "yaml" ;
2024-09-18 21:06:34 +02:00
import { FileService } from "../frontend/app/store/services" ;
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
import { RpcApi } from "../frontend/app/store/wshclientapi" ;
2024-09-18 21:06:34 +02:00
import { isDev } from "../frontend/util/isdev" ;
2024-08-06 20:05:26 +02:00
import { fireAndForget } from "../frontend/util/util" ;
2024-10-30 22:20:19 +01:00
import { delay } from "./emain-util" ;
2024-12-02 19:56:56 +01:00
import { focusedWaveWindow , getAllWaveWindows } from "./emain-window" ;
2024-09-19 20:04:18 +02:00
import { ElectronWshClient } from "./emain-wsh" ;
2024-08-06 20:05:26 +02:00
export let updater : Updater ;
2024-09-19 20:04:18 +02:00
function getUpdateChannel ( settings : SettingsType ) : string {
const updaterConfigPath = path . join ( process . resourcesPath ! , "app-update.yml" ) ;
const updaterConfig = YAML . parse ( readFileSync ( updaterConfigPath , { encoding : "utf8" } ) . toString ( ) ) ;
console . log ( "Updater config from binary:" , updaterConfig ) ;
const updaterChannel : string = updaterConfig . channel ? ? "latest" ;
const settingsChannel = settings [ "autoupdate:channel" ] ;
let retVal = settingsChannel ;
// If the user setting doesn't exist yet, set it to the value of the updater config.
2024-10-11 04:32:15 +02:00
// If the user was previously on the `latest` channel and has downloaded a `beta` version, update their configured channel to `beta` to prevent downgrading.
if ( ! settingsChannel || ( settingsChannel == "latest" && updaterChannel == "beta" ) ) {
2024-09-19 20:04:18 +02:00
console . log ( "Update channel setting does not exist, setting to value from updater config." ) ;
RpcApi . SetConfigCommand ( ElectronWshClient , { "autoupdate:channel" : updaterChannel } ) ;
retVal = updaterChannel ;
}
console . log ( "Update channel:" , retVal ) ;
return retVal ;
}
2024-08-06 20:05:26 +02:00
export class Updater {
2024-08-08 20:55:44 +02:00
autoCheckInterval : NodeJS.Timeout | null ;
intervalms : number ;
autoCheckEnabled : boolean ;
2024-08-06 20:05:26 +02:00
availableUpdateReleaseName : string | null ;
availableUpdateReleaseNotes : string | null ;
2024-08-07 00:13:59 +02:00
private _status : UpdaterStatus ;
2024-08-06 20:05:26 +02:00
lastUpdateCheck : Date ;
2024-09-04 04:16:04 +02:00
constructor ( settings : SettingsType ) {
this . intervalms = settings [ "autoupdate:intervalms" ] ;
2024-09-19 20:04:18 +02:00
console . log ( "Update check interval in milliseconds:" , this . intervalms ) ;
2024-09-04 04:16:04 +02:00
this . autoCheckEnabled = settings [ "autoupdate:enabled" ] ;
2024-09-19 20:04:18 +02:00
console . log ( "Update check enabled:" , this . autoCheckEnabled ) ;
2024-08-08 20:55:44 +02:00
this . _status = "up-to-date" ;
this . lastUpdateCheck = new Date ( 0 ) ;
this . autoCheckInterval = null ;
this . availableUpdateReleaseName = null ;
2024-09-04 04:16:04 +02:00
autoUpdater . autoInstallOnAppQuit = settings [ "autoupdate:installonquit" ] ;
2024-09-19 20:04:18 +02:00
console . log ( "Install update on quit:" , settings [ "autoupdate:installonquit" ] ) ;
2024-08-08 20:55:44 +02:00
2024-09-18 23:25:52 +02:00
// Only update the release channel if it's specified, otherwise use the one configured in the updater.
2024-09-19 20:04:18 +02:00
autoUpdater . channel = getUpdateChannel ( settings ) ;
Add release channels (#385)
## New release flow
1. Run "Bump Version" workflow with the desired version bump and the
prerelease flag set to `true`. This will push a new version bump to the
target branch and create a new git tag.
- See below for more info on how the version bumping works.
2. A new "Build Helper" workflow run will kick off automatically for the
new tag. Once it is complete, test the new build locally by downloading
with the [download
script](https://github.com/wavetermdev/thenextwave/blob/main/scripts/artifacts/download-staged-artifact.sh).
3. Release the new build using the [publish
script](https://github.com/wavetermdev/thenextwave/blob/main/scripts/artifacts/publish-from-staging.sh).
This will trigger electron-updater to distribute the package to beta
users.
4. Run "Bump Version" again with a release bump (either `major`,
`minor`, or `patch`) and the prerelease flag set to `false`.
6. Release the new build to all channels using the [publish
script](https://github.com/wavetermdev/thenextwave/blob/main/scripts/artifacts/publish-from-staging.sh).
This will trigger electron-updater to distribute the package to all
users.
## Change Summary
Creates a new "Bump Version" workflow to manage versioning and tag
creation.
Build Helper is now automated.
### Version bumps
Updates the `version.cjs` script so that an argument can be passed to
trigger a version bump. Under the hood, this utilizes NPM's `semver`
package.
If arguments are present, the version will be bumped.
If only a single argument is given, the following are valid inputs:
- `none`: No-op.
- `patch`: Bumps the patch version.
- `minor`: Bumps the minor version.
- `major`: Bumps the major version.
- '1', 'true': Bumps the prerelease version.
If two arguments are given, the first argument must be either `none`,
`patch`, `minor`, or `major`. The second argument must be `1` or `true`
to bump the prerelease version.
### electron-builder
We are now using the release channels support in electron-builder. This
will automatically detect the channel being built based on the package
version to determine which channel update files need to be generated.
See
[here](https://www.electron.build/tutorials/release-using-channels.html)
for more information.
### Github Actions
#### Bump Version
This adds a new "Bump Version" workflow for managing versioning and
queuing new builds. When run, this workflow will bump the version,
create a new tag, and push the changes to the target branch. There is a
new dropdown when queuing the "Bump Version" workflow to select what
kind of version bump to perform. A bump must always be performed when
running a new build to ensure consistency.
I had to create a GitHub App to grant write permissions to our main
branch for the version bump commits. I've made a separate workflow file
to manage the version bump commits, which should help prevent tampering.
Thanks to using the GitHub API directly, I am able to make these commits
signed!
#### Build Helper
Build Helper is now triggered when new tags are created, rather than
being triggered automatically. This ensures we're always creating
artifacts from known checkpoints.
### Settings
Adds a new `autoupdate:channel` configuration to the settings file. If
unset, the default from the artifact will be used (should correspond to
the channel of the artifact when downloaded).
## Future Work
I want to add a release workflow that will automatically copy over the
corresponding version artifacts to the release bucket when a new GitHub
Release is created.
I also want to separate versions into separate subdirectories in the
release bucket so we can clean them up more-easily.
---------
Co-authored-by: wave-builder <builds@commandline.dev>
Co-authored-by: wave-builder[bot] <181805596+wave-builder[bot]@users.noreply.github.com>
2024-09-17 22:10:35 +02:00
2024-08-06 20:05:26 +02:00
autoUpdater . removeAllListeners ( ) ;
autoUpdater . on ( "error" , ( err ) = > {
console . log ( "updater error" ) ;
console . log ( err ) ;
2024-10-30 22:24:37 +01:00
if ( ! err . toString ( ) ? . includes ( "net::ERR_INTERNET_DISCONNECTED" ) ) this . status = "error" ;
2024-08-06 20:05:26 +02:00
} ) ;
autoUpdater . on ( "checking-for-update" , ( ) = > {
console . log ( "checking-for-update" ) ;
this . status = "checking" ;
} ) ;
autoUpdater . on ( "update-available" , ( ) = > {
console . log ( "update-available; downloading..." ) ;
2024-09-18 23:30:45 +02:00
this . status = "downloading" ;
2024-08-06 20:05:26 +02:00
} ) ;
autoUpdater . on ( "update-not-available" , ( ) = > {
console . log ( "update-not-available" ) ;
2024-09-18 23:30:45 +02:00
this . status = "up-to-date" ;
2024-08-06 20:05:26 +02:00
} ) ;
autoUpdater . on ( "update-downloaded" , ( event ) = > {
console . log ( "update-downloaded" , [ event ] ) ;
this . availableUpdateReleaseName = event . releaseName ;
this . availableUpdateReleaseNotes = event . releaseNotes as string | null ;
// Display the update banner and create a system notification
this . status = "ready" ;
2024-09-18 21:06:34 +02:00
const updateNotification = new Notification ( {
2024-08-06 20:05:26 +02:00
title : "Wave Terminal" ,
body : "A new version of Wave Terminal is ready to install." ,
} ) ;
updateNotification . on ( "click" , ( ) = > {
2024-12-06 03:09:54 +01:00
fireAndForget ( this . promptToInstallUpdate . bind ( this ) ) ;
2024-08-06 20:05:26 +02:00
} ) ;
updateNotification . show ( ) ;
} ) ;
}
2024-08-08 20:55:44 +02:00
/ * *
* The status of the Updater .
* /
2024-08-06 20:05:26 +02:00
get status ( ) : UpdaterStatus {
return this . _status ;
}
private set status ( value : UpdaterStatus ) {
this . _status = value ;
2024-10-17 23:34:02 +02:00
getAllWaveWindows ( ) . forEach ( ( window ) = > {
2024-12-06 03:26:20 +01:00
const allTabs = Array . from ( window . allLoadedTabViews . values ( ) ) ;
2024-10-17 23:34:02 +02:00
allTabs . forEach ( ( tab ) = > {
tab . webContents . send ( "app-update-status" , value ) ;
} ) ;
2024-08-06 20:05:26 +02:00
} ) ;
}
/ * *
2024-08-08 20:55:44 +02:00
* Check for updates and start the background update check , if configured .
2024-08-06 20:05:26 +02:00
* /
2024-08-08 20:55:44 +02:00
async start() {
if ( this . autoCheckEnabled ) {
console . log ( "starting updater" ) ;
this . autoCheckInterval = setInterval ( ( ) = > {
fireAndForget ( ( ) = > this . checkForUpdates ( false ) ) ;
} , 600000 ) ; // intervals are unreliable when an app is suspended so we will check every 10 mins if the interval has passed.
await this . checkForUpdates ( false ) ;
}
}
/ * *
* Stop the background update check , if configured .
* /
stop() {
console . log ( "stopping updater" ) ;
if ( this . autoCheckInterval ) {
clearInterval ( this . autoCheckInterval ) ;
this . autoCheckInterval = null ;
}
2024-08-06 20:05:26 +02:00
}
/ * *
2024-08-07 01:09:24 +02:00
* Checks if the configured interval time has passed since the last update check , and if so , checks for updates using the ` autoUpdater ` object
2024-08-06 20:05:26 +02:00
* @param userInput Whether the user is requesting this . If so , an alert will report the result of the check .
* /
async checkForUpdates ( userInput : boolean ) {
const now = new Date ( ) ;
2024-08-07 01:08:49 +02:00
// Run an update check always if the user requests it, otherwise only if there's an active update check interval and enough time has elapsed.
2024-08-06 20:05:26 +02:00
if (
2024-08-07 00:13:59 +02:00
userInput ||
2024-08-08 20:55:44 +02:00
( this . autoCheckInterval &&
( ! this . lastUpdateCheck || Math . abs ( now . getTime ( ) - this . lastUpdateCheck . getTime ( ) ) > this . intervalms ) )
2024-08-06 20:05:26 +02:00
) {
2024-08-06 23:51:06 +02:00
const result = await autoUpdater . checkForUpdates ( ) ;
2024-08-07 01:08:49 +02:00
// If the user requested this check and we do not have an available update, let them know with a popup dialog. No need to tell them if there is an update, because we show a banner once the update is ready to install.
2024-08-06 23:51:06 +02:00
if ( userInput && ! result . downloadPromise ) {
const dialogOpts : Electron.MessageBoxOptions = {
type : "info" ,
message : "There are currently no updates available." ,
} ;
2024-12-02 19:56:56 +01:00
dialog . showMessageBox ( focusedWaveWindow , dialogOpts ) ;
2024-08-06 23:51:06 +02:00
}
2024-08-07 01:08:49 +02:00
// Only update the last check time if this is an automatic check. This ensures the interval remains consistent.
2024-08-07 00:36:21 +02:00
if ( ! userInput ) this . lastUpdateCheck = now ;
2024-08-06 20:05:26 +02:00
}
}
/ * *
* Prompts the user to install the downloaded application update and restarts the application
* /
2024-08-07 01:19:55 +02:00
async promptToInstallUpdate() {
2024-08-06 20:05:26 +02:00
const dialogOpts : Electron.MessageBoxOptions = {
type : "info" ,
buttons : [ "Restart" , "Later" ] ,
title : "Application Update" ,
message : process.platform === "win32" ? this . availableUpdateReleaseNotes : this.availableUpdateReleaseName ,
detail : "A new version has been downloaded. Restart the application to apply the updates." ,
} ;
2024-10-17 23:34:02 +02:00
const allWindows = getAllWaveWindows ( ) ;
2024-08-06 20:05:26 +02:00
if ( allWindows . length > 0 ) {
2024-12-02 19:56:56 +01:00
await dialog . showMessageBox ( focusedWaveWindow ? ? allWindows [ 0 ] , dialogOpts ) . then ( ( { response } ) = > {
2024-10-17 23:34:02 +02:00
if ( response === 0 ) {
2024-12-06 03:09:54 +01:00
fireAndForget ( this . installUpdate . bind ( this ) ) ;
2024-10-17 23:34:02 +02:00
}
} ) ;
2024-08-06 20:05:26 +02:00
}
}
2024-08-07 01:19:55 +02:00
/ * *
* Restarts the app and installs an update if it is available .
* /
2024-10-30 22:20:19 +01:00
async installUpdate() {
2024-08-07 01:19:55 +02:00
if ( this . status == "ready" ) {
this . status = "installing" ;
2024-10-30 22:20:19 +01:00
await delay ( 1000 ) ;
2024-08-07 01:19:55 +02:00
autoUpdater . quitAndInstall ( ) ;
}
}
2024-08-06 20:05:26 +02:00
}
2024-11-01 18:20:15 +01:00
export function getResolvedUpdateChannel ( ) : string {
return isDev ( ) ? "dev" : ( autoUpdater . channel ? ? "latest" ) ;
}
2024-12-06 03:09:54 +01:00
ipcMain . on ( "install-app-update" , ( ) = > fireAndForget ( updater ? . promptToInstallUpdate . bind ( updater ) ) ) ;
2024-09-18 21:06:34 +02:00
ipcMain . on ( "get-app-update-status" , ( event ) = > {
2024-08-06 20:05:26 +02:00
event . returnValue = updater ? . status ;
} ) ;
2024-09-18 23:25:52 +02:00
ipcMain . on ( "get-updater-channel" , ( event ) = > {
2024-11-01 18:20:15 +01:00
event . returnValue = getResolvedUpdateChannel ( ) ;
2024-09-18 23:25:52 +02:00
} ) ;
2024-08-06 20:05:26 +02:00
2024-08-07 00:13:59 +02:00
let autoUpdateLock = false ;
2024-08-06 20:05:26 +02:00
/ * *
* Configures the auto - updater based on the user ' s preference
* /
export async function configureAutoUpdater() {
2024-08-06 23:25:30 +02:00
if ( isDev ( ) ) {
console . log ( "skipping auto-updater in dev mode" ) ;
2024-08-07 00:13:59 +02:00
return ;
2024-08-06 23:25:30 +02:00
}
2024-08-06 20:05:26 +02:00
// simple lock to prevent multiple auto-update configuration attempts, this should be very rare
if ( autoUpdateLock ) {
console . log ( "auto-update configuration already in progress, skipping" ) ;
return ;
}
autoUpdateLock = true ;
try {
console . log ( "Configuring updater" ) ;
2024-09-18 21:06:34 +02:00
const settings = ( await FileService . GetFullConfig ( ) ) . settings ;
2024-09-04 04:16:04 +02:00
updater = new Updater ( settings ) ;
2024-08-08 20:55:44 +02:00
await updater . start ( ) ;
2024-08-06 20:05:26 +02:00
} catch ( e ) {
console . warn ( "error configuring updater" , e . toString ( ) ) ;
}
autoUpdateLock = false ;
}