From e5132e8198c6921e6acc9c1fd3eb6e12959ac017 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Mon, 20 May 2019 01:08:26 +0200 Subject: [PATCH] implement AutoUpdater --- ElectronNET.API/AutoUpdater.cs | 96 +++++++++++++++++++ ElectronNET.API/Electron.cs | 5 + .../Entities/BlockMapDataHolder.cs | 35 +++++++ ElectronNET.API/Entities/ReleaseNoteInfo.cs | 18 ++++ .../Entities/UpdateCancellationToken.cs | 29 ++++++ ElectronNET.API/Entities/UpdateCheckResult.cs | 23 +++++ ElectronNET.API/Entities/UpdateFileInfo.cs | 13 +++ ElectronNET.API/Entities/UpdateInfo.cs | 38 ++++++++ .../Actions/DeployEmbeddedElectronFiles.cs | 1 + ElectronNET.CLI/ElectronNET.CLI.csproj | 1 + ElectronNET.Host/api/autoUpdater.js | 28 ++++++ ElectronNET.Host/api/autoUpdater.js.map | 1 + ElectronNET.Host/api/autoUpdater.ts | 22 +++++ ElectronNET.Host/main.js | 3 +- ElectronNET.Host/package.json | 3 +- ElectronNET.WebApp/electron.manifest.json | 21 +++- 16 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 ElectronNET.API/AutoUpdater.cs create mode 100644 ElectronNET.API/Entities/BlockMapDataHolder.cs create mode 100644 ElectronNET.API/Entities/ReleaseNoteInfo.cs create mode 100644 ElectronNET.API/Entities/UpdateCancellationToken.cs create mode 100644 ElectronNET.API/Entities/UpdateCheckResult.cs create mode 100644 ElectronNET.API/Entities/UpdateFileInfo.cs create mode 100644 ElectronNET.API/Entities/UpdateInfo.cs create mode 100644 ElectronNET.Host/api/autoUpdater.js create mode 100644 ElectronNET.Host/api/autoUpdater.js.map create mode 100644 ElectronNET.Host/api/autoUpdater.ts diff --git a/ElectronNET.API/AutoUpdater.cs b/ElectronNET.API/AutoUpdater.cs new file mode 100644 index 0000000..7a44162 --- /dev/null +++ b/ElectronNET.API/AutoUpdater.cs @@ -0,0 +1,96 @@ +using ElectronNET.API.Entities; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ElectronNET.API +{ + /// + /// Enable apps to automatically update themselves. Based on electron-updater. + /// + public sealed class AutoUpdater + { + private static AutoUpdater _autoUpdater; + private static object _syncRoot = new object(); + + internal AutoUpdater() { } + + internal static AutoUpdater Instance + { + get + { + if (_autoUpdater == null) + { + lock (_syncRoot) + { + if (_autoUpdater == null) + { + _autoUpdater = new AutoUpdater(); + } + } + } + + return _autoUpdater; + } + } + + /// + /// Asks the server whether there is an update. + /// + /// + public Task CheckForUpdatesAsync() + { + var taskCompletionSource = new TaskCompletionSource(); + string guid = Guid.NewGuid().ToString(); + + BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesComplete" + guid, (updateCheckResult) => + { + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesComplete" + guid); + taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + }); + + BridgeConnector.Socket.Emit("autoUpdaterCheckForUpdates", guid); + + return taskCompletionSource.Task; + } + + /// + /// Asks the server whether there is an update. + /// + /// This will immediately download an update, then install when the app quits. + /// + /// + public Task CheckForUpdatesAndNotifyAsync() + { + var taskCompletionSource = new TaskCompletionSource(); + string guid = Guid.NewGuid().ToString(); + + BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesAndNotifyComplete" + guid, (updateCheckResult) => + { + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesAndNotifyComplete" + guid); + taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + }); + + BridgeConnector.Socket.Emit("autoUpdaterCheckForUpdatesAndNotify", guid); + + return taskCompletionSource.Task; + } + + /// + /// Restarts the app and installs the update after it has been downloaded. + /// It should only be called after `update-downloaded` has been emitted. + /// + /// Note: QuitAndInstall() will close all application windows first and only emit `before-quit` event on `app` after that. + /// This is different from the normal quit event sequence. + /// + /// *windows-only* Runs the installer in silent mode. Defaults to `false`. + /// Run the app after finish even on silent install. Not applicable for macOS. Ignored if `isSilent` is set to `false`. + public void QuitAndInstall(bool isSilent = false, bool isForceRunAfter = false) + { + BridgeConnector.Socket.Emit("autoUpdaterQuitAndInstall", isSilent, isForceRunAfter); + } + } +} diff --git a/ElectronNET.API/Electron.cs b/ElectronNET.API/Electron.cs index 1f739d0..4206f89 100644 --- a/ElectronNET.API/Electron.cs +++ b/ElectronNET.API/Electron.cs @@ -15,6 +15,11 @@ /// public static App App { get { return App.Instance; } } + /// + /// Enable apps to automatically update themselves. Based on electron-updater. + /// + public static AutoUpdater AutoUpdater { get { return AutoUpdater.Instance; } } + /// /// Control your windows. /// diff --git a/ElectronNET.API/Entities/BlockMapDataHolder.cs b/ElectronNET.API/Entities/BlockMapDataHolder.cs new file mode 100644 index 0000000..9294ac0 --- /dev/null +++ b/ElectronNET.API/Entities/BlockMapDataHolder.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class BlockMapDataHolder + { + /// + /// The file size. Used to verify downloaded size (save one HTTP request to get length). + /// Also used when block map data is embedded into the file(appimage, windows web installer package). + /// + public double Size { get; set; } + + /// + /// The block map file size. Used when block map data is embedded into the file (appimage, windows web installer package). + /// This information can be obtained from the file itself, but it requires additional HTTP request, + /// so, to reduce request count, block map size is specified in the update metadata too. + /// + public double BlockMapSize { get; set; } + + /// + /// The file checksum. + /// + public string Sha512 { get; set; } + + /// + /// + /// + public bool IsAdminRightsRequired { get; set; } + } +} diff --git a/ElectronNET.API/Entities/ReleaseNoteInfo.cs b/ElectronNET.API/Entities/ReleaseNoteInfo.cs new file mode 100644 index 0000000..c9ebf2a --- /dev/null +++ b/ElectronNET.API/Entities/ReleaseNoteInfo.cs @@ -0,0 +1,18 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class ReleaseNoteInfo + { + /// + /// The version. + /// + public string Version { get; set; } + + /// + /// The note. + /// + public string Note { get; set; } + } +} diff --git a/ElectronNET.API/Entities/UpdateCancellationToken.cs b/ElectronNET.API/Entities/UpdateCancellationToken.cs new file mode 100644 index 0000000..f89348c --- /dev/null +++ b/ElectronNET.API/Entities/UpdateCancellationToken.cs @@ -0,0 +1,29 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class UpdateCancellationToken + { + /// + /// + /// + public bool Cancelled { get; set; } + + /// + /// + /// + public void Cancel() + { + + } + + /// + /// + /// + public void Dispose() + { + + } + } +} diff --git a/ElectronNET.API/Entities/UpdateCheckResult.cs b/ElectronNET.API/Entities/UpdateCheckResult.cs new file mode 100644 index 0000000..66c75aa --- /dev/null +++ b/ElectronNET.API/Entities/UpdateCheckResult.cs @@ -0,0 +1,23 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class UpdateCheckResult + { + /// + /// + /// + public UpdateInfo UpdateInfo { get; set; } = new UpdateInfo(); + + /// + /// + /// + public string[] Download { get; set; } + + /// + /// + /// + public UpdateCancellationToken CancellationToken { get; set; } + } +} diff --git a/ElectronNET.API/Entities/UpdateFileInfo.cs b/ElectronNET.API/Entities/UpdateFileInfo.cs new file mode 100644 index 0000000..5f5d6f6 --- /dev/null +++ b/ElectronNET.API/Entities/UpdateFileInfo.cs @@ -0,0 +1,13 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class UpdateFileInfo : BlockMapDataHolder + { + /// + /// + /// + public string Url { get; set; } + } +} diff --git a/ElectronNET.API/Entities/UpdateInfo.cs b/ElectronNET.API/Entities/UpdateInfo.cs new file mode 100644 index 0000000..5b33bf0 --- /dev/null +++ b/ElectronNET.API/Entities/UpdateInfo.cs @@ -0,0 +1,38 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class UpdateInfo + { + /// + /// The version. + /// + public string Version { get; set; } + + /// + /// + /// + public UpdateFileInfo[] Files { get; set; } = new UpdateFileInfo[0]; + + /// + /// The release name. + /// + public string ReleaseName { get; set; } + + /// + /// The release notes. + /// + public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0]; + + /// + /// + /// + public string ReleaseDate { get; set; } + + /// + /// The staged rollout percentage, 0-100. + /// + public int StagingPercentage { get; set; } + } +} \ No newline at end of file diff --git a/ElectronNET.CLI/Commands/Actions/DeployEmbeddedElectronFiles.cs b/ElectronNET.CLI/Commands/Actions/DeployEmbeddedElectronFiles.cs index 7cb6542..5c82bf0 100644 --- a/ElectronNET.CLI/Commands/Actions/DeployEmbeddedElectronFiles.cs +++ b/ElectronNET.CLI/Commands/Actions/DeployEmbeddedElectronFiles.cs @@ -27,6 +27,7 @@ namespace ElectronNET.CLI.Commands.Actions EmbeddedFileHelper.DeployEmbeddedFile(hostApiFolder, "shell.js", "api."); EmbeddedFileHelper.DeployEmbeddedFile(hostApiFolder, "screen.js", "api."); EmbeddedFileHelper.DeployEmbeddedFile(hostApiFolder, "clipboard.js", "api."); + EmbeddedFileHelper.DeployEmbeddedFile(hostApiFolder, "autoUpdater.js", "api."); string splashscreenFolder = Path.Combine(tempPath, "splashscreen"); if (Directory.Exists(splashscreenFolder) == false) diff --git a/ElectronNET.CLI/ElectronNET.CLI.csproj b/ElectronNET.CLI/ElectronNET.CLI.csproj index a018aab..9d4dfd6 100644 --- a/ElectronNET.CLI/ElectronNET.CLI.csproj +++ b/ElectronNET.CLI/ElectronNET.CLI.csproj @@ -92,6 +92,7 @@ This package contains the dotnet tooling to electronize your application. + diff --git a/ElectronNET.Host/api/autoUpdater.js b/ElectronNET.Host/api/autoUpdater.js new file mode 100644 index 0000000..8c97fe7 --- /dev/null +++ b/ElectronNET.Host/api/autoUpdater.js @@ -0,0 +1,28 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +const electron_updater_1 = require("electron-updater"); +const path = require('path'); +let electronSocket; +module.exports = (socket) => { + electronSocket = socket; + socket.on('autoUpdaterCheckForUpdatesAndNotify', (guid) => __awaiter(this, void 0, void 0, function* () { + const updateCheckResult = yield electron_updater_1.autoUpdater.checkForUpdatesAndNotify(); + electronSocket.emit('autoUpdaterCheckForUpdatesAndNotifyComplete' + guid, updateCheckResult); + })); + socket.on('autoUpdaterCheckForUpdates', (guid) => __awaiter(this, void 0, void 0, function* () { + // autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml'); + const updateCheckResult = yield electron_updater_1.autoUpdater.checkForUpdates(); + electronSocket.emit('autoUpdaterCheckForUpdatesComplete' + guid, updateCheckResult); + })); + socket.on('autoUpdaterQuitAndInstall', (isSilent, isForceRunAfter) => __awaiter(this, void 0, void 0, function* () { + electron_updater_1.autoUpdater.quitAndInstall(isSilent, isForceRunAfter); + })); +}; +//# sourceMappingURL=autoUpdater.js.map \ No newline at end of file diff --git a/ElectronNET.Host/api/autoUpdater.js.map b/ElectronNET.Host/api/autoUpdater.js.map new file mode 100644 index 0000000..6529890 --- /dev/null +++ b/ElectronNET.Host/api/autoUpdater.js.map @@ -0,0 +1 @@ +{"version":3,"file":"autoUpdater.js","sourceRoot":"","sources":["autoUpdater.ts"],"names":[],"mappings":";;;;;;;;;AAAA,uDAA+C;AAC/C,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAC7B,IAAI,cAAc,CAAC;AAEnB,iBAAS,CAAC,MAAuB,EAAE,EAAE;IACjC,cAAc,GAAG,MAAM,CAAC;IAExB,MAAM,CAAC,EAAE,CAAC,qCAAqC,EAAE,CAAO,IAAI,EAAE,EAAE;QAC5D,MAAM,iBAAiB,GAAG,MAAM,8BAAW,CAAC,wBAAwB,EAAE,CAAC;QACvE,cAAc,CAAC,IAAI,CAAC,6CAA6C,GAAG,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACjG,CAAC,CAAA,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,4BAA4B,EAAE,CAAO,IAAI,EAAE,EAAE;QACnD,6EAA6E;QAC7E,MAAM,iBAAiB,GAAG,MAAM,8BAAW,CAAC,eAAe,EAAE,CAAC;QAC9D,cAAc,CAAC,IAAI,CAAC,oCAAoC,GAAG,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACxF,CAAC,CAAA,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,2BAA2B,EAAE,CAAO,QAAQ,EAAE,eAAe,EAAE,EAAE;QACvE,8BAAW,CAAC,cAAc,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC1D,CAAC,CAAA,CAAC,CAAC;AACP,CAAC,CAAC"} \ No newline at end of file diff --git a/ElectronNET.Host/api/autoUpdater.ts b/ElectronNET.Host/api/autoUpdater.ts new file mode 100644 index 0000000..a3f3c07 --- /dev/null +++ b/ElectronNET.Host/api/autoUpdater.ts @@ -0,0 +1,22 @@ +import { autoUpdater } from 'electron-updater'; +const path = require('path'); +let electronSocket; + +export = (socket: SocketIO.Socket) => { + electronSocket = socket; + + socket.on('autoUpdaterCheckForUpdatesAndNotify', async (guid) => { + const updateCheckResult = await autoUpdater.checkForUpdatesAndNotify(); + electronSocket.emit('autoUpdaterCheckForUpdatesAndNotifyComplete' + guid, updateCheckResult); + }); + + socket.on('autoUpdaterCheckForUpdates', async (guid) => { + // autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml'); + const updateCheckResult = await autoUpdater.checkForUpdates(); + electronSocket.emit('autoUpdaterCheckForUpdatesComplete' + guid, updateCheckResult); + }); + + socket.on('autoUpdaterQuitAndInstall', async (isSilent, isForceRunAfter) => { + autoUpdater.quitAndInstall(isSilent, isForceRunAfter); + }); +}; diff --git a/ElectronNET.Host/main.js b/ElectronNET.Host/main.js index f51156a..de630dc 100644 --- a/ElectronNET.Host/main.js +++ b/ElectronNET.Host/main.js @@ -6,7 +6,7 @@ const portscanner = require('portscanner'); const imageSize = require('image-size'); let io, server, browserWindows, ipc, apiProcess, loadURL; let appApi, menu, dialogApi, notification, tray, webContents; -let globalShortcut, shellApi, screen, clipboard; +let globalShortcut, shellApi, screen, clipboard, autoUpdater; let splashScreen, mainWindowId, hostHook; const currentBinPath = path.join(__dirname.replace('app.asar', ''), 'bin'); @@ -104,6 +104,7 @@ function startSocketApiBridge(port) { appApi = require('./api/app')(socket, app); browserWindows = require('./api/browserWindows')(socket, app); + autoUpdater = require('./api/autoUpdater')(socket); ipc = require('./api/ipc')(socket); menu = require('./api/menu')(socket); dialogApi = require('./api/dialog')(socket); diff --git a/ElectronNET.Host/package.json b/ElectronNET.Host/package.json index ca9a69e..3d6fea9 100644 --- a/ElectronNET.Host/package.json +++ b/ElectronNET.Host/package.json @@ -1,6 +1,6 @@ { "name": "electron.net.host", - "version": "1.0.0", + "version": "1.0.2", "description": "Electron-Host for Electron.NET.", "repository": { "url": "https://github.com/ElectronNET/Electron.NET" @@ -12,6 +12,7 @@ "start": "tsc -p ." }, "dependencies": { + "electron-updater": "^4.0.6", "image-size": "^0.7.4", "portscanner": "^2.2.0", "socket.io": "^2.2.0" diff --git a/ElectronNET.WebApp/electron.manifest.json b/ElectronNET.WebApp/electron.manifest.json index 59d5d4d..847cbde 100644 --- a/ElectronNET.WebApp/electron.manifest.json +++ b/ElectronNET.WebApp/electron.manifest.json @@ -7,9 +7,20 @@ "build": { "appId": "com.electronnetapidemos.app", "productName": "ElectronNET API Demos", - "copyright": "Copyright © 2019", + "copyright": "Copyright � 2019", "buildVersion": "5.22.12", "compression": "maximum", + "win": { + "icon": "Assets/electron.ico", + "publish": [ + { + "provider": "github", + "owner": "ElectronNET", + "repo": "electron.net-api-demos", + "token": " Insert GH_TOKEN here!" + } + ] + }, "directories": { "output": "../../../bin/Desktop", "buildResources": "Assets" @@ -18,14 +29,18 @@ { "from": "./bin", "to": "bin", - "filter": [ "**/*" ] + "filter": [ + "**/*" + ] } ], "files": [ { "from": "./ElectronHostHook/node_modules", "to": "ElectronHostHook/node_modules", - "filter": [ "**/*" ] + "filter": [ + "**/*" + ] }, "**/*" ]