From 4e9de2abfa563a9b78bb08de1069840e618c211a Mon Sep 17 00:00:00 2001 From: Fre Date: Wed, 8 Jul 2020 11:16:00 +0200 Subject: [PATCH] Aanpassingen autoupdate & async code --- ElectronNET.API/App.cs | 86 ++++++++++-- ElectronNET.API/AsyncHelper.cs | 46 ++++++ ElectronNET.API/AutoUpdater.cs | 131 +++++++++++++++++- ElectronNET.API/BridgeConnector.cs | 8 ++ ElectronNET.API/ElectronNET.API.csproj | 1 + ElectronNET.API/Entities/ElectronException.cs | 28 ++++ ElectronNET.API/Entities/SemVer.cs | 59 ++++++++ ElectronNET.API/HybridSupport.cs | 9 +- ElectronNET.CLI/done.txt | 5 + ElectronNET.CLI/updatetool.ps1 | 2 + ElectronNET.Host/api/autoUpdater.ts | 40 +++++- ElectronNET.Host/main.js | 18 ++- 12 files changed, 404 insertions(+), 29 deletions(-) create mode 100644 ElectronNET.API/AsyncHelper.cs create mode 100644 ElectronNET.API/Entities/ElectronException.cs create mode 100644 ElectronNET.API/Entities/SemVer.cs create mode 100644 ElectronNET.CLI/done.txt create mode 100644 ElectronNET.CLI/updatetool.ps1 diff --git a/ElectronNET.API/App.cs b/ElectronNET.API/App.cs index 194dd4b..c59eb28 100644 --- a/ElectronNET.API/App.cs +++ b/ElectronNET.API/App.cs @@ -407,6 +407,27 @@ namespace ElectronNET.API /// which will be preferred over name by Electron. /// public string Name + { + [Obsolete("Use the asynchronous version NameAsync instead")] + get + { + return AsyncHelper.RunSync(async () => await NameAsync); + } + set + { + BridgeConnector.Socket.Emit("appSetName", value); + } + } + + /// + /// A property that indicates the current application's name, which is the name in the + /// application's package.json file. + /// + /// Usually the name field of package.json is a short lowercase name, according to the npm modules spec. You + /// should usually also specify a productName field, which is your application's full capitalized name, and + /// which will be preferred over name by Electron. + /// + public Task NameAsync { get { @@ -423,14 +444,34 @@ namespace ElectronNET.API BridgeConnector.Socket.Emit("appGetName"); return taskCompletionSource.Task; - }).Result; - } - set - { - BridgeConnector.Socket.Emit("appSetName", value); + }); } } + /// + /// + /// + /// + public Task GetName() + + { + + var taskCompletionSource = new TaskCompletionSource(); + + BridgeConnector.Socket.On("appGetNameCompleted", (result) => + { + BridgeConnector.Socket.Off("appGetNameCompleted"); + taskCompletionSource.SetResult((string)result); + }); + + BridgeConnector.Socket.Emit("appGetName"); + + return taskCompletionSource.Task; + + } + + + internal App() { CommandLine = new CommandLine(); @@ -1479,6 +1520,27 @@ namespace ElectronNET.API /// is used. /// public string UserAgentFallback + { + [Obsolete("Use the asynchronous version UserAgentFallbackAsync instead")] + get + { + return AsyncHelper.RunSync(async () => await UserAgentFallbackAsync); + } + set + { + BridgeConnector.Socket.Emit("appSetUserAgentFallback", value); + } + } + + /// + /// A which is the user agent string Electron will use as a global fallback. + /// + /// This is the user agent that will be used when no user agent is set at the webContents or + /// session level. It is useful for ensuring that your entire app has the same user agent. Set to a + /// custom value as early as possible in your app's initialization to ensure that your overridden value + /// is used. + /// + public Task UserAgentFallbackAsync { get { @@ -1487,19 +1549,15 @@ namespace ElectronNET.API var taskCompletionSource = new TaskCompletionSource(); BridgeConnector.Socket.On("appGetUserAgentFallbackCompleted", (result) => - { - BridgeConnector.Socket.Off("appGetUserAgentFallbackCompleted"); - taskCompletionSource.SetResult((string)result); - }); + { + BridgeConnector.Socket.Off("appGetUserAgentFallbackCompleted"); + taskCompletionSource.SetResult((string)result); + }); BridgeConnector.Socket.Emit("appGetUserAgentFallback"); return taskCompletionSource.Task; - }).Result; - } - set - { - BridgeConnector.Socket.Emit("appSetUserAgentFallback", value); + }); } } diff --git a/ElectronNET.API/AsyncHelper.cs b/ElectronNET.API/AsyncHelper.cs new file mode 100644 index 0000000..8feeea6 --- /dev/null +++ b/ElectronNET.API/AsyncHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ElectronNET.API +{ + internal static class AsyncHelper + { + private static readonly TaskFactory _taskFactory = new + TaskFactory(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + + public static TResult RunSync(Func> func) + => _taskFactory + .StartNew(func) + .Unwrap() + .GetAwaiter() + .GetResult(); + + public static void RunSync(Func func) + => _taskFactory + .StartNew(func) + .Unwrap() + .GetAwaiter() + .GetResult(); + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { + using (var timeoutCancellationTokenSource = new CancellationTokenSource()) + { + var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + return await task; // Very important in order to propagate exceptions + } + else + { + throw new TimeoutException($"{nameof(TimeoutAfter)}: The operation has timed out after {timeout:mm\\:ss}"); + } + } + } + } +} diff --git a/ElectronNET.API/AutoUpdater.cs b/ElectronNET.API/AutoUpdater.cs index ccf2590..57ab1c6 100644 --- a/ElectronNET.API/AutoUpdater.cs +++ b/ElectronNET.API/AutoUpdater.cs @@ -1,6 +1,12 @@ using ElectronNET.API.Entities; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; namespace ElectronNET.API @@ -182,11 +188,48 @@ namespace ElectronNET.API } } + /// + /// The current application version + /// + public Task CurrentVersionAsync + { + get + { + return Task.Run(() => + { + var taskCompletionSource = new TaskCompletionSource(); + + BridgeConnector.Socket.On("autoUpdater-currentVersion-get-reply", (result) => + { + BridgeConnector.Socket.Off("autoUpdater-currentVersion-get-reply"); + SemVer version = ((JObject)result).ToObject(); + taskCompletionSource.SetResult(version); + }); + BridgeConnector.Socket.Emit("autoUpdater-currentVersion-get"); + + return taskCompletionSource.Task; + }); + } + } + /// /// Get the update channel. Not applicable for GitHub. /// Doesn’t return channel from the update configuration, only if was previously set. /// + [Obsolete("Use the asynchronous version ChannelAsync instead")] public string Channel + { + get + { + return AsyncHelper.RunSync(async () => await ChannelAsync); + } + } + + /// + /// Get the update channel. Not applicable for GitHub. + /// Doesn’t return channel from the update configuration, only if was previously set. + /// + public Task ChannelAsync { get { @@ -199,18 +242,52 @@ namespace ElectronNET.API BridgeConnector.Socket.Off("autoUpdater-channel-get-reply"); taskCompletionSource.SetResult(result.ToString()); }); - BridgeConnector.Socket.Emit("autoUpdater-channel-get"); return taskCompletionSource.Task; - }).Result; + }); + } + } + + + + /// + /// The request headers. + /// + public Task> RequestHeadersAsync + { + get + { + return Task.Run(() => + { + var taskCompletionSource = new TaskCompletionSource>(); + BridgeConnector.Socket.On("autoUpdater-requestHeaders-get-reply", (headers) => + { + BridgeConnector.Socket.Off("autoUpdater-requestHeaders-get-reply"); + Dictionary result = ((JObject)headers).ToObject>(); + taskCompletionSource.SetResult(result); + }); + BridgeConnector.Socket.Emit("autoUpdater-requestHeaders-get"); + return taskCompletionSource.Task; + }); } } /// - /// Emitted when there is an error while updating. + /// The request headers. /// - public event Action OnError + public Dictionary RequestHeaders + { + set + { + BridgeConnector.Socket.Emit("autoUpdater-requestHeaders-set", JObject.FromObject(value, _jsonSerializer)); + } + } + + /// + /// Emitted when there is an error while updating. + /// + public event Action OnError { add { @@ -416,9 +493,26 @@ namespace ElectronNET.API string guid = Guid.NewGuid().ToString(); BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesComplete" + guid, (updateCheckResult) => + { + try + { + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesComplete" + guid); + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesError" + guid); + taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + } + catch (Exception ex) + { + taskCompletionSource.SetException(ex); + } + }); + BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesError" + guid, (error) => { BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesComplete" + guid); - taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesError" + guid); + string message = "An error occurred in CheckForUpdatesAsync"; + if (error != null && !string.IsNullOrEmpty(error.ToString())) + message = JsonConvert.SerializeObject(error); + taskCompletionSource.SetException(new ElectronException(message)); }); BridgeConnector.Socket.Emit("autoUpdaterCheckForUpdates", guid); @@ -438,9 +532,29 @@ namespace ElectronNET.API string guid = Guid.NewGuid().ToString(); BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesAndNotifyComplete" + guid, (updateCheckResult) => + { + try + { + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesAndNotifyComplete" + guid); + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesAndNotifyError" + guid); + if (updateCheckResult == null) + taskCompletionSource.SetResult(null); + else + taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + } + catch (Exception ex) + { + taskCompletionSource.SetException(ex); + } + }); + BridgeConnector.Socket.On("autoUpdaterCheckForUpdatesAndNotifyError" + guid, (error) => { BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesAndNotifyComplete" + guid); - taskCompletionSource.SetResult(JObject.Parse(updateCheckResult.ToString()).ToObject()); + BridgeConnector.Socket.Off("autoUpdaterCheckForUpdatesAndNotifyError" + guid); + string message = "An error occurred in autoUpdaterCheckForUpdatesAndNotify"; + if (error != null) + message = JsonConvert.SerializeObject(error); + taskCompletionSource.SetException(new ElectronException(message)); }); BridgeConnector.Socket.Emit("autoUpdaterCheckForUpdatesAndNotify", guid); @@ -501,5 +615,10 @@ namespace ElectronNET.API return taskCompletionSource.Task; } + + private readonly JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; } } diff --git a/ElectronNET.API/BridgeConnector.cs b/ElectronNET.API/BridgeConnector.cs index 08a8474..1f0bcd9 100644 --- a/ElectronNET.API/BridgeConnector.cs +++ b/ElectronNET.API/BridgeConnector.cs @@ -23,6 +23,14 @@ namespace ElectronNET.API { Console.WriteLine("BridgeConnector connected!"); }); + _socket.On(Socket.EVENT_CONNECT_ERROR, (args) => + { + Console.WriteLine("Socket error! {0}", args??"no args"); + }); + _socket.On(Socket.EVENT_DISCONNECT, (args) => + { + Console.WriteLine("Socket Disconnect! {0}", args ?? "no args"); + }); } } } diff --git a/ElectronNET.API/ElectronNET.API.csproj b/ElectronNET.API/ElectronNET.API.csproj index 21a5ae1..76e2c52 100644 --- a/ElectronNET.API/ElectronNET.API.csproj +++ b/ElectronNET.API/ElectronNET.API.csproj @@ -40,6 +40,7 @@ This package contains the API to access the "native" electron API. all runtime; build; native; contentfiles; analyzers + diff --git a/ElectronNET.API/Entities/ElectronException.cs b/ElectronNET.API/Entities/ElectronException.cs new file mode 100644 index 0000000..21e4555 --- /dev/null +++ b/ElectronNET.API/Entities/ElectronException.cs @@ -0,0 +1,28 @@ +using System; + +namespace ElectronNET.API.Entities +{ + /// + /// Electron Exception + /// + [Serializable] + public class ElectronException : Exception + { + /// + /// + /// + public ElectronException() + { + + } + + /// + /// + /// + /// + public ElectronException(string error) : base(error) + { + + } + } +} diff --git a/ElectronNET.API/Entities/SemVer.cs b/ElectronNET.API/Entities/SemVer.cs new file mode 100644 index 0000000..e1a08f1 --- /dev/null +++ b/ElectronNET.API/Entities/SemVer.cs @@ -0,0 +1,59 @@ +namespace ElectronNET.API.Entities +{ + /// + /// + /// + public class SemVer + { + /// + /// + /// + public string Raw { get; set; } + /// + /// + /// + public bool Loose { get; set; } + /// + /// + /// + public SemVerOptions Options { get; set; } + /// + /// + /// + public int Major { get; set; } + /// + /// + /// + public int Minor { get; set; } + /// + /// + /// + public int Patch { get; set; } + /// + /// + /// + public string Version { get; set; } + /// + /// + /// + public string[] Build { get; set; } + /// + /// + /// + public string[] Prerelease { get; set; } + } + + /// + /// + /// + public class SemVerOptions { + /// + /// + /// + public bool? Loose { get; set; } + /// + /// + /// + public bool? IncludePrerelease { get; set; } + } +} diff --git a/ElectronNET.API/HybridSupport.cs b/ElectronNET.API/HybridSupport.cs index 935ea51..0a3c59f 100644 --- a/ElectronNET.API/HybridSupport.cs +++ b/ElectronNET.API/HybridSupport.cs @@ -1,4 +1,6 @@ -namespace ElectronNET.API +using Quobject.SocketIoClientDotNet.Client; + +namespace ElectronNET.API { /// /// @@ -18,5 +20,10 @@ return !string.IsNullOrEmpty(BridgeSettings.SocketPort); } } + + /// + /// + /// + public static Socket Socket { get { return BridgeConnector.Socket; } } } } \ No newline at end of file diff --git a/ElectronNET.CLI/done.txt b/ElectronNET.CLI/done.txt new file mode 100644 index 0000000..1efa930 --- /dev/null +++ b/ElectronNET.CLI/done.txt @@ -0,0 +1,5 @@ +added npm package serialize-error to CLI api as a dependency +It's used because node errors aren't stringified completely. ("message" gets lost in normal stringify while sending over socket) + +catch() clauses in electron-updater + diff --git a/ElectronNET.CLI/updatetool.ps1 b/ElectronNET.CLI/updatetool.ps1 new file mode 100644 index 0000000..ac8e9e7 --- /dev/null +++ b/ElectronNET.CLI/updatetool.ps1 @@ -0,0 +1,2 @@ +dotnet tool uninstall -g electronnet.cli +dotnet tool install -g --add-source ../artifacts electronnet.cli \ No newline at end of file diff --git a/ElectronNET.Host/api/autoUpdater.ts b/ElectronNET.Host/api/autoUpdater.ts index f5493e0..4e26d2b 100644 --- a/ElectronNET.Host/api/autoUpdater.ts +++ b/ElectronNET.Host/api/autoUpdater.ts @@ -93,6 +93,11 @@ export = (socket: SocketIO.Socket) => { autoUpdater.updateConfigPath = value; }); + socket.on('autoUpdater-currentVersion-get', () => + { + electronSocket.emit('autoUpdater-currentVersion-get-reply', autoUpdater.currentVersion); + }); + socket.on('autoUpdater-channel-get', () => { electronSocket.emit('autoUpdater-channel-get-reply', autoUpdater.channel || ''); }); @@ -101,19 +106,42 @@ export = (socket: SocketIO.Socket) => { autoUpdater.channel = value; }); + socket.on('autoUpdater-requestHeaders-get', () => + { + electronSocket.emit('autoUpdater-requestHeaders-get-reply', autoUpdater.requestHeaders); + }); + + socket.on('autoUpdater-requestHeaders-set', (value) => + { + autoUpdater.requestHeaders = value; + }); + // Methods ******** - socket.on('autoUpdaterCheckForUpdatesAndNotify', async (guid) => { - const updateCheckResult = await autoUpdater.checkForUpdatesAndNotify(); - electronSocket.emit('autoUpdaterCheckForUpdatesAndNotifyComplete' + guid, updateCheckResult); + socket.on('autoUpdaterCheckForUpdatesAndNotify', async (guid) => + { + autoUpdater.checkForUpdatesAndNotify().then((updateCheckResult) => + { + electronSocket.emit('autoUpdaterCheckForUpdatesAndNotifyComplete' + guid, updateCheckResult); + }).catch((error) => + { + electronSocket.emit('autoUpdaterCheckForUpdatesAndNotifyError' + guid, error); + }); }); - socket.on('autoUpdaterCheckForUpdates', async (guid) => { + socket.on('autoUpdaterCheckForUpdates', async (guid) => + { // autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml'); - const updateCheckResult = await autoUpdater.checkForUpdates(); - electronSocket.emit('autoUpdaterCheckForUpdatesComplete' + guid, updateCheckResult); + autoUpdater.checkForUpdates().then((updateCheckResult) => + { + electronSocket.emit('autoUpdaterCheckForUpdatesComplete' + guid, updateCheckResult); + }).catch((error) => + { + electronSocket.emit('autoUpdaterCheckForUpdatesError' + guid, error); + }); }); + socket.on('autoUpdaterQuitAndInstall', async (isSilent, isForceRunAfter) => { autoUpdater.quitAndInstall(isSilent, isForceRunAfter); }); diff --git a/ElectronNET.Host/main.js b/ElectronNET.Host/main.js index d382fdf..2068e00 100644 --- a/ElectronNET.Host/main.js +++ b/ElectronNET.Host/main.js @@ -88,6 +88,7 @@ function startSplashScreen() { throw new Error(error.message); } + console.log("splashscreen ", dimensions.width, dimensions.height); splashScreen = new BrowserWindow({ width: dimensions.width, height: dimensions.height, @@ -144,6 +145,7 @@ function startSocketApiBridge(port) { // live reload watch happen. socket.on('disconnect', function (reason) { console.log('Got disconnect! Reason: ' + reason); + socket.removeAllListeners(); delete require.cache[require.resolve('./api/app')]; delete require.cache[require.resolve('./api/browserWindows')]; delete require.cache[require.resolve('./api/commandLine')]; @@ -232,11 +234,17 @@ function startAspCoreBackend(electronPort) { let binFilePath = path.join(currentBinPath, binaryFile); var options = { cwd: currentBinPath }; + console.log("Starting child process", binFilePath, parameters, options); apiProcess = cProcess(binFilePath, parameters, options); - + console.log("Started"); apiProcess.stdout.on('data', (data) => { console.log(`stdout: ${data.toString()}`); }); + apiProcess.on('exit', (code) => + { + console.log(`child process exit: ${code}`); + app.exit(code); + }); } } @@ -259,11 +267,17 @@ function startAspCoreBackendWithWatch(electronPort) { cwd: currentBinPath, env: process.env, }; + console.log("Starting child process", binFilePath, parameters, options); apiProcess = cProcess('dotnet', parameters, options); - + console.log("Started"); apiProcess.stdout.on('data', (data) => { console.log(`stdout: ${data.toString()}`); }); + apiProcess.on('exit', (code) => + { + console.log(`child process exit: ${code}`); + app.exit(code); + }); } }