mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
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:
@@ -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
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 };
|
||||
|
||||
Vendored
+5
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user