Add experimental mode to start electron from C# (for development purposes)

This commit is contained in:
theolivenbaum
2022-07-14 16:56:50 +02:00
parent e2615a8dc1
commit 90b4a287d9
8 changed files with 288 additions and 45 deletions

View File

@@ -476,6 +476,13 @@ namespace ElectronNET.API
Log("ElectronNET socket {1} failed to connect {0}", ex, socket.Id);
};
socket.OnReconnectFailed += (_, ex) =>
{
_connectedSocketEvent.Reset();
Log("ElectronNET socket {1} failed to reconnect {0}", ex, socket.Id);
};
socket.OnReconnected += (_, __) =>
{
_connectedSocketEvent.Set();
@@ -490,8 +497,8 @@ namespace ElectronNET.API
socket.OnError += (_, msg) =>
{
//_connectedSocketEvent.Reset();
Log("ElectronNET socket {1} error: {0}...", msg, socket.Id);
//_connectedSocketEvent.Reset();
Log("ElectronNET socket {1} error: {0}...", msg, socket.Id);
};
_socket = socket;
@@ -530,7 +537,7 @@ namespace ElectronNET.API
}
internal static ILogger<App> Logger { private get; set; }
internal static string AuthKey { private get; set; }
internal static string AuthKey { get; set; } = null;
private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
{

View File

@@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
namespace ElectronNET.API
{
public static partial class Electron
{
/// <summary>
/// Experimental code, use with care
/// </summary>
public static class Experimental
{
/// <summary>
/// Starts electron from C#, use during development to avoid having to fully publish / build your app on every compile cycle
/// You will need to run the CLI at least once (and once per update) to bootstrap all required files
/// </summary>
/// <param name="webPort"></param>
/// <param name="projectPath"></param>
/// <param name="extraElectronArguments"></param>
/// <param name="clearCache"></param>
/// <exception cref="DirectoryNotFoundException"></exception>
/// <exception cref="Exception"></exception>
public static async Task StartElectronForDevelopment(int webPort, string projectPath = null, string[] extraElectronArguments = null, bool clearCache = false)
{
string aspCoreProjectPath;
if (!string.IsNullOrEmpty(projectPath))
{
if (Directory.Exists(projectPath))
{
aspCoreProjectPath = projectPath;
}
else
{
throw new DirectoryNotFoundException(projectPath);
}
}
else
{
aspCoreProjectPath = Directory.GetCurrentDirectory();
}
string tempPath = Path.Combine(aspCoreProjectPath, "obj", "Host");
if (!Directory.Exists(tempPath))
{
Directory.CreateDirectory(tempPath);
}
var mainFileJs = Path.Combine(tempPath, "main.js");
if (!File.Exists(mainFileJs))
{
throw new Exception("You need to run once the electronize-h5 start command to bootstrap the necessary files");
}
var nodeModulesDirPath = Path.Combine(tempPath, "node_modules");
bool runNpmInstall = false;
if (!Directory.Exists(nodeModulesDirPath))
{
runNpmInstall = true;
}
var packagesJson = Path.Combine(tempPath, "package.json");
var packagesPrevious = Path.Combine(tempPath, "package.json.previous");
if (!runNpmInstall)
{
if (File.Exists(packagesPrevious))
{
if (File.ReadAllText(packagesPrevious) != File.ReadAllText(packagesJson))
{
runNpmInstall = true;
}
}
else
{
runNpmInstall = true;
}
}
if (runNpmInstall)
{
throw new Exception("You need to run once the electronize-h5 start command to bootstrap the necessary files");
}
string arguments = "";
if (extraElectronArguments is object)
{
arguments = string.Join(' ', extraElectronArguments);
}
if (clearCache)
{
arguments += " --clear-cache=true";
}
BridgeConnector.AuthKey = Guid.NewGuid().ToString().Replace("-", "");
var socketPort = FreeTcpPort();
arguments += $" --development=true --devauth={BridgeConnector.AuthKey} --devport={socketPort}";
string path = Path.Combine(tempPath, "node_modules", ".bin");
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
ProcessHelper.Execute(@"electron.cmd ""..\..\main.js"" " + arguments, path);
}
else
{
ProcessHelper.Execute(@"./electron ""../../main.js"" " + arguments, path);
}
BridgeSettings.InitializePorts(socketPort, webPort);
await Task.Delay(500);
}
/// <summary>
/// Return a free local TCP port
/// </summary>
/// <returns></returns>
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
}
}
}

View File

@@ -1,13 +1,14 @@
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning;
using System;
using System.Collections.Generic;
namespace ElectronNET.API
{
/// <summary>
/// The Electron.NET API
/// </summary>
public static class Electron
public static partial class Electron
{
private static ILoggerFactory loggerFactory;
@@ -17,7 +18,13 @@ namespace ElectronNET.API
/// <exception cref="Exception"></exception>
public static void ReadAuth()
{
if (!string.IsNullOrEmpty(BridgeConnector.AuthKey))
{
throw new Exception($"Don't call ReadAuth twice or from with {nameof(Experimental)}.{nameof(Experimental.StartElectronForDevelopment)}");
}
var line = Console.ReadLine();
if(line.StartsWith("Auth="))
{
BridgeConnector.AuthKey = line.Substring("Auth=".Length);

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ElectronNET.API
{
internal class ProcessHelper
{
public static void Execute(string command, string workingDirectoryPath)
{
using (Process cmd = new Process())
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
cmd.StartInfo = new ProcessStartInfo("cmd.exe", "/c " + command);
}
else
{
// works for OSX and Linux (at least on Ubuntu)
var escapedArgs = command.Replace("\"", "\\\"");
cmd.StartInfo = new ProcessStartInfo("bash", $"-c \"{escapedArgs}\"");
}
cmd.StartInfo.RedirectStandardInput = false;
cmd.StartInfo.RedirectStandardOutput = false;
cmd.StartInfo.RedirectStandardError = false;
cmd.StartInfo.CreateNoWindow = true;
cmd.StartInfo.UseShellExecute = false;
cmd.StartInfo.WorkingDirectory = workingDirectoryPath;
cmd.Start();
}
}
}
}

View File

@@ -9,7 +9,7 @@ const crypto = require('crypto');
fixPath(); //For macOS and Linux packaged-apps, the path variable might be missing
const auth = crypto.randomBytes(32).toString('hex');
let auth = crypto.randomBytes(32).toString('hex');
let io, server, browserWindows, ipc, apiProcess, loadURL;
let appApi, menu, dialogApi, notification, tray, webContents;
@@ -25,6 +25,8 @@ let ignoreApiProcessClosed = false;
let manifestJsonFileName = 'electron.manifest.json';
let watchable = false;
let development = false;
if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
};
@@ -33,6 +35,10 @@ if (app.commandLine.hasSwitch('watch')) {
watchable = true;
};
if (app.commandLine.hasSwitch('development')) {
development = true;
};
let currentBinPath = path.join(__dirname.replace('app.asar', ''), 'bin');
let manifestJsonFilePath = path.join(currentBinPath, manifestJsonFileName);
@@ -42,6 +48,10 @@ if (watchable) {
manifestJsonFilePath = path.join(currentBinPath, manifestJsonFileName);
}
if (development) {
auth = app.commandLine.getSwitchValue("devauth");
}
// handle macOS events for opening the app with a file, etc
app.on('will-finish-launching', () => {
app.on('open-file', (evt, file) => {
@@ -136,19 +146,27 @@ app.on('ready', () => {
if (isSplashScreenEnabled()) {
startSplashScreen();
}
// Added default port as configurable for port restricted environments.
let defaultElectronPort = 8000;
if (manifestJsonFile.electronPort) {
defaultElectronPort = (manifestJsonFile.electronPort)
if (defaultElectronPort == 'random') {
defaultElectronPort = (Math.floor(Math.random() * 2000 + 8000)); //Use random port to reduce risk of race conditions between when we find a free port here, and when the app locks on the port
}
}
// hostname needs to be localhost, otherwise Windows Firewall will be triggered.
portscanner.findAPortNotInUse(defaultElectronPort, 65535, 'localhost', function (error, port) {
if (development) {
let port = parseInt(app.commandLine.getSwitchValue('devport'));
try { console.log('Electron Socket IO Port: ' + port); } catch { }
startSocketApiBridge(port);
});
} else {
// Added default port as configurable for port restricted environments.
let defaultElectronPort = 8000;
if (manifestJsonFile.electronPort) {
defaultElectronPort = (manifestJsonFile.electronPort)
if (defaultElectronPort == 'random') {
defaultElectronPort = (Math.floor(Math.random() * 2000 + 8000)); //Use random port to reduce risk of race conditions between when we find a free port here, and when the app locks on the port
}
}
// hostname needs to be localhost, otherwise Windows Firewall will be triggered.
portscanner.findAPortNotInUse(defaultElectronPort, 65535, 'localhost', function (error, port) {
try { console.log('Electron Socket IO Port: ' + port); } catch { }
startSocketApiBridge(port);
});
}
});
app.on('quit', async (event, exitCode) => {
@@ -268,8 +286,11 @@ function startSocketApiBridge(port) {
server.on('listening', function () {
try { console.log('Electron Socket started on port %s at %s', server.address().port, server.address().address); } catch { }
// Now that socket connection is established, we can guarantee port will not be open for portscanner
if (watchable) {
startAspCoreBackendWithWatch(port);
} else if (development) {
//Nothing else to do
} else {
startAspCoreBackend(port);
}
@@ -283,7 +304,8 @@ function startSocketApiBridge(port) {
let checkReconnectTimeout = 0;
// @ts-ignore
io.on('connection', (socket) => {
try { console.log('Socket ' + socket.id + ' connected from .NET'); } catch { }
isConnected = true;
clearTimeout(checkReconnectTimeout);
@@ -311,7 +333,6 @@ function startSocketApiBridge(port) {
});
socket.on("auth", function (authKey) {
if (authKey != auth) {
throw new Error("Invalid auth key");
}

View File

@@ -2,24 +2,49 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
namespace ElectronNET.WebApp
{
public class Program
{
public static void Main(string[] args)
public static async Task Main(string[] args)
{
IWebHostBuilder builder;
#if DEBUG
var webPort = Electron.Experimental.FreeTcpPort();
await Electron.Experimental.StartElectronForDevelopment(webPort);
builder = CreateWebHostBuilder(args);
// check for the content folder if its exists in base director otherwise no need to include
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
{
builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory);
}
builder.UseUrls("http://localhost:" + webPort);
#else
builder = CreateWebHostBuilder(args);
Debugger.Launch();
Electron.ReadAuth();
CreateWebHostBuilder(args).Build().Run();
builder.UseElectron(args);
#endif
await builder.Build().RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) => { logging.AddConsole(); })
.UseElectron(args)
.UseStartup<Startup>();
}
}

View File

@@ -1,4 +1,29 @@
{
"profiles": {
"WSL": {
"commandName": "WSL2",
"launchUrl": "http://localhost:50395/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:50395/"
},
"distributionName": ""
},
"run with electronize": {
"commandName": "Executable",
"executablePath": "$(SolutionDir)ElectronNET.CLI\\bin\\Debug\\net6.0\\dotnet-electronize-h5.exe",
"commandLineArgs": "start /from-build-output $(SolutionDir)ElectronNET.WebApp\\bin\\$(Configuration)\\net6.0",
"workingDirectory": "$(SolutionDir)ElectronNET.WebApp"
},
"run from csharp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:50395/",
"hotReloadEnabled": false
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
@@ -6,27 +31,5 @@
"applicationUrl": "http://localhost:50394/",
"sslPort": 0
}
},
"profiles": {
"Electron.NET App": {
"commandName": "Executable",
"executablePath": "$(SolutionDir)ElectronNET.CLI\\bin\\Debug\\net5.0\\dotnet-electronize-h5.exe",
"commandLineArgs": "start /from-build-output $(SolutionDir)ElectronNET.WebApp\\bin\\$(Configuration)\\net5.0",
"workingDirectory": "$(SolutionDir)ElectronNET.WebApp"
},
"IIS Express": {
"commandName": "IISExpress",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ElectronNET.WebApp": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:50395/"
}
}
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
namespace ElectronNET.WebApp
{
@@ -43,11 +44,11 @@ namespace ElectronNET.WebApp
if (HybridSupport.IsElectronActive)
{
ElectronBootstrap();
Task.Run(() => ElectronBootstrap());
}
}
public async void ElectronBootstrap()
public async Task ElectronBootstrap()
{
//AddDevelopmentTests();