mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01: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:
parent
bdfb80e62f
commit
98385b1e0d
86
buildres/upload-release.sh
Normal file
86
buildres/upload-release.sh
Normal 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
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
|||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import cn from "classnames";
|
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 { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
|
||||||
import { commandRtnHandler, isBlank } from "@/util/util";
|
import { commandRtnHandler, isBlank } from "@/util/util";
|
||||||
import * as appconst from "@/app/appconst";
|
import * as appconst from "@/app/appconst";
|
||||||
@ -64,6 +64,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
|||||||
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
|
prtn = GlobalCommandRunner.releaseCheckAutoOff(false);
|
||||||
}
|
}
|
||||||
commandRtnHandler(prtn, this.errorMessage);
|
commandRtnHandler(prtn, this.errorMessage);
|
||||||
|
getApi().changeAutoUpdate(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFontSizes(): DropdownItem[] {
|
getFontSizes(): DropdownItem[] {
|
||||||
|
@ -169,6 +169,44 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
|||||||
GlobalModel.modalsModel.pushModal(appconst.SESSION_SETTINGS);
|
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() {
|
getSessions() {
|
||||||
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
|
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
|
||||||
const sessionList: Session[] = [];
|
const sessionList: Session[] = [];
|
||||||
@ -227,11 +265,6 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 mainSidebar = GlobalModel.mainSidebarModel;
|
||||||
const isCollapsed = mainSidebar.getCollapsed();
|
const isCollapsed = mainSidebar.getCollapsed();
|
||||||
return (
|
return (
|
||||||
@ -291,15 +324,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
|||||||
{this.getSessions()}
|
{this.getSessions()}
|
||||||
</div>
|
</div>
|
||||||
<div className="bottom" id="sidebar-bottom">
|
<div className="bottom" id="sidebar-bottom">
|
||||||
<If condition={needsUpdate}>
|
{this.getUpdateAppBanner()}
|
||||||
<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>
|
|
||||||
<If condition={GlobalModel.isDev}>
|
<If condition={GlobalModel.isDev}>
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
key="apps"
|
key="apps"
|
||||||
|
@ -11,7 +11,7 @@ import * as winston from "winston";
|
|||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as waveutil from "../util/util";
|
import * as waveutil from "../util/util";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { handleJsonFetchResponse } from "@/util/util";
|
import { handleJsonFetchResponse, fireAndForget } from "@/util/util";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
|
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
|
||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
@ -22,12 +22,13 @@ const AuthKeyFile = "waveterm.authkey";
|
|||||||
const DevServerEndpoint = "http://127.0.0.1:8090";
|
const DevServerEndpoint = "http://127.0.0.1:8090";
|
||||||
const ProdServerEndpoint = "http://127.0.0.1:1619";
|
const ProdServerEndpoint = "http://127.0.0.1:1619";
|
||||||
|
|
||||||
let isDev = process.env[WaveDevVarName] != null;
|
const isDev = process.env[WaveDevVarName] != null;
|
||||||
let waveHome = getWaveHomeDir();
|
const waveHome = getWaveHomeDir();
|
||||||
let DistDir = isDev ? "dist-dev" : "dist";
|
const DistDir = isDev ? "dist-dev" : "dist";
|
||||||
|
const instanceId = uuidv4();
|
||||||
|
const oldConsoleLog = console.log;
|
||||||
|
|
||||||
let GlobalAuthKey = "";
|
let GlobalAuthKey = "";
|
||||||
let instanceId = uuidv4();
|
|
||||||
let oldConsoleLog = console.log;
|
|
||||||
let wasActive = true;
|
let wasActive = true;
|
||||||
let wasInFg = true;
|
let wasInFg = true;
|
||||||
let currentGlobalShortcut: string | null = null;
|
let currentGlobalShortcut: string | null = null;
|
||||||
@ -38,18 +39,18 @@ ensureDir(waveHome);
|
|||||||
|
|
||||||
// these are either "darwin/amd64" or "darwin/arm64"
|
// these are either "darwin/amd64" or "darwin/arm64"
|
||||||
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
|
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
|
||||||
let unamePlatform = process.platform;
|
const unamePlatform = process.platform;
|
||||||
let unameArch: string = process.arch;
|
let unameArch: string = process.arch;
|
||||||
if (unameArch == "x64") {
|
if (unameArch == "x64") {
|
||||||
unameArch = "amd64";
|
unameArch = "amd64";
|
||||||
}
|
}
|
||||||
let loggerTransports: winston.transport[] = [
|
const loggerTransports: winston.transport[] = [
|
||||||
new winston.transports.File({ filename: path.join(waveHome, "waveterm-app.log"), level: "info" }),
|
new winston.transports.File({ filename: path.join(waveHome, "waveterm-app.log"), level: "info" }),
|
||||||
];
|
];
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
loggerTransports.push(new winston.transports.Console());
|
loggerTransports.push(new winston.transports.Console());
|
||||||
}
|
}
|
||||||
let loggerConfig = {
|
const loggerConfig = {
|
||||||
level: "info",
|
level: "info",
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
@ -57,8 +58,8 @@ let loggerConfig = {
|
|||||||
),
|
),
|
||||||
transports: loggerTransports,
|
transports: loggerTransports,
|
||||||
};
|
};
|
||||||
let logger = winston.createLogger(loggerConfig);
|
const logger = winston.createLogger(loggerConfig);
|
||||||
function log(...msg) {
|
function log(...msg: any[]) {
|
||||||
try {
|
try {
|
||||||
logger.info(util.format(...msg));
|
logger.info(util.format(...msg));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -78,7 +79,7 @@ console.log(
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
console.log("waveterm-app WAVETERM_DEV set");
|
console.log("waveterm-app WAVETERM_DEV set");
|
||||||
}
|
}
|
||||||
let app = electron.app;
|
const app = electron.app;
|
||||||
app.setName(isDev ? "Wave (Dev)" : "Wave");
|
app.setName(isDev ? "Wave (Dev)" : "Wave");
|
||||||
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||||
let waveSrvShouldRestart = false;
|
let waveSrvShouldRestart = false;
|
||||||
@ -101,7 +102,7 @@ function getWaveHomeDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkPromptMigrate() {
|
function checkPromptMigrate() {
|
||||||
let waveHome = getWaveHomeDir();
|
const waveHome = getWaveHomeDir();
|
||||||
if (isDev || fs.existsSync(waveHome)) {
|
if (isDev || fs.existsSync(waveHome)) {
|
||||||
// don't migrate if we're running dev version or if wave home directory already exists
|
// don't migrate if we're running dev version or if wave home directory already exists
|
||||||
return;
|
return;
|
||||||
@ -109,8 +110,8 @@ function checkPromptMigrate() {
|
|||||||
if (process.env.HOME == null) {
|
if (process.env.HOME == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let homeDir: string = process.env.HOME;
|
const homeDir: string = process.env.HOME;
|
||||||
let promptHome: string = path.join(homeDir, "prompt");
|
const promptHome: string = path.join(homeDir, "prompt");
|
||||||
if (!fs.existsSync(promptHome) || !fs.existsSync(path.join(promptHome, "prompt.db"))) {
|
if (!fs.existsSync(promptHome) || !fs.existsSync(path.join(promptHome, "prompt.db"))) {
|
||||||
// make sure we have a valid prompt home directory (prompt.db must exist inside)
|
// make sure we have a valid prompt home directory (prompt.db must exist inside)
|
||||||
return;
|
return;
|
||||||
@ -147,39 +148,38 @@ function getWaveSrvPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWaveSrvCmd() {
|
function getWaveSrvCmd() {
|
||||||
let waveSrvPath = getWaveSrvPath();
|
const waveSrvPath = getWaveSrvPath();
|
||||||
let waveHome = getWaveHomeDir();
|
const waveHome = getWaveHomeDir();
|
||||||
let logFile = path.join(waveHome, "wavesrv.log");
|
const logFile = path.join(waveHome, "wavesrv.log");
|
||||||
return `"${waveSrvPath}" >> "${logFile}" 2>&1`;
|
return `"${waveSrvPath}" >> "${logFile}" 2>&1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaveSrvCwd() {
|
function getWaveSrvCwd() {
|
||||||
let waveHome = getWaveHomeDir();
|
return getWaveHomeDir();
|
||||||
return waveHome;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDir(dir) {
|
function ensureDir(dir: fs.PathLike) {
|
||||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function readAuthKey() {
|
function readAuthKey() {
|
||||||
let homeDir = getWaveHomeDir();
|
const homeDir = getWaveHomeDir();
|
||||||
let authKeyFileName = path.join(homeDir, AuthKeyFile);
|
const authKeyFileName = path.join(homeDir, AuthKeyFile);
|
||||||
if (!fs.existsSync(authKeyFileName)) {
|
if (!fs.existsSync(authKeyFileName)) {
|
||||||
let authKeyStr = String(uuidv4());
|
const authKeyStr = String(uuidv4());
|
||||||
fs.writeFileSync(authKeyFileName, authKeyStr, { mode: 0o600 });
|
fs.writeFileSync(authKeyFileName, authKeyStr, { mode: 0o600 });
|
||||||
return authKeyStr;
|
return authKeyStr;
|
||||||
}
|
}
|
||||||
let authKeyData = fs.readFileSync(authKeyFileName);
|
const authKeyData = fs.readFileSync(authKeyFileName);
|
||||||
let authKeyStr = String(authKeyData);
|
const authKeyStr = String(authKeyData);
|
||||||
if (authKeyStr == null || authKeyStr == "") {
|
if (authKeyStr == null || authKeyStr == "") {
|
||||||
throw new Error("cannot read authkey");
|
throw new Error("cannot read authkey");
|
||||||
}
|
}
|
||||||
return authKeyStr.trim();
|
return authKeyStr.trim();
|
||||||
}
|
}
|
||||||
const reloadAcceleratorKey = unamePlatform == "darwin" ? "Option+R" : "Super+R";
|
const reloadAcceleratorKey = unamePlatform == "darwin" ? "Option+R" : "Super+R";
|
||||||
let cmdOrAlt = process.platform === "darwin" ? "Cmd" : "Alt";
|
const cmdOrAlt = process.platform === "darwin" ? "Cmd" : "Alt";
|
||||||
let menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
role: "appMenu",
|
role: "appMenu",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -254,7 +254,7 @@ let menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let menu = electron.Menu.buildFromTemplate(menuTemplate);
|
const menu = electron.Menu.buildFromTemplate(menuTemplate);
|
||||||
electron.Menu.setApplicationMenu(menu);
|
electron.Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
let MainWindow: Electron.BrowserWindow | null = null;
|
let MainWindow: Electron.BrowserWindow | null = null;
|
||||||
@ -274,12 +274,12 @@ function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEven
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
||||||
if (!event.frame || event.frame.parent == null) {
|
if (!event.frame?.parent) {
|
||||||
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let url = event.url;
|
const url = event.url;
|
||||||
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
||||||
if (event.frame.name == "webview") {
|
if (event.frame.name == "webview") {
|
||||||
// "webview" links always open in new window
|
// "webview" links always open in new window
|
||||||
@ -289,13 +289,12 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("frame navigation canceled");
|
console.log("frame navigation canceled");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow(clientData: ClientDataType | null) {
|
function createMainWindow(clientData: ClientDataType | null) {
|
||||||
let bounds = calcBounds(clientData);
|
const bounds = calcBounds(clientData);
|
||||||
setKeyUtilPlatform(platform());
|
setKeyUtilPlatform(platform());
|
||||||
let win = new electron.BrowserWindow({
|
const win = new electron.BrowserWindow({
|
||||||
x: bounds.x,
|
x: bounds.x,
|
||||||
y: bounds.y,
|
y: bounds.y,
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
@ -309,17 +308,17 @@ function createMainWindow(clientData: ClientDataType | null) {
|
|||||||
preload: path.join(getAppBasePath(), DistDir, "preload.js"),
|
preload: path.join(getAppBasePath(), DistDir, "preload.js"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let indexHtml = isDev ? "index-dev.html" : "index.html";
|
const indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||||
win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
|
win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
|
||||||
win.webContents.on("before-input-event", (e, input) => {
|
win.webContents.on("before-input-event", (e, input) => {
|
||||||
let waveEvent = adaptFromElectronKeyEvent(input);
|
const waveEvent = adaptFromElectronKeyEvent(input);
|
||||||
if (win.isFocused()) {
|
if (win.isFocused()) {
|
||||||
wasActive = true;
|
wasActive = true;
|
||||||
}
|
}
|
||||||
if (input.type != "keyDown") {
|
if (input.type != "keyDown") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mods = getMods(input);
|
const mods = getMods(input);
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:t")) {
|
if (checkKeyPressed(waveEvent, "Cmd:t")) {
|
||||||
win.webContents.send("t-cmd", mods);
|
win.webContents.send("t-cmd", mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -378,7 +377,7 @@ function createMainWindow(clientData: ClientDataType | null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code.startsWith("Digit") && input.meta) {
|
if (input.code.startsWith("Digit") && input.meta) {
|
||||||
let digitNum = parseInt(input.code.substr(5));
|
const digitNum = parseInt(input.code.substring(5));
|
||||||
if (isNaN(digitNum) || digitNum < 1 || digitNum > 9) {
|
if (isNaN(digitNum) || digitNum < 1 || digitNum > 9) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -386,7 +385,7 @@ function createMainWindow(clientData: ClientDataType | null) {
|
|||||||
win.webContents.send("digit-cmd", { digit: digitNum }, mods);
|
win.webContents.send("digit-cmd", { digit: digitNum }, mods);
|
||||||
}
|
}
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:[") || checkKeyPressed(waveEvent, "Cmd:]")) {
|
if (checkKeyPressed(waveEvent, "Cmd:[") || checkKeyPressed(waveEvent, "Cmd:]")) {
|
||||||
let rel = checkKeyPressed(waveEvent, "Cmd:]") ? 1 : -1;
|
const rel = checkKeyPressed(waveEvent, "Cmd:]") ? 1 : -1;
|
||||||
win.webContents.send("bracket-cmd", { relative: rel }, mods);
|
win.webContents.send("bracket-cmd", { relative: rel }, mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@ -420,9 +419,9 @@ function createMainWindow(clientData: ClientDataType | null) {
|
|||||||
console.log("openExternal discord", url);
|
console.log("openExternal discord", url);
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
} else if (url.startsWith("https://extern/?")) {
|
} else if (url.startsWith("https://extern/?")) {
|
||||||
let qmark = url.indexOf("?");
|
const qmark = url.indexOf("?");
|
||||||
let param = url.substr(qmark + 1);
|
const param = url.substring(qmark + 1);
|
||||||
let newUrl = decodeURIComponent(param);
|
const newUrl = decodeURIComponent(param);
|
||||||
console.log("openExternal extern", newUrl);
|
console.log("openExternal extern", newUrl);
|
||||||
electron.shell.openExternal(newUrl);
|
electron.shell.openExternal(newUrl);
|
||||||
} else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
|
} else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
|
||||||
@ -432,18 +431,18 @@ function createMainWindow(clientData: ClientDataType | null) {
|
|||||||
console.log("window-open denied", url);
|
console.log("window-open denied", url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mainResizeHandler(e, win) {
|
function mainResizeHandler(_: any, win: Electron.BrowserWindow) {
|
||||||
if (win == null || win.isDestroyed() || win.fullScreen) {
|
if (win == null || win.isDestroyed() || win.fullScreen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let bounds = win.getBounds();
|
const bounds = win.getBounds();
|
||||||
// console.log("resize/move", win.getBounds());
|
const winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x };
|
||||||
let winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x };
|
const url = new URL(getBaseHostPort() + "/api/set-winsize");
|
||||||
let url = new URL(getBaseHostPort() + "/api/set-winsize");
|
const fetchHeaders = getFetchHeaders();
|
||||||
let fetchHeaders = getFetchHeaders();
|
|
||||||
fetch(url, { method: "post", body: JSON.stringify(winSize), headers: fetchHeaders })
|
fetch(url, { method: "post", body: JSON.stringify(winSize), headers: fetchHeaders })
|
||||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -452,11 +451,11 @@ function mainResizeHandler(e, win) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calcBounds(clientData: ClientDataType) {
|
function calcBounds(clientData: ClientDataType) {
|
||||||
let primaryDisplay = electron.screen.getPrimaryDisplay();
|
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
||||||
let pdBounds = primaryDisplay.bounds;
|
const pdBounds = primaryDisplay.bounds;
|
||||||
let size = { x: 100, y: 100, width: pdBounds.width - 200, height: pdBounds.height - 200 };
|
const size = { x: 100, y: 100, width: pdBounds.width - 200, height: pdBounds.height - 200 };
|
||||||
if (clientData != null && clientData.winsize != null && clientData.winsize.width > 0) {
|
if (clientData?.winsize?.width > 0) {
|
||||||
let cwinSize = clientData.winsize;
|
const cwinSize = clientData.winsize;
|
||||||
if (cwinSize.width > 0) {
|
if (cwinSize.width > 0) {
|
||||||
size.width = cwinSize.width;
|
size.width = cwinSize.width;
|
||||||
}
|
}
|
||||||
@ -497,32 +496,26 @@ app.on("window-all-closed", () => {
|
|||||||
|
|
||||||
electron.ipcMain.on("get-id", (event) => {
|
electron.ipcMain.on("get-id", (event) => {
|
||||||
event.returnValue = instanceId + ":" + event.processId;
|
event.returnValue = instanceId + ":" + event.processId;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-platform", (event) => {
|
electron.ipcMain.on("get-platform", (event) => {
|
||||||
event.returnValue = unamePlatform;
|
event.returnValue = unamePlatform;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-isdev", (event) => {
|
electron.ipcMain.on("get-isdev", (event) => {
|
||||||
event.returnValue = isDev;
|
event.returnValue = isDev;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-authkey", (event) => {
|
electron.ipcMain.on("get-authkey", (event) => {
|
||||||
event.returnValue = GlobalAuthKey;
|
event.returnValue = GlobalAuthKey;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("wavesrv-status", (event) => {
|
electron.ipcMain.on("wavesrv-status", (event) => {
|
||||||
event.returnValue = waveSrvProc != null;
|
event.returnValue = waveSrvProc != null;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-initial-termfontfamily", (event) => {
|
electron.ipcMain.on("get-initial-termfontfamily", (event) => {
|
||||||
event.returnValue = initialClientData?.feopts?.termfontfamily;
|
event.returnValue = initialClientData?.feopts?.termfontfamily;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("restart-server", (event) => {
|
electron.ipcMain.on("restart-server", (event) => {
|
||||||
@ -534,7 +527,6 @@ electron.ipcMain.on("restart-server", (event) => {
|
|||||||
runWaveSrv();
|
runWaveSrv();
|
||||||
}
|
}
|
||||||
event.returnValue = true;
|
event.returnValue = true;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("reload-window", (event) => {
|
electron.ipcMain.on("reload-window", (event) => {
|
||||||
@ -542,35 +534,29 @@ electron.ipcMain.on("reload-window", (event) => {
|
|||||||
MainWindow.reload();
|
MainWindow.reload();
|
||||||
}
|
}
|
||||||
event.returnValue = true;
|
event.returnValue = true;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("open-external-link", async (_, url) => {
|
electron.ipcMain.on("open-external-link", (_, url) => fireAndForget(() => electron.shell.openExternal(url)));
|
||||||
try {
|
|
||||||
await electron.shell.openExternal(url);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("error opening external link", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
electron.ipcMain.on("reregister-global-shortcut", (event, shortcut: string) => {
|
electron.ipcMain.on("reregister-global-shortcut", (event, shortcut: string) => {
|
||||||
reregisterGlobalShortcut(shortcut);
|
reregisterGlobalShortcut(shortcut);
|
||||||
event.returnValue = true;
|
event.returnValue = true;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("get-last-logs", async (event, numberOfLines) => {
|
electron.ipcMain.on("get-last-logs", (event, numberOfLines) => {
|
||||||
try {
|
fireAndForget(async () => {
|
||||||
const logPath = path.join(getWaveHomeDir(), "wavesrv.log");
|
try {
|
||||||
const lastLines = await readLastLinesOfFile(logPath, numberOfLines);
|
const logPath = path.join(getWaveHomeDir(), "wavesrv.log");
|
||||||
event.reply("last-logs", lastLines);
|
const lastLines = await readLastLinesOfFile(logPath, numberOfLines);
|
||||||
} catch (err) {
|
event.reply("last-logs", lastLines);
|
||||||
console.error("Error reading log file:", err);
|
} catch (err) {
|
||||||
event.reply("last-logs", "Error reading log file.");
|
console.error("Error reading log file:", err);
|
||||||
}
|
event.reply("last-logs", "Error reading log file.");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function readLastLinesOfFile(filePath, lineCount) {
|
function readLastLinesOfFile(filePath: string, lineCount: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
child_process.exec(`tail -n ${lineCount} "${filePath}"`, (err, stdout, stderr) => {
|
child_process.exec(`tail -n ${lineCount} "${filePath}"`, (err, stdout, stderr) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -587,8 +573,8 @@ function readLastLinesOfFile(filePath, lineCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getContextMenu(): any {
|
function getContextMenu(): any {
|
||||||
let menu = new electron.Menu();
|
const menu = new electron.Menu();
|
||||||
let menuItem = new electron.MenuItem({ label: "Testing", click: () => console.log("click testing!") });
|
const menuItem = new electron.MenuItem({ label: "Testing", click: () => console.log("click testing!") });
|
||||||
menu.append(menuItem);
|
menu.append(menuItem);
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
@ -600,8 +586,8 @@ function getFetchHeaders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getClientDataPoll(loopNum: number) {
|
async function getClientDataPoll(loopNum: number) {
|
||||||
let lastTime = loopNum >= 6;
|
const lastTime = loopNum >= 6;
|
||||||
let cdata = await getClientData(!lastTime, loopNum);
|
const cdata = await getClientData(!lastTime, loopNum);
|
||||||
if (lastTime || cdata != null) {
|
if (lastTime || cdata != null) {
|
||||||
return cdata;
|
return cdata;
|
||||||
}
|
}
|
||||||
@ -609,9 +595,9 @@ async function getClientDataPoll(loopNum: number) {
|
|||||||
return getClientDataPoll(loopNum + 1);
|
return getClientDataPoll(loopNum + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClientData(willRetry: boolean, retryNum: number) {
|
async function getClientData(willRetry: boolean, retryNum: number) {
|
||||||
let url = new URL(getBaseHostPort() + "/api/get-client-data");
|
const url = new URL(getBaseHostPort() + "/api/get-client-data");
|
||||||
let fetchHeaders = getFetchHeaders();
|
const fetchHeaders = getFetchHeaders();
|
||||||
return fetch(url, { headers: fetchHeaders })
|
return fetch(url, { headers: fetchHeaders })
|
||||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -623,9 +609,9 @@ function getClientData(willRetry: boolean, retryNum: number) {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (willRetry) {
|
if (willRetry) {
|
||||||
console.log("error getting client-data from wavesrv, will retry", "(" + retryNum + ")");
|
console.log("error getting client-data from wavesrv, will retry", "(" + retryNum + ")");
|
||||||
return null;
|
} else {
|
||||||
|
console.log("error getting client-data from wavesrv, failed: ", err);
|
||||||
}
|
}
|
||||||
console.log("error getting client-data from wavesrv, failed: ", err);
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -643,18 +629,18 @@ function sendWSSC() {
|
|||||||
function runWaveSrv() {
|
function runWaveSrv() {
|
||||||
let pResolve: (value: unknown) => void;
|
let pResolve: (value: unknown) => void;
|
||||||
let pReject: (reason?: any) => void;
|
let pReject: (reason?: any) => void;
|
||||||
let rtnPromise = new Promise((argResolve, argReject) => {
|
const rtnPromise = new Promise((argResolve, argReject) => {
|
||||||
pResolve = argResolve;
|
pResolve = argResolve;
|
||||||
pReject = argReject;
|
pReject = argReject;
|
||||||
});
|
});
|
||||||
let envCopy = Object.assign({}, process.env);
|
const envCopy = { ...process.env };
|
||||||
envCopy[WaveAppPathVarName] = getAppBasePath();
|
envCopy[WaveAppPathVarName] = getAppBasePath();
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
envCopy[WaveDevVarName] = "1";
|
envCopy[WaveDevVarName] = "1";
|
||||||
}
|
}
|
||||||
let waveSrvCmd = getWaveSrvCmd();
|
const waveSrvCmd = getWaveSrvCmd();
|
||||||
console.log("trying to run local server", waveSrvCmd);
|
console.log("trying to run local server", waveSrvCmd);
|
||||||
let proc = child_process.spawn("bash", ["-c", waveSrvCmd], {
|
const proc = child_process.spawn("bash", ["-c", waveSrvCmd], {
|
||||||
cwd: getWaveSrvCwd(),
|
cwd: getWaveSrvCwd(),
|
||||||
env: envCopy,
|
env: envCopy,
|
||||||
});
|
});
|
||||||
@ -679,29 +665,29 @@ function runWaveSrv() {
|
|||||||
proc.on("error", (e) => {
|
proc.on("error", (e) => {
|
||||||
console.log("error running wavesrv", e);
|
console.log("error running wavesrv", e);
|
||||||
});
|
});
|
||||||
proc.stdout.on("data", (output) => {
|
proc.stdout.on("data", (_) => {
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
proc.stderr.on("data", (output) => {
|
proc.stderr.on("data", (_) => {
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
return rtnPromise;
|
return rtnPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
electron.ipcMain.on("context-screen", (event, { screenId }, { x, y }) => {
|
electron.ipcMain.on("context-screen", (_, { screenId }, { x, y }) => {
|
||||||
console.log("context-screen", screenId);
|
console.log("context-screen", screenId);
|
||||||
let menu = getContextMenu();
|
const menu = getContextMenu();
|
||||||
menu.popup({ x, y });
|
menu.popup({ x, y });
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("context-editmenu", (event, { x, y }, opts) => {
|
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
|
||||||
if (opts == null) {
|
if (opts == null) {
|
||||||
opts = {};
|
opts = {};
|
||||||
}
|
}
|
||||||
console.log("context-editmenu");
|
console.log("context-editmenu");
|
||||||
let menu = new electron.Menu();
|
const menu = new electron.Menu();
|
||||||
if (opts.showCut) {
|
if (opts.showCut) {
|
||||||
let menuItem = new electron.MenuItem({ label: "Cut", role: "cut" });
|
const menuItem = new electron.MenuItem({ label: "Cut", role: "cut" });
|
||||||
menu.append(menuItem);
|
menu.append(menuItem);
|
||||||
}
|
}
|
||||||
let menuItem = new electron.MenuItem({ label: "Copy", role: "copy" });
|
let menuItem = new electron.MenuItem({ label: "Copy", role: "copy" });
|
||||||
@ -723,16 +709,17 @@ async function createMainWindowWrap() {
|
|||||||
if (clientData && clientData.winsize.fullscreen) {
|
if (clientData && clientData.winsize.fullscreen) {
|
||||||
MainWindow.setFullScreen(true);
|
MainWindow.setFullScreen(true);
|
||||||
}
|
}
|
||||||
|
configureAutoUpdaterStartup(clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sleep(ms) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve, reject) => setTimeout(resolve, ms));
|
return new Promise((resolve, _) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function logActiveState() {
|
function logActiveState() {
|
||||||
let activeState = { fg: wasInFg, active: wasActive, open: true };
|
const activeState = { fg: wasInFg, active: wasActive, open: true };
|
||||||
let url = new URL(getBaseHostPort() + "/api/log-active-state");
|
const url = new URL(getBaseHostPort() + "/api/log-active-state");
|
||||||
let fetchHeaders = getFetchHeaders();
|
const fetchHeaders = getFetchHeaders();
|
||||||
fetch(url, { method: "post", body: JSON.stringify(activeState), headers: fetchHeaders })
|
fetch(url, { method: "post", body: JSON.stringify(activeState), headers: fetchHeaders })
|
||||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -765,7 +752,7 @@ function reregisterGlobalShortcut(shortcut: string) {
|
|||||||
currentGlobalShortcut = null;
|
currentGlobalShortcut = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ok = electron.globalShortcut.register(shortcut, () => {
|
const ok = electron.globalShortcut.register(shortcut, () => {
|
||||||
console.log("global shortcut triggered, showing window");
|
console.log("global shortcut triggered, showing window");
|
||||||
MainWindow?.show();
|
MainWindow?.show();
|
||||||
});
|
});
|
||||||
@ -777,10 +764,161 @@ function reregisterGlobalShortcut(shortcut: string) {
|
|||||||
currentGlobalShortcut = shortcut;
|
currentGlobalShortcut = shortcut;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== AUTO-UPDATER ====== //
|
||||||
|
let autoUpdateLock = false;
|
||||||
|
let autoUpdateInterval: NodeJS.Timeout | null = null;
|
||||||
|
let availableUpdateReleaseName: string | null = null;
|
||||||
|
let availableUpdateReleaseNotes: string | null = null;
|
||||||
|
let appUpdateStatus = "unavailable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the app update status and sends it to the main window
|
||||||
|
* @param status The AppUpdateStatus to set, either "ready" or "unavailable"
|
||||||
|
*/
|
||||||
|
function setAppUpdateStatus(status: string) {
|
||||||
|
appUpdateStatus = status;
|
||||||
|
if (MainWindow != null) {
|
||||||
|
MainWindow.webContents.send("app-update-status", appUpdateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the auto-updater and sets up event listeners
|
||||||
|
* @returns The interval at which the auto-updater checks for updates
|
||||||
|
*/
|
||||||
|
function initUpdater(): NodeJS.Timeout {
|
||||||
|
const { autoUpdater } = electron;
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
console.log("skipping auto-updater in dev mode");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppUpdateStatus("unavailable");
|
||||||
|
let feedURL = `https://dl.waveterm.dev/autoupdate/${unamePlatform}/${unameArch}`;
|
||||||
|
let serverType: "default" | "json" = "default";
|
||||||
|
|
||||||
|
if (unamePlatform == "darwin") {
|
||||||
|
feedURL += "/RELEASES.json";
|
||||||
|
serverType = "json";
|
||||||
|
}
|
||||||
|
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
url: feedURL,
|
||||||
|
headers: { "User-Agent": "Wave Auto-Update" },
|
||||||
|
serverType,
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.removeAllListeners();
|
||||||
|
|
||||||
|
autoUpdater.on("error", (err) => {
|
||||||
|
console.log("updater error");
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("checking-for-update", () => {
|
||||||
|
console.log("checking-for-update");
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-available", () => {
|
||||||
|
console.log("update-available; downloading...");
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-not-available", () => {
|
||||||
|
console.log("update-not-available");
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-downloaded", (event, releaseNotes, releaseName, releaseDate, updateURL) => {
|
||||||
|
console.log("update-downloaded", [event, releaseNotes, releaseName, releaseDate, updateURL]);
|
||||||
|
availableUpdateReleaseName = releaseName;
|
||||||
|
availableUpdateReleaseNotes = releaseNotes;
|
||||||
|
|
||||||
|
// Display the update banner and create a system notification
|
||||||
|
setAppUpdateStatus("ready");
|
||||||
|
const updateNotification = new electron.Notification({
|
||||||
|
title: "Wave Terminal",
|
||||||
|
body: "A new version of Wave Terminal is ready to install.",
|
||||||
|
});
|
||||||
|
updateNotification.on("click", () => {
|
||||||
|
fireAndForget(installAppUpdate);
|
||||||
|
});
|
||||||
|
updateNotification.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// check for updates right away and keep checking later
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
return setInterval(autoUpdater.checkForUpdates, 600000); // 10 minutes in ms
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user to install the downloaded application update and restarts the application
|
||||||
|
*/
|
||||||
|
async function installAppUpdate() {
|
||||||
|
const dialogOpts: Electron.MessageBoxOptions = {
|
||||||
|
type: "info",
|
||||||
|
buttons: ["Restart", "Later"],
|
||||||
|
title: "Application Update",
|
||||||
|
message: process.platform === "win32" ? availableUpdateReleaseNotes : availableUpdateReleaseName,
|
||||||
|
detail: "A new version has been downloaded. Restart the application to apply the updates.",
|
||||||
|
};
|
||||||
|
|
||||||
|
await electron.dialog.showMessageBox(MainWindow, dialogOpts).then(({ response }) => {
|
||||||
|
if (response === 0) electron.autoUpdater.quitAndInstall();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.ipcMain.on("install-app-update", () => fireAndForget(installAppUpdate));
|
||||||
|
electron.ipcMain.on("get-app-update-status", (event) => {
|
||||||
|
event.returnValue = appUpdateStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.on("change-auto-update", (_, enable: boolean) => {
|
||||||
|
configureAutoUpdater(enable);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the auto-updater based on the client data
|
||||||
|
* @param clientData The client data to use to configure the auto-updater. If the clientData has noreleasecheck set to true, the auto-updater will be disabled.
|
||||||
|
*/
|
||||||
|
function configureAutoUpdaterStartup(clientData: ClientDataType) {
|
||||||
|
configureAutoUpdater(!clientData.clientopts.noreleasecheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the auto-updater based on the user's preference
|
||||||
|
* @param enabled Whether the auto-updater should be enabled
|
||||||
|
*/
|
||||||
|
function configureAutoUpdater(enabled: boolean) {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// only configure auto-updater on macOS
|
||||||
|
if (unamePlatform == "darwin") {
|
||||||
|
if (enabled && autoUpdateInterval == null) {
|
||||||
|
try {
|
||||||
|
console.log("configuring auto updater");
|
||||||
|
autoUpdateInterval = initUpdater();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error configuring auto updater", e.toString());
|
||||||
|
}
|
||||||
|
} else if (autoUpdateInterval != null) {
|
||||||
|
console.log("user has disabled auto-updates, stopping updater");
|
||||||
|
clearInterval(autoUpdateInterval);
|
||||||
|
autoUpdateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoUpdateLock = false;
|
||||||
|
}
|
||||||
|
// ====== AUTO-UPDATER ====== //
|
||||||
|
|
||||||
// ====== MAIN ====== //
|
// ====== MAIN ====== //
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let instanceLock = app.requestSingleInstanceLock();
|
const instanceLock = app.requestSingleInstanceLock();
|
||||||
if (!instanceLock) {
|
if (!instanceLock) {
|
||||||
console.log("waveterm-app could not get single-instance-lock, shutting down");
|
console.log("waveterm-app could not get single-instance-lock, shutting down");
|
||||||
app.quit();
|
app.quit();
|
||||||
|
@ -15,6 +15,10 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
|
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
|
||||||
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),
|
reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),
|
||||||
openExternalLink: (url) => ipcRenderer.send("open-external-link", url),
|
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),
|
onTCmd: (callback) => ipcRenderer.on("t-cmd", callback),
|
||||||
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
|
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
|
||||||
onLCmd: (callback) => ipcRenderer.on("l-cmd", callback),
|
onLCmd: (callback) => ipcRenderer.on("l-cmd", callback),
|
||||||
|
@ -125,6 +125,10 @@ class Model {
|
|||||||
name: "renderVersion",
|
name: "renderVersion",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appUpdateStatus = mobx.observable.box(getApi().getAppUpdateStatus(), {
|
||||||
|
name: "appUpdateStatus",
|
||||||
|
});
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.clientId = getApi().getId();
|
this.clientId = getApi().getId();
|
||||||
this.isDev = getApi().getIsDev();
|
this.isDev = getApi().getIsDev();
|
||||||
@ -178,6 +182,7 @@ class Model {
|
|||||||
getApi().onBracketCmd(this.onBracketCmd.bind(this));
|
getApi().onBracketCmd(this.onBracketCmd.bind(this));
|
||||||
getApi().onDigitCmd(this.onDigitCmd.bind(this));
|
getApi().onDigitCmd(this.onDigitCmd.bind(this));
|
||||||
getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this));
|
getApi().onWaveSrvStatusChange(this.onWaveSrvStatusChange.bind(this));
|
||||||
|
getApi().onAppUpdateStatus(this.onAppUpdateStatus.bind(this));
|
||||||
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
|
document.addEventListener("keydown", this.docKeyDownHandler.bind(this));
|
||||||
document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this));
|
document.addEventListener("selectionchange", this.docSelectionChangeHandler.bind(this));
|
||||||
setTimeout(() => this.getClientDataLoop(1), 10);
|
setTimeout(() => this.getClientDataLoop(1), 10);
|
||||||
@ -756,8 +761,7 @@ class Model {
|
|||||||
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
|
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
|
||||||
this.inputModel.resetInput();
|
this.inputModel.resetInput();
|
||||||
if (genUpdate.type == "model") {
|
if (genUpdate.type == "model") {
|
||||||
const modelUpdate = genUpdate as ModelUpdatePacket;
|
const reversedGenUpdate = genUpdate.data.slice().reverse();
|
||||||
const reversedGenUpdate = modelUpdate.data.slice().reverse();
|
|
||||||
const lastCmdLine = reversedGenUpdate.find((update) => "cmdline" in update);
|
const lastCmdLine = reversedGenUpdate.find((update) => "cmdline" in update);
|
||||||
if (lastCmdLine) {
|
if (lastCmdLine) {
|
||||||
// TODO a bit of a hack since this update gets applied in runUpdate_internal.
|
// 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) {
|
runUpdate_internal(genUpdate: UpdatePacket, uiContext: UIContextType, interactive: boolean) {
|
||||||
if (genUpdate.type == "pty") {
|
if (genUpdate.type == "pty") {
|
||||||
const ptyMsg = genUpdate.data as PtyDataUpdateType;
|
const ptyMsg = genUpdate.data;
|
||||||
if (isBlank(ptyMsg.remoteid)) {
|
if (isBlank(ptyMsg.remoteid)) {
|
||||||
// regular update
|
// regular update
|
||||||
this.updatePtyData(ptyMsg);
|
this.updatePtyData(ptyMsg);
|
||||||
@ -829,7 +833,7 @@ class Model {
|
|||||||
this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
|
this.remotesModel.receiveData(ptyMsg.remoteid, ptyMsg.ptypos, ptyData);
|
||||||
}
|
}
|
||||||
} else if (genUpdate.type == "model") {
|
} else if (genUpdate.type == "model") {
|
||||||
const modelUpdateItems = genUpdate.data as ModelUpdateItemType[];
|
const modelUpdateItems = genUpdate.data;
|
||||||
|
|
||||||
let showedRemotesModal = false;
|
let showedRemotesModal = false;
|
||||||
const [oldActiveSessionId, oldActiveScreenId] = this.getActiveIds();
|
const [oldActiveSessionId, oldActiveScreenId] = this.getActiveIds();
|
||||||
@ -1100,8 +1104,7 @@ class Model {
|
|||||||
|
|
||||||
isInfoUpdate(update: UpdatePacket): boolean {
|
isInfoUpdate(update: UpdatePacket): boolean {
|
||||||
if (update.type == "model") {
|
if (update.type == "model") {
|
||||||
const modelUpdate = update as ModelUpdatePacket;
|
return update.data.some((u) => u.info != null || u.history != null);
|
||||||
return modelUpdate.data.some((u) => u.info != null || u.history != null);
|
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1506,6 +1509,21 @@ class Model {
|
|||||||
const resp = await prtn;
|
const resp = await prtn;
|
||||||
const _ = await handleJsonFetchResponse(url, resp);
|
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 };
|
export { Model, getApi };
|
||||||
|
5
src/types/custom.d.ts
vendored
5
src/types/custom.d.ts
vendored
@ -11,6 +11,7 @@ declare global {
|
|||||||
type HistoryTypeStrs = "global" | "session" | "screen";
|
type HistoryTypeStrs = "global" | "session" | "screen";
|
||||||
type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error";
|
type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error";
|
||||||
type LineContainerStrs = "main" | "sidebar" | "history";
|
type LineContainerStrs = "main" | "sidebar" | "history";
|
||||||
|
type AppUpdateStatusType = "unavailable" | "ready";
|
||||||
|
|
||||||
type OV<V> = mobx.IObservableValue<V>;
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
type OArr<V> = mobx.IObservableArray<V>;
|
type OArr<V> = mobx.IObservableArray<V>;
|
||||||
@ -869,6 +870,10 @@ declare global {
|
|||||||
reloadWindow: () => void;
|
reloadWindow: () => void;
|
||||||
openExternalLink: (url: string) => void;
|
openExternalLink: (url: string) => void;
|
||||||
reregisterGlobalShortcut: (shortcut: 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;
|
onTCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onICmd: (callback: (mods: KeyModsType) => void) => void;
|
onICmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
|
@ -363,6 +363,16 @@ function ces(s: string) {
|
|||||||
return s;
|
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 {
|
export {
|
||||||
handleJsonFetchResponse,
|
handleJsonFetchResponse,
|
||||||
base64ToString,
|
base64ToString,
|
||||||
@ -389,4 +399,5 @@ export {
|
|||||||
getRemoteConnVal,
|
getRemoteConnVal,
|
||||||
getRemoteName,
|
getRemoteName,
|
||||||
ces,
|
ces,
|
||||||
|
fireAndForget,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user