namespace ElectronNET.Runtime.Services.ElectronProcess { using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using ElectronNET.Common; using ElectronNET.Runtime.Data; /// /// Launches and manages the Electron app process. /// [Localizable(false)] internal class ElectronProcessActive : ElectronProcessBase { private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$"); private readonly bool isUnpackaged; private readonly string electronBinaryName; private readonly string extraArguments; private readonly int socketPort; private ProcessRunner process; /// Initializes a new instance of the class. /// The is debug. /// Name of the electron. /// The extraArguments. /// The socket port. public ElectronProcessActive(bool isUnpackaged, string electronBinaryName, string extraArguments, int socketPort) { this.isUnpackaged = isUnpackaged; this.electronBinaryName = electronBinaryName; this.extraArguments = extraArguments; this.socketPort = socketPort; } protected override async Task StartCore() { var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); string startCmd, args, workingDir; if (this.isUnpackaged) { this.CheckRuntimeIdentifier(); var electrondir = Path.Combine(dir.FullName, ".electron"); ProcessRunner chmodRunner = null; try { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var distFolder = Path.Combine(electrondir, "node_modules", "electron", "dist"); chmodRunner = new ProcessRunner("ElectronRunner-Chmod"); chmodRunner.Run("chmod", "-R +x " + distFolder, electrondir); await chmodRunner.WaitForExitAsync().ConfigureAwait(true); if (chmodRunner.LastExitCode != 0) { throw new Exception("Failed to set executable permissions on Electron dist folder."); } } } catch (Exception ex) { Console.Error.WriteLine("[StartCore]: Exception: " + chmodRunner?.StandardError); Console.Error.WriteLine("[StartCore]: Exception: " + chmodRunner?.StandardOutput); Console.Error.WriteLine("[StartCore]: Exception: " + ex); } startCmd = Path.Combine(electrondir, "node_modules", "electron", "dist", "electron"); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { startCmd = Path.Combine(electrondir, "node_modules", "electron", "dist", "Electron.app", "Contents", "MacOS", "Electron"); } args = $"main.js -unpackeddotnet --trace-warnings -electronforcedport={this.socketPort:D} " + this.extraArguments; workingDir = electrondir; } else { dir = dir.Parent!.Parent!; startCmd = Path.Combine(dir.FullName, this.electronBinaryName); args = $"-dotnetpacked -electronforcedport={this.socketPort:D} " + this.extraArguments; workingDir = dir.FullName; } // We don't await this in order to let the state transition to "Starting" Task.Run(async () => await this.StartInternal(startCmd, args, workingDir).ConfigureAwait(false)); } private void CheckRuntimeIdentifier() { var buildInfoRid = ElectronNetRuntime.BuildInfo.RuntimeIdentifier; if (string.IsNullOrEmpty(buildInfoRid)) { return; } var osPart = buildInfoRid.Split('-').First(); var mismatch = false; switch (osPart) { case "win": if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { mismatch = true; } break; case "linux": if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { mismatch = true; } break; case "osx": if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { mismatch = true; } break; case "freebsd": if (!RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) { mismatch = true; } break; } if (mismatch) { throw new PlatformNotSupportedException($"This Electron.NET application was built for '{buildInfoRid}'. It cannot run on this platform."); } } protected override Task StopCore() { this.process.Cancel(); return Task.CompletedTask; } private async Task StartInternal(string startCmd, string args, string directoriy) { var tcs = new TaskCompletionSource(); using var cts = new CancellationTokenSource(2 * 60_000); // cancel after 2 minutes using var _ = cts.Token.Register(() => { // Time is over - let's kill the process and move on this.process.Cancel(); // We don't want to raise exceptions here - just pass the barrier tcs.SetResult(); }); void Read_SocketIO_Parameters(object sender, string line) { // Look for "Electron Socket: listening on port %s at ..." var match = extractor.Match(line); if (match?.Success ?? false) { var port = int.Parse(match.Groups[1].Value); var token = match.Groups[2].Value; this.process.LineReceived -= Read_SocketIO_Parameters; ElectronNetRuntime.ElectronAuthToken = token; ElectronNetRuntime.ElectronSocketPort = port; tcs.SetResult(); } } void Monitor_SocketIO_Failure(object sender, EventArgs e) { // We don't want to raise exceptions here - just pass the barrier if (tcs.Task.IsCompleted) { this.Process_Exited(sender, e); } else { tcs.SetResult(); } } try { Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd); Console.Error.WriteLine("[StartInternal]: args: {0}", args); this.process = new ProcessRunner("ElectronRunner"); this.process.ProcessExited += Monitor_SocketIO_Failure; this.process.LineReceived += Read_SocketIO_Parameters; this.process.Run(startCmd, args, directoriy); await tcs.Task.ConfigureAwait(false); Console.Error.WriteLine("[StartInternal]: after run:"); if (!this.process.IsRunning) { Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardError); Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput); Task.Run(() => this.TransitionState(LifetimeState.Stopped)); } else { this.TransitionState(LifetimeState.Ready); } } catch (Exception ex) { Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardError); Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardOutput); Console.Error.WriteLine("[StartInternal]: Exception: " + ex); throw; } } private void Process_Exited(object sender, EventArgs e) { this.TransitionState(LifetimeState.Stopped); } } }