diff --git a/ElectronNET.API/App.cs b/ElectronNET.API/App.cs index 0e13d05..274786c 100644 --- a/ElectronNET.API/App.cs +++ b/ElectronNET.API/App.cs @@ -581,7 +581,7 @@ namespace ElectronNET.API /// Options for the relaunch. public void Relaunch(RelaunchOptions relaunchOptions) { - BridgeConnector.EmitSync("appRelaunch", relaunchOptions); + BridgeConnector.EmitSync("appRelaunch", JObject.FromObject(relaunchOptions, _jsonSerializer)); } /// @@ -601,7 +601,7 @@ namespace ElectronNET.API /// public void Focus(FocusOptions focusOptions) { - BridgeConnector.Emit("appFocus", focusOptions); + BridgeConnector.Emit("appFocus", JObject.FromObject(focusOptions, _jsonSerializer)); } /// @@ -891,7 +891,7 @@ namespace ElectronNET.API /// Array of objects. /// The cancellation token. /// Whether the call succeeded. - public Task SetUserTasksAsync(UserTask[] userTasks, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appSetUserTasks", "appSetUserTasksCompleted", cancellationToken, userTasks); + public Task SetUserTasksAsync(UserTask[] userTasks, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appSetUserTasks", "appSetUserTasksCompleted", cancellationToken, JArray.FromObject(userTasks, _jsonSerializer)); /// /// Jump List settings for the application. @@ -918,7 +918,7 @@ namespace ElectronNET.API /// Array of objects. public void SetJumpList(JumpListCategory[] categories) { - BridgeConnector.Emit("appSetJumpList", categories); + BridgeConnector.Emit("appSetJumpList", JArray.FromObject(categories, _jsonSerializer)); } /// @@ -1054,7 +1054,7 @@ namespace ElectronNET.API /// /// The cancellation token. /// Result of import. Value of 0 indicates success. - public Task ImportCertificateAsync(ImportCertificateOptions options, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appImportCertificate", "appImportCertificateCompleted", cancellationToken, options); + public Task ImportCertificateAsync(ImportCertificateOptions options, CancellationToken cancellationToken = default) => BridgeConnector.OnResult("appImportCertificate", "appImportCertificateCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer)); /// /// Memory and cpu usage statistics of all the processes associated with the app. @@ -1120,7 +1120,7 @@ namespace ElectronNET.API /// The cancellation token. public Task GetLoginItemSettingsAsync(LoginItemSettingsOptions options, CancellationToken cancellationToken = default) => options is null ? BridgeConnector.OnResult("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken) - : BridgeConnector.OnResult("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken, options); + : BridgeConnector.OnResult("appGetLoginItemSettings", "appGetLoginItemSettingsCompleted", cancellationToken, JObject.FromObject(options, _jsonSerializer)); /// /// Set the app's login item settings. @@ -1130,7 +1130,7 @@ namespace ElectronNET.API /// public void SetLoginItemSettings(LoginSettings loginSettings) { - BridgeConnector.Emit("appSetLoginItemSettings", loginSettings); + BridgeConnector.Emit("appSetLoginItemSettings", JObject.FromObject(loginSettings, _jsonSerializer)); } /// @@ -1179,7 +1179,7 @@ namespace ElectronNET.API /// About panel options. public void SetAboutPanelOptions(AboutPanelOptions options) { - BridgeConnector.Emit("appSetAboutPanelOptions", options); + BridgeConnector.Emit("appSetAboutPanelOptions", JObject.FromObject(options, _jsonSerializer)); } /// @@ -1243,5 +1243,10 @@ namespace ElectronNET.API /// The event name /// The handler public void Once(string eventName, Action fn) => Events.Instance.Once(ModuleName, eventName, fn); + + private readonly JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; } } \ No newline at end of file diff --git a/ElectronNET.API/BrowserWindow.cs b/ElectronNET.API/BrowserWindow.cs index 8402d2f..0b8222c 100644 --- a/ElectronNET.API/BrowserWindow.cs +++ b/ElectronNET.API/BrowserWindow.cs @@ -1682,7 +1682,7 @@ namespace ElectronNET.API public void SetMenu(MenuItem[] menuItems) { menuItems.AddMenuItemsId(); - BridgeConnector.Emit("browserWindowSetMenu", Id, menuItems); + BridgeConnector.Emit("browserWindowSetMenu", Id, JArray.FromObject(menuItems, _jsonSerializer)); _items.AddRange(menuItems); BridgeConnector.Off("windowMenuItemClicked"); @@ -1788,7 +1788,7 @@ namespace ElectronNET.API }); thumbarButtons.AddThumbarButtonsId(); - BridgeConnector.Emit("browserWindowSetThumbarButtons", Id, thumbarButtons); + BridgeConnector.Emit("browserWindowSetThumbarButtons", Id, JArray.FromObject(thumbarButtons, _jsonSerializer)); _thumbarButtons.Clear(); _thumbarButtons.AddRange(thumbarButtons); @@ -2003,5 +2003,11 @@ namespace ElectronNET.API { BridgeConnector.Emit("browserWindow-setBrowserView", Id, browserView.Id); } + + private static readonly JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; } } diff --git a/ElectronNET.API/Dock.cs b/ElectronNET.API/Dock.cs index 9706341..9c00b2d 100644 --- a/ElectronNET.API/Dock.cs +++ b/ElectronNET.API/Dock.cs @@ -138,7 +138,7 @@ namespace ElectronNET.API public void SetMenu(MenuItem[] menuItems) { menuItems.AddMenuItemsId(); - BridgeConnector.Emit("dock-setMenu", menuItems); + BridgeConnector.Emit("dock-setMenu", JArray.FromObject(menuItems, _jsonSerializer)); _items.AddRange(menuItems); BridgeConnector.Off("dockMenuItemClicked"); @@ -146,7 +146,6 @@ namespace ElectronNET.API MenuItem menuItem = _items.GetMenuItem(id); menuItem?.Click(); }); - } /// @@ -163,5 +162,11 @@ namespace ElectronNET.API { BridgeConnector.Emit("dock-setIcon", image); } + + private static readonly JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; } } \ No newline at end of file diff --git a/ElectronNET.API/IpcMain.cs b/ElectronNET.API/IpcMain.cs index 1508078..eae3f3c 100644 --- a/ElectronNET.API/IpcMain.cs +++ b/ElectronNET.API/IpcMain.cs @@ -207,6 +207,27 @@ namespace ElectronNET.API } } + /// + /// Log a message to the console output pipe. This is used when running with "detachedProcess" : true on the electron.manifest.json, + /// as in that case we can't open pipes to read the console output from the child process anymore + /// + /// Message to log + public static void ConsoleLog(string text) + { + BridgeConnector.Emit("console-stdout", text); + } + + /// + /// Log a message to the console error pipe. This is used when running with "detachedProcess" : true on the electron.manifest.json, + /// as in that case we can't open pipes to read the console output from the child process anymore + /// + /// Message to log + + public static void ConsoleError(string text) + { + BridgeConnector.Emit("console-stderr", text); + } + private JsonSerializer _jsonSerializer = new JsonSerializer() { ContractResolver = new CamelCasePropertyNamesContractResolver(), diff --git a/ElectronNET.API/Menu.cs b/ElectronNET.API/Menu.cs index 76bba2e..aa98613 100644 --- a/ElectronNET.API/Menu.cs +++ b/ElectronNET.API/Menu.cs @@ -58,7 +58,7 @@ namespace ElectronNET.API menuItems.AddMenuItemsId(); menuItems.AddSubmenuTypes(); - BridgeConnector.Emit("menu-setApplicationMenu", menuItems); + BridgeConnector.Emit("menu-setApplicationMenu", JArray.FromObject(menuItems, _jsonSerializer)); _menuItems.AddRange(menuItems); BridgeConnector.Off("menuItemClicked"); @@ -87,7 +87,7 @@ namespace ElectronNET.API menuItems.AddMenuItemsId(); menuItems.AddSubmenuTypes(); - BridgeConnector.Emit("menu-setContextMenu", browserWindow.Id, menuItems); + BridgeConnector.Emit("menu-setContextMenu", browserWindow.Id, JArray.FromObject(menuItems, _jsonSerializer)); if (!_contextMenuItems.ContainsKey(browserWindow.Id)) { @@ -112,5 +112,11 @@ namespace ElectronNET.API { BridgeConnector.Emit("menu-contextMenuPopup", browserWindow.Id); } + + private JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; } } diff --git a/ElectronNET.API/Tray.cs b/ElectronNET.API/Tray.cs index fb5db5c..c37f5fe 100644 --- a/ElectronNET.API/Tray.cs +++ b/ElectronNET.API/Tray.cs @@ -242,7 +242,7 @@ namespace ElectronNET.API public void Show(string image, MenuItem[] menuItems) { menuItems.AddMenuItemsId(); - BridgeConnector.Emit("create-tray", image, menuItems); + BridgeConnector.Emit("create-tray", image, JArray.FromObject(menuItems, _jsonSerializer)); _items.Clear(); _items.AddRange(menuItems); @@ -324,33 +324,39 @@ namespace ElectronNET.API public Task IsDestroyedAsync() => BridgeConnector.OnResult("tray-isDestroyed", "tray-isDestroyedCompleted"); private const string ModuleName = "tray"; + /// /// Subscribe to an unmapped event on the module. /// /// The event name /// The handler - public void On(string eventName, Action fn) - => Events.Instance.On(ModuleName, eventName, fn); + public void On(string eventName, Action fn) => Events.Instance.On(ModuleName, eventName, fn); + /// /// Subscribe to an unmapped event on the module. /// /// The event name /// The handler - public void On(string eventName, Action fn) - => Events.Instance.On(ModuleName, eventName, fn); + public void On(string eventName, Action fn) => Events.Instance.On(ModuleName, eventName, fn); + /// /// Subscribe to an unmapped event on the module once. /// /// The event name /// The handler - public void Once(string eventName, Action fn) - => Events.Instance.Once(ModuleName, eventName, fn); + public void Once(string eventName, Action fn) => Events.Instance.Once(ModuleName, eventName, fn); + /// /// Subscribe to an unmapped event on the module once. /// /// The event name /// The handler - public void Once(string eventName, Action fn) - => Events.Instance.Once(ModuleName, eventName, fn); + public void Once(string eventName, Action fn) => Events.Instance.Once(ModuleName, eventName, fn); + + private JsonSerializer _jsonSerializer = new JsonSerializer() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; } } diff --git a/ElectronNET.API/WindowManager.cs b/ElectronNET.API/WindowManager.cs index cf91098..e8869d3 100644 --- a/ElectronNET.API/WindowManager.cs +++ b/ElectronNET.API/WindowManager.cs @@ -150,6 +150,7 @@ namespace ElectronNET.API ContractResolver = new CamelCasePropertyNamesContractResolver(), NullValueHandling = NullValueHandling.Ignore }; + BridgeConnector.Emit("createBrowserWindow", JObject.FromObject(options, ownjsonSerializer), loadUrl); } diff --git a/ElectronNET.CLI/ProcessHelper.cs b/ElectronNET.CLI/ProcessHelper.cs index 9e0048f..5f7d6cc 100644 --- a/ElectronNET.CLI/ProcessHelper.cs +++ b/ElectronNET.CLI/ProcessHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.InteropServices; @@ -6,6 +7,33 @@ namespace ElectronNET.CLI { public class ProcessHelper { + private static ConcurrentDictionary _activeProcess = new(); + + public static void KillActive() + { + foreach(var kv in _activeProcess) + { + if (!kv.Key.HasExited) + { + try + { + kv.Key.CloseMainWindow(); + } + catch + { + + } + try + { + kv.Key.Kill(true); + } + catch + { + + } + } + } + } public static int CmdExecute(string command, string workingDirectoryPath, bool output = true, bool waitForExit = true) { using (Process cmd = new Process()) @@ -43,7 +71,11 @@ namespace ElectronNET.CLI if (waitForExit) { + _activeProcess[cmd] = true; + cmd.WaitForExit(); + + _activeProcess.TryRemove(cmd, out _); } return cmd.ExitCode; diff --git a/ElectronNET.CLI/Program.cs b/ElectronNET.CLI/Program.cs index b35ef6c..891b15e 100644 --- a/ElectronNET.CLI/Program.cs +++ b/ElectronNET.CLI/Program.cs @@ -19,6 +19,12 @@ namespace ElectronNET.CLI Environment.Exit(-1); } + Console.CancelKeyPress += (s,e) => + { + ProcessHelper.KillActive(); + Environment.Exit(-1); + }; + ICommand command = null; switch (args[0]) diff --git a/ElectronNET.Host/main.js b/ElectronNET.Host/main.js index cdb900c..862871b 100644 --- a/ElectronNET.Host/main.js +++ b/ElectronNET.Host/main.js @@ -5,6 +5,7 @@ const path = require('path'); const cProcess = require('child_process').spawn; 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, autoUpdater; @@ -101,7 +102,16 @@ app.on('ready', () => { app.on('quit', async (event, exitCode) => { await server.close(); - apiProcess.kill(); + + var detachedProcess = false; + + if (manifestJsonFile.hasOwnProperty('detachedProcess')) { + detachedProcess = manifestJsonFile.detachedProcess; + } + + if (!detachedProcess) { + apiProcess.kill(); + } }); function isSplashScreenEnabled() { @@ -141,7 +151,7 @@ function startSplashScreen() { if (manifestJsonFile.splashscreen.hasOwnProperty('timeout')) { var timeout = manifestJsonFile.splashscreen.timeout; setTimeout((t) => { - if (splashScreen != null ) { + if (splashScreen) { splashScreen.hide(); } }, timeout); @@ -261,6 +271,14 @@ function startSocketApiBridge(port) { } }); + socket.on('console-stdout', (data) => { + console.log(`stdout: ${data.toString()}`); + }); + + socket.on('console-stderr', (data) => { + console.log(`stderr: ${data.toString()}`); + }); + try { const hostHookScriptFilePath = path.join(__dirname, 'ElectronHostHook', 'index.js'); @@ -296,7 +314,7 @@ function startAspCoreBackend(electronPort) { function startBackend(aspCoreBackendPort) { console.log('ASP.NET Core Port: ' + aspCoreBackendPort); loadURL = `http://localhost:${aspCoreBackendPort}`; - const parameters = [getEnvironmentParameter(), `/electronPort=${electronPort}`, `/electronWebPort=${aspCoreBackendPort}`]; + const parameters = [getEnvironmentParameter(), `/electronPort=${electronPort}`, `/electronWebPort=${aspCoreBackendPort}`, `/electronPID=${process.pid}`]; let binaryFile = manifestJsonFile.executable; const os = require('os'); @@ -304,17 +322,31 @@ function startAspCoreBackend(electronPort) { binaryFile = binaryFile + '.exe'; } + var detachedProcess = false; + var stdioopt = 'pipe'; + + if (manifestJsonFile.hasOwnProperty('detachedProcess')) { + detachedProcess = manifestJsonFile.detachedProcess; + if (detachedProcess) { + stdioopt = 'ignore'; + } + } + let binFilePath = path.join(currentBinPath, binaryFile); - var options = { cwd: currentBinPath }; + + var options = { cwd: currentBinPath, detached: detachedProcess, stdio: stdioopt }; + apiProcess = cProcess(binFilePath, parameters, options); - apiProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data.toString()}`); - }); + if (!detachedProcess) { + apiProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data.toString()}`); + }); - apiProcess.stderr.on('data', (data) => { - console.log(`stderr: ${data.toString()}`); - }); + apiProcess.stderr.on('data', (data) => { + console.log(`stderr: ${data.toString()}`); + }); + } apiProcess.on('close', (code) => { console.log(`ASP.NET Process exited with code ${code}`); @@ -323,6 +355,11 @@ function startAspCoreBackend(electronPort) { app.exit(code); } }); + + if (detachedProcess) { + console.log('Detached from ASP.NET process'); + apiProcess.unref(); + } } } @@ -339,21 +376,31 @@ function startAspCoreBackendWithWatch(electronPort) { function startBackend(aspCoreBackendPort) { console.log('ASP.NET Core Watch Port: ' + aspCoreBackendPort); loadURL = `http://localhost:${aspCoreBackendPort}`; - const parameters = ['watch', 'run', getEnvironmentParameter(), `/electronPort=${electronPort}`, `/electronWebPort=${aspCoreBackendPort}`]; + const parameters = ['watch', 'run', getEnvironmentParameter(), `/electronPort=${electronPort}`, `/electronWebPort=${aspCoreBackendPort}`, `/electronPID=${process.pid}`]; + + var detachedProcess = false; + var stdioopt = 'pipe'; + + if (manifestJsonFile.hasOwnProperty('detachedProcess')) { + detachedProcess = manifestJsonFile.detachedProcess; + if (detachedProcess) { + stdioopt = 'ignore'; + } + } + + var options = { cwd: currentBinPath, env: process.env, detached: detachedProcess, stdio: stdioopt }; - var options = { - cwd: currentBinPath, - env: process.env, - }; apiProcess = cProcess('dotnet', parameters, options); - apiProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data.toString()}`); - }); + if (!detachedProcess) { + apiProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data.toString()}`); + }); - apiProcess.stderr.on('data', (data) => { - console.log(`stderr: ${data.toString()}`); - }); + apiProcess.stderr.on('data', (data) => { + console.log(`stderr: ${data.toString()}`); + }); + } apiProcess.on('close', (code) => { console.log(`ASP.NET Process exited with code ${code}`); @@ -362,6 +409,11 @@ function startAspCoreBackendWithWatch(electronPort) { app.exit(code); } }); + + if (detachedProcess) { + console.log('Detached from ASP.NET process'); + apiProcess.unref(); + } } }