Enable automatic updates on macOS using Electron's autoUpdater (#342)

* clean up emain

* add restart on disable auto update

* save work

* add zip

* save

* fix zip command

* make build-universal more generic

* clean up script

* fix autoupdate config

* update feed

* fix update feed path

* switch to custom update flow

* show notification

* remove enum

* test change

* debug sidebar

* save work

* remove weird import

* remove listeners if present

* save debug

* fixed banner

* fix sending of appupdatestatus, add comments

* Change to primary feed

* more comments, less debugs

* rename to app update

* code cleanup, add fireAndForget

* update paths for objects
This commit is contained in:
Evan Simkowitz
2024-02-27 19:42:29 -08:00
committed by GitHub
parent bdfb80e62f
commit 98385b1e0d
8 changed files with 415 additions and 127 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# This script is used to upload signed and notarized releases to S3 and update the Electron auto-update release feeds.
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
BUILDS_DIR=$SCRIPT_DIR/builds
TEMP2_DIR=$SCRIPT_DIR/temp2
MAIN_RELEASE_PATH="dl.waveterm.dev/build"
AUTOUPDATE_RELEASE_PATH="dl.waveterm.dev/autoupdate"
# Copy the builds to the temp2 directory
echo "Copying builds to temp2"
rm -rf $TEMP2_DIR
mkdir -p $TEMP2_DIR
cp -r $BUILDS_DIR/* $TEMP2_DIR
UVERSION=$(cat $TEMP2_DIR/version.txt)
if [ -z "$UVERSION" ]; then
echo "version.txt is empty"
exit 1
fi
# Find the DMG file
echo "Finding DMG"
DMG=$(find $TEMP2_DIR -type f -iname "*.dmg")
# Ensure there is only one
NUM_DMGS=$(echo $DMG | wc -l)
if [ "0" -eq "$NUM_DMGS" ]; then
echo "no DMG found in $TEMP2_DIR"
exit 1
elif [ "1" -lt "$NUM_DMGS" ]; then
echo "multiple DMGs found in $TEMP2_DIR"
exit 1
fi
# Find the Mac zip
echo "Finding Mac zip"
MAC_ZIP=$(find $TEMP2_DIR -type f -iname "*mac*.zip")
# Ensure there is only one
NUM_MAC_ZIPS=$(echo $MAC_ZIP | wc -l)
if [ "0" -eq "$NUM_MAC_ZIPS" ]; then
echo "no Mac zip found in $TEMP2_DIR"
exit 1
elif [ "1" -lt "$NUM_MAC_ZIPS" ]; then
echo "multiple Mac zips found in $TEMP2_DIR"
exit 1
fi
# Find the Linux zips
echo "Finding Linux zips"
LINUX_ZIPS=$(find $TEMP2_DIR -type f -iname "*linux*.zip")
# Ensure there is at least one
NUM_LINUX_ZIPS=$(echo $LINUX_ZIPS | wc -l)
if [ "0" -eq "$NUM_LINUX_ZIPS" ]; then
echo "no Linux zips found in $TEMP2_DIR"
exit 1
fi
# Upload the DMG
echo "Uploading DMG"
DMG_NAME=$(basename $DMG)
aws s3 cp $DMG s3:/$MAIN_RELEASE_PATH/$DMG_NAME
# Upload the Linux zips
echo "Uploading Linux zips"
for LINUX_ZIP in $LINUX_ZIPS; do
LINUX_ZIP_NAME=$(basename $LINUX_ZIP)
aws s3 cp $LINUX_ZIP s3://$MAIN_RELEASE_PATH/$LINUX_ZIP_NAME
done
# Upload the autoupdate Mac zip
echo "Uploading Mac zip"
MAC_ZIP_NAME=$(basename $MAC_ZIP)
aws s3 cp $MAC_ZIP s3://$AUTOUPDATE_RELEASE_PATH/$MAC_ZIP_NAME
# Update the autoupdate feeds
echo "Updating autoupdate feeds"
RELEASES_CONTENTS="{\"name\":\"$UVERSION\",\"notes\":\"\",\"url\":\"https://$AUTOUPDATE_RELEASE_PATH/$MAC_ZIP_NAME\"}"
aws s3 cp - s3://$AUTOUPDATE_RELEASE_PATH/darwin/arm64/RELEASES.json <<< $RELEASES_CONTENTS
aws s3 cp - s3://$AUTOUPDATE_RELEASE_PATH/darwin/x64/RELEASES.json <<< $RELEASES_CONTENTS
# Clean up
echo "Cleaning up"
rm -rf $TEMP2_DIR
+2 -1
View File
@@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
import { commandRtnHandler, isBlank } from "@/util/util";
import * as appconst from "@/app/appconst";
@@ -64,6 +64,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
}
commandRtnHandler(prtn, this.errorMessage);
getApi().changeAutoUpdate(val);
}
getFontSizes(): DropdownItem[] {
+39 -14
View File
@@ -169,6 +169,44 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
GlobalModel.modalsModel.pushModal(appconst.SESSION_SETTINGS);
}
/**
* Get the update banner for the app, if we need to show it.
* @returns Either a banner to install the ready update, a link to the download page, or null if no update is available.
*/
@boundMethod
getUpdateAppBanner(): React.ReactNode {
if (GlobalModel.platform == "darwin") {
const status = GlobalModel.appUpdateStatus.get();
if (status == "ready") {
return (
<SideBarItem
key="update-ready"
className="update-banner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Click to Install Update"
onClick={() => GlobalModel.installAppUpdate()}
/>
);
}
} else {
const clientData = this.props.clientData;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
if (compareLoose(appconst.VERSION, clientData.releaseinfo.latestversion) < 0) {
return (
<SideBarItem
key="update-available"
className="update-banner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
);
}
}
}
return null;
}
getSessions() {
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
const sessionList: Session[] = [];
@@ -227,11 +265,6 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
}
render() {
const clientData = this.props.clientData;
let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(appconst.VERSION, clientData.releaseinfo.latestversion) < 0;
}
const mainSidebar = GlobalModel.mainSidebarModel;
const isCollapsed = mainSidebar.getCollapsed();
return (
@@ -291,15 +324,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
{this.getSessions()}
</div>
<div className="bottom" id="sidebar-bottom">
<If condition={needsUpdate}>
<SideBarItem
key="update-available"
className="update-banner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
</If>
{this.getUpdateAppBanner()}
<If condition={GlobalModel.isDev}>
<SideBarItem
key="apps"
+244 -106
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -15,6 +15,10 @@ contextBridge.exposeInMainWorld("api", {
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),
openExternalLink: (url) => ipcRenderer.send("open-external-link", url),
changeAutoUpdate: (enabled) => ipcRenderer.send("change-auto-update", enabled),
installAppUpdate: () => ipcRenderer.send("install-app-update"),
getAppUpdateStatus: () => ipcRenderer.sendSync("get-app-update-status"),
onAppUpdateStatus: (callback) => ipcRenderer.on("app-update-status", (_, val) => callback(val)),
onTCmd: (callback) => ipcRenderer.on("t-cmd", callback),
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
onLCmd: (callback) => ipcRenderer.on("l-cmd", callback),
+24 -6
View File
@@ -125,6 +125,10 @@ class Model {
name: "renderVersion",
});
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
name: "appUpdateStatus",
});
private constructor() {
this.clientId = getApi().getId();
this.isDev = getApi().getIsDev();
@@ -178,6 +182,7 @@ class Model {
getApi().onBracketCmd(this.onBracketCmd.bind(this));
getApi().onDigitCmd(this.onDigitCmd.bind(this));
getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this));
getApi().onAppUpdateStatus(this.onAppUpdateStatus.bind(this));
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this));
setTimeout(() => this.getClientDataLoop(1), 10);
@@ -756,8 +761,7 @@ class Model {
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
this.inputModel.resetInput();
if (genUpdate.type == "model") {
const modelUpdate = genUpdate as ModelUpdatePacket;
const reversedGenUpdate = modelUpdate.data.slice().reverse();
const reversedGenUpdate = genUpdate.data.slice().reverse();
const lastCmdLine = reversedGenUpdate.find((update) => "cmdline" in update);
if (lastCmdLine) {
// TODO a bit of a hack since this update gets applied in runUpdate_internal.
@@ -819,7 +823,7 @@ class Model {
runUpdate_internal(genUpdate: UpdatePacket, uiContext: UIContextType, interactive: boolean) {
if (genUpdate.type == "pty") {
const ptyMsg = genUpdate.data as PtyDataUpdateType;
const ptyMsg = genUpdate.data;
if (isBlank(ptyMsg.remoteid)) {
// regular update
this.updatePtyData(ptyMsg);
@@ -829,7 +833,7 @@ class Model {
this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
}
} else if (genUpdate.type == "model") {
const modelUpdateItems = genUpdate.data as ModelUpdateItemType[];
const modelUpdateItems = genUpdate.data;
let showedRemotesModal = false;
const [oldActiveSessionId, oldActiveScreenId] = this.getActiveIds();
@@ -1100,8 +1104,7 @@ class Model {
isInfoUpdate(update: UpdatePacket): boolean {
if (update.type == "model") {
const modelUpdate = update as ModelUpdatePacket;
return modelUpdate.data.some((u) => u.info != null || u.history != null);
return update.data.some((u) => u.info != null || u.history != null);
} else {
return false;
}
@@ -1506,6 +1509,21 @@ class Model {
const resp = await prtn;
const _ = await handleJsonFetchResponse(url, resp);
}
/**
* Tell Electron to install the waiting app update. Will prompt for user input before restarting.
*/
installAppUpdate(): void {
if (this.appUpdateStatus.get() == "ready") {
getApi().installAppUpdate();
}
}
onAppUpdateStatus(status: AppUpdateStatusType) {
mobx.action(() => {
this.appUpdateStatus.set(status);
})();
}
}
export { Model, getApi };
+5
View File
@@ -11,6 +11,7 @@ declare global {
type HistoryTypeStrs = "global" | "session" | "screen";
type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error";
type LineContainerStrs = "main" | "sidebar" | "history";
type AppUpdateStatusType = "unavailable" | "ready";
type OV<V> = mobx.IObservableValue<V>;
type OArr<V> = mobx.IObservableArray<V>;
@@ -869,6 +870,10 @@ declare global {
reloadWindow: () => void;
openExternalLink: (url: string) => void;
reregisterGlobalShortcut: (shortcut: string) => void;
changeAutoUpdate: (enabled: boolean) => void;
installAppUpdate: () => void;
getAppUpdateStatus: () => AppUpdateStatusType;
onAppUpdateStatus: (callback: (status: AppUpdateStatusType) => void) => void;
onTCmd: (callback: (mods: KeyModsType) => void) => void;
onICmd: (callback: (mods: KeyModsType) => void) => void;
onLCmd: (callback: (mods: KeyModsType) => void) => void;
+11
View File
@@ -363,6 +363,16 @@ function ces(s: string) {
return s;
}
/**
* A wrapper function for running a promise and catching any errors
* @param f The promise to run
*/
function fireAndForget(f: () => Promise<void>) {
f().catch((e) => {
console.log("fireAndForget error", e);
});
}
export {
handleJsonFetchResponse,
base64ToString,
@@ -389,4 +399,5 @@ export {
getRemoteConnVal,
getRemoteName,
ces,
fireAndForget,
};