using MsmhToolsClass; using MsmhToolsClass.MsmhAgnosticServer; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.Net; using System.Reflection; namespace SDCAgnosticServer; public static partial class Program { // Defaults AgnosticSettings private const int DefaultPort = 8080; private const int DefaultPortMin = 53; private const int DefaultPortMax = 65535; private const AgnosticSettings.WorkingMode DefaultWorkingMode = AgnosticSettings.WorkingMode.DnsAndProxy; private const int DefaultMaxRequests = 5000; private const int DefaultMaxRequestsMin = 20; private const int DefaultMaxRequestsMax = int.MaxValue; private const int DefaultDnsTimeoutSec = 5; private const int DefaultDnsTimeoutSecMin = 3; private const int DefaultDnsTimeoutSecMax = 10; private const int DefaultProxyTimeoutSec = 40; private const int DefaultProxyTimeoutSecMin = 0; private const int DefaultProxyTimeoutSecMax = 600; private const float DefaultKillOnCpuUsage = 40; private const int DefaultKillOnCpuUsageMin = 10; private const int DefaultKillOnCpuUsageMax = 95; private const bool DefaultBlockPort80 = true; private const bool DefaultAllowInsecure = false; private static readonly IPAddress DefaultBootstrapIp = IPAddress.None; private const int DefaultBootstrapPort = 53; private const bool DefaultApplyUpstreamOnlyToBlockedIps = true; // Defaults SSL AgnosticSettings private const bool DefaultSSLEnable = false; private const bool DefaultSSLChangeSni = false; // Defaults Fragment private const int DefaultFragmentBeforeSniChunks = 50; private const int DefaultFragmentBeforeSniChunksMin = 1; private const int DefaultFragmentBeforeSniChunksMax = 500; private static readonly string DefaultFragmentChunkModeStr = Key.Programs.Fragment.Mode.Program.ChunkMode.SNI; private const AgnosticProgram.Fragment.ChunkMode DefaultFragmentChunkMode = AgnosticProgram.Fragment.ChunkMode.SNI; private const int DefaultFragmentSniChunks = 5; private const int DefaultFragmentSniChunksMin = 1; private const int DefaultFragmentSniChunksMax = 500; private const int DefaultFragmentAntiPatternOffset = 2; private const int DefaultFragmentAntiPatternOffsetMin = 0; private const int DefaultFragmentAntiPatternOffsetMax = 50; private const int DefaultFragmentFragmentDelay = 1; private const int DefaultFragmentFragmentDelayMin = 0; private const int DefaultFragmentFragmentDelayMax = 500; private static string Profile = string.Empty; private static readonly List<string> LoadCommands = new(); private static readonly Stopwatch StopWatchShowRequests = new(); private static readonly Stopwatch StopWatchShowChunkDetails = new(); private static bool WriteRequestsToLog { get; set; } = false; private static bool WriteFragmentDetailsToLog { get; set; } = false; private static int ParentPID { get; set; } = -1; private static ConcurrentDictionary<uint, string> WaitingInputCommands { get; set; } = new(); public static async void ProxyServer_OnRequestReceived(object? sender, EventArgs e) { if (WriteRequestsToLog) if (sender is string msg) { if (!StopWatchShowRequests.IsRunning) StopWatchShowRequests.Start(); if (StopWatchShowRequests.ElapsedMilliseconds > 2) { await WriteToStderrAsync(msg, ConsoleColor.DarkGray, false); StopWatchShowRequests.Restart(); } } } public static async void FragmentStaticProgram_OnChunkDetailsReceived(object? sender, EventArgs e) { if (WriteFragmentDetailsToLog) if (sender is string msg) { if (!StopWatchShowChunkDetails.IsRunning) StopWatchShowChunkDetails.Start(); if (StopWatchShowChunkDetails.ElapsedMilliseconds > 50) { await WriteToStderrAsync(msg, ConsoleColor.DarkCyan, false); StopWatchShowChunkDetails.Restart(); } } } /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static async Task Main() { // Title string title = $"Msmh Agnostic Server v{Assembly.GetExecutingAssembly().GetName().Version}"; if (OperatingSystem.IsWindows()) Console.Title = title; // Invariant Culture Info.SetCulture(CultureInfo.InvariantCulture); // First Help await WriteToStdoutAsync(title); await WriteToStdoutAsync("Type \"Help\" To Get Help."); // Exit When Parent Terminated ExitAuto(); // Execute Wainting Commands ExecuteCommands(); // Read Commands await ReadCommandsAsync(); } private static async void ExitAuto() { await Task.Run(async () => { while (true) { await Task.Delay(2000); if (ParentPID == -1 || ParentPID == 0) continue; bool isParentExist = ProcessManager.FindProcessByPID(ParentPID); if (!isParentExist) { Environment.Exit(0); await ProcessManager.KillProcessByPidAsync(Environment.ProcessId); } } }); } private static async Task ReadCommandsAsync() { await Task.Run(() => { uint n = 0; while (true) { try { string? input = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(input)) { n++; WaitingInputCommands.TryAdd(n, input); } } catch (Exception ex) { Debug.WriteLine("ReadCommandsAsync: " + ex.Message); } } }); } private static async void ExecuteCommands() { await Task.Run(async () => { while (true) { try { if (WaitingInputCommands.IsEmpty) await Task.Delay(50); else { while (true) { int n1 = WaitingInputCommands.Count; await Task.Delay(200); int n2 = WaitingInputCommands.Count; if (n1 == n2) break; } List<KeyValuePair<uint, string>> commandsList = WaitingInputCommands.ToList(); var sortedCommandsList = commandsList.OrderBy(x => x.Key); foreach (KeyValuePair<uint, string> command in sortedCommandsList) { await ExecuteCommandsAsync(command.Value); WaitingInputCommands.TryRemove(command.Key, out _); //Debug.WriteLine($"{command.Key}"); await Task.Delay(25); } } } catch (Exception ex) { Debug.WriteLine("ExecuteCommands: " + ex.Message); } } }); } private static async Task MainDetailsAsync(string profileName) { try { // Main Details - Machine Read bool isMatch = false; foreach (ServerProfile sf in ServerProfiles) { if (sf.AgnosticServer != null && sf.Settings != null && !string.IsNullOrEmpty(sf.Name)) { if (profileName.Equals(sf.Name)) { string mainDetails = $"details|{sf.AgnosticServer.IsRunning}|"; // 1 mainDetails += $"{sf.AgnosticServer.ListeningPort}|"; // 2 mainDetails += $"{sf.AgnosticServer.ActiveProxyTunnels}|"; // 3 mainDetails += $"{sf.AgnosticServer.MaxRequests}|"; // 4 mainDetails += $"{sf.AgnosticServer.SettingsSSL_.EnableSSL}|"; // 5 mainDetails += $"{sf.AgnosticServer.SettingsSSL_.ChangeSni}|"; // 6 mainDetails += $"{sf.AgnosticServer.IsFragmentActive}|"; // 7 mainDetails += $"{sf.AgnosticServer.FragmentProgram.FragmentMode}|"; // 8 mainDetails += $"{sf.AgnosticServer.RulesProgram.RulesMode}|"; // 9 await WriteToStdoutAsync(mainDetails); isMatch = true; break; } } } if (!isMatch) await WriteToStdoutAsync($"Wrong Profile Name", ConsoleColor.DarkRed); } catch (Exception ex) { Debug.WriteLine("MainDetailsAsync: " + ex.Message); } } private static async Task ShowProfileMsgAsync(bool save = true) { try { Profile = Profile.Trim(); if (!string.IsNullOrEmpty(Profile)) { await WriteToStdoutAsync($"{Environment.NewLine}{nameof(Profile)} Set To {Profile}", ConsoleColor.Cyan, true, Key.Common.Profile); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile }; AddProfile(sf); } } } catch (Exception ex) { Debug.WriteLine("ShowProfileMsgAsync: " + ex.Message); } } private static async Task ShowParentProcessMsgAsync() { string pid = ParentPID != -1 && ParentPID != 0 ? $"{ParentPID}" : "No Parent"; string msg = $"\nParent Process ID: {pid}"; await WriteToStdoutAsync(msg, ConsoleColor.Green, true, Key.ParentProcess.Name); } private static async Task ShowSettingsMsgAsync(AgnosticSettings settings, bool save = true) { try { string msg = "\nSettings:"; msg += $"\nPort: {settings.ListenerPort}"; msg += $"\nWorking Mode: {settings.Working_Mode}"; msg += $"\nMax Requests: {settings.MaxRequests}"; msg += $"\nDNS Timeout: {settings.DnsTimeoutSec} Seconds"; msg += $"\nProxy Timeout: {settings.ProxyTimeoutSec} Seconds"; msg += $"\nKill On Cpu Usage: {settings.KillOnCpuUsage}%"; msg += $"\nBlock Port 80: {settings.BlockPort80}"; msg += $"\nAllow Insecure: {settings.AllowInsecure}"; msg += $"\nDNS Servers Count: {settings.DNSs.Count}"; if (!string.IsNullOrEmpty(settings.CloudflareCleanIP)) msg += $"\nCloudflare Clean IP: {settings.CloudflareCleanIP}"; msg += $"\nBootstrap IP Address: {settings.BootstrapIpAddress}"; msg += $"\nBootstrap Port: {settings.BootstrapPort}"; if (!string.IsNullOrEmpty(settings.UpstreamProxyScheme)) { msg += $"\nUpstream Proxy Scheme: {settings.UpstreamProxyScheme}"; if (!string.IsNullOrEmpty(settings.UpstreamProxyUser)) { msg += $"\nUpstream Proxy User: {settings.UpstreamProxyUser}"; if (!string.IsNullOrEmpty(settings.UpstreamProxyPass)) msg += $"\nUpstream Proxy Pass: {settings.UpstreamProxyPass}"; } msg += $"\nApply Upstream Only To Blocked IPs: {settings.ApplyUpstreamOnlyToBlockedIps}"; } await WriteToStdoutAsync(msg, ConsoleColor.Blue, true, Key.Setting.Name); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile, Settings = settings }; AddProfile(sf); } } catch (Exception ex) { Debug.WriteLine("ShowSettingsMsgAsync: " + ex.Message); } } private static async Task ShowSettingsSSLMsgAsync(AgnosticSettingsSSL settingsSSL, bool save = true) { try { string msg = "\nSSL Settings:"; msg += $"\nEnabled: {settingsSSL.EnableSSL}"; if (settingsSSL.EnableSSL) { if (!string.IsNullOrEmpty(settingsSSL.RootCA_Path)) { msg += $"\nRootCA_Path:"; msg += $"\n{settingsSSL.RootCA_Path}"; } if (!string.IsNullOrEmpty(settingsSSL.RootCA_KeyPath)) { msg += $"\nRootCA_KeyPath:"; msg += $"\n{settingsSSL.RootCA_KeyPath}"; } if (!string.IsNullOrEmpty(settingsSSL.Cert_Path)) { msg += $"\nCert_Path:"; msg += $"\n{settingsSSL.Cert_Path}"; } if (!string.IsNullOrEmpty(settingsSSL.Cert_KeyPath)) { msg += $"\nCert_KeyPath:"; msg += $"\n{settingsSSL.Cert_KeyPath}"; } msg += $"\nChange SNI: {settingsSSL.ChangeSni}"; if (!string.IsNullOrEmpty(settingsSSL.DefaultSni) && !string.IsNullOrWhiteSpace(settingsSSL.DefaultSni)) msg += $"\nDefault SNI: {settingsSSL.DefaultSni}"; else msg += "\nDefault SNI: Empty (Original SNI Will Be Used)"; } await WriteToStdoutAsync(msg, ConsoleColor.Blue, true, Key.SSLSetting.Name); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile, SettingsSSL = settingsSSL }; AddProfile(sf); } } catch (Exception ex) { Debug.WriteLine("ShowSettingsSSLMsgAsync: " + ex.Message); } } private static async Task ShowFragmentMsgAsync(AgnosticProgram.Fragment fragmentProgram, bool save = true) { try { string result = $"\n{Key.Programs.Fragment.Name} Mode: {fragmentProgram.FragmentMode}"; if (fragmentProgram.FragmentMode == AgnosticProgram.Fragment.Mode.Program) { result += $"\nBefore Sni Chunks: {fragmentProgram.BeforeSniChunks}"; result += $"\nChunks Mode: {fragmentProgram.DPIChunkMode}"; result += $"\n\"{fragmentProgram.DPIChunkMode}\" Chunks: {fragmentProgram.SniChunks}"; result += $"\nAnti-Pattern Offset: {fragmentProgram.AntiPatternOffset} Chunks"; result += $"\nFragment Delay: {fragmentProgram.FragmentDelay} ms"; } await WriteToStdoutAsync(result, ConsoleColor.Green, true, Key.Programs.Fragment.Name); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile, Fragment = fragmentProgram }; AddProfile(sf); } } catch (Exception ex) { Debug.WriteLine("ShowFragmentMsgAsync: " + ex.Message); } } private static async Task ShowRulesMsgAsync(AgnosticProgram.Rules rulesProgram, bool save = true) { try { string result = $"\n{Key.Programs.Rules.Name} Mode: {rulesProgram.RulesMode}"; if (rulesProgram.RulesMode != AgnosticProgram.Rules.Mode.Disable) result += $"\nRules:\n{rulesProgram.PathOrText}"; await WriteToStdoutAsync(result, ConsoleColor.Green, true, Key.Programs.Rules.Name); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile, Rules = rulesProgram }; AddProfile(sf); } } catch (Exception ex) { Debug.WriteLine("ShowRulesMsgAsync: " + ex.Message); } } private static async Task ShowDnsLimitMsgAsync(AgnosticProgram.DnsLimit dnsLimitProgram, bool save = true) { try { string result = $"\n{Key.Programs.DnsLimit.Name} Enable: {dnsLimitProgram.EnableDnsLimit}"; if (dnsLimitProgram.EnableDnsLimit) { result += $"\nDisable Plain DNS: {dnsLimitProgram.DisablePlainDns}"; result += $"\n{Key.Programs.DnsLimit.DoHPathLimitMode.Name} Mode: {dnsLimitProgram.LimitDoHMode}"; if (dnsLimitProgram.LimitDoHMode != AgnosticProgram.DnsLimit.LimitDoHPathsMode.Disable) result += $"\nDoH Paths:\n{dnsLimitProgram.PathOrText}"; } await WriteToStdoutAsync(result, ConsoleColor.Green, true, Key.Programs.DnsLimit.Name); if (save) { // Add Or Update Server Profile ServerProfile sf = new() { Name = Profile, DnsLimit = dnsLimitProgram }; AddProfile(sf); } } catch (Exception ex) { Debug.WriteLine("ShowDnsLimitMsgAsync: " + ex.Message); } } private static async Task ShowStatusAsync() { foreach (ServerProfile sf in ServerProfiles) { if (sf.AgnosticServer != null && sf.Settings != null && !string.IsNullOrEmpty(sf.Name)) { string result = $"\nServer {sf.Name} Running: {sf.AgnosticServer.IsRunning}"; await WriteToStdoutAsync(result, ConsoleColor.Blue); if (sf.AgnosticServer.IsRunning) { string addressMsg = string.Empty; bool isIPv4Supported = NetworkTool.IsIPv4Supported(); bool isIPv6Supported = NetworkTool.IsIPv6Supported(); if (isIPv4Supported || isIPv6Supported) addressMsg += $"\nListning On:"; if (isIPv4Supported) { addressMsg += $"\n{IPAddress.Loopback}:{sf.Settings.ListenerPort}"; IPAddress? localIPv4 = NetworkTool.GetLocalIPv4(); if (localIPv4 != null) addressMsg += $"\n{localIPv4}:{sf.Settings.ListenerPort}"; } if (isIPv6Supported) { addressMsg += $"\n{IPAddress.IPv6Loopback}:{sf.Settings.ListenerPort}"; IPAddress? localIPv6 = NetworkTool.GetLocalIPv6(); if (localIPv6 != null) addressMsg += $"\n{localIPv6}:{sf.Settings.ListenerPort}"; } await WriteToStdoutAsync(addressMsg, ConsoleColor.Blue); } if (sf.Settings != null) await ShowSettingsMsgAsync(sf.Settings, false); if (sf.SettingsSSL != null) await ShowSettingsSSLMsgAsync(sf.SettingsSSL, false); if (sf.Fragment != null) await ShowFragmentMsgAsync(sf.Fragment, false); if (sf.Rules != null) await ShowRulesMsgAsync(sf.Rules, false); if (sf.DnsLimit != null) await ShowDnsLimitMsgAsync(sf.DnsLimit, false); } } await ShowParentProcessMsgAsync(); } private static async Task FlushDnsAsync() { try { foreach (ServerProfile sf in ServerProfiles) { if (sf.AgnosticServer != null && sf.AgnosticServer.IsRunning) { sf.AgnosticServer.FlushDnsCache(); } } if (OperatingSystem.IsWindows()) await ProcessManager.ExecuteAsync("ipconfig", null, "/flushdns", true, true); await WriteToStdoutAsync("DNS Flushed", ConsoleColor.Green); } catch (Exception ex) { Debug.WriteLine("FlushDnsAsync: " + ex.Message); } } private static async Task SaveCommandsToFileAsync() { try { string? p = ConsoleTools.GetCommandsPath(); if (!string.IsNullOrEmpty(p)) { List<string> commands = new(); foreach (ServerProfile sf in ServerProfiles) { if (sf.AgnosticServer != null && !string.IsNullOrEmpty(sf.Name)) { string baseCmd = nameof(Profile); string cmd = $"{baseCmd} {sf.Name}"; commands.Add(cmd); if (sf.Settings != null) { baseCmd = Key.Setting.Name; cmd = $"{baseCmd} -{Key.Setting.Port}={sf.Settings.ListenerPort}"; cmd += $" -{Key.Setting.WorkingMode}={sf.Settings.Working_Mode}"; cmd += $" -{Key.Setting.MaxRequests}={sf.Settings.MaxRequests}"; cmd += $" -{Key.Setting.DnsTimeoutSec}={sf.Settings.DnsTimeoutSec}"; cmd += $" -{Key.Setting.ProxyTimeoutSec}={sf.Settings.ProxyTimeoutSec}"; cmd += $" -{Key.Setting.KillOnCpuUsage}={sf.Settings.KillOnCpuUsage}"; cmd += $" -{Key.Setting.BlockPort80}={sf.Settings.BlockPort80}"; cmd += $" -{Key.Setting.AllowInsecure}={sf.Settings.AllowInsecure}"; if (sf.Settings.DNSs.Any()) { cmd += $" -{Key.Setting.DNSs}="; foreach (string dns in sf.Settings.DNSs) cmd += $"{dns},"; if (cmd.EndsWith(',')) cmd = cmd.TrimEnd(','); } if (!string.IsNullOrEmpty(sf.Settings.CloudflareCleanIP)) cmd += $" -{Key.Setting.CfCleanIP}={sf.Settings.CloudflareCleanIP}"; cmd += $" -{Key.Setting.BootstrapIp}={sf.Settings.BootstrapIpAddress}"; cmd += $" -{Key.Setting.BootstrapPort}={sf.Settings.BootstrapPort}"; if (!string.IsNullOrEmpty(sf.Settings.UpstreamProxyScheme)) { cmd += $" -{Key.Setting.ProxyScheme}={sf.Settings.UpstreamProxyScheme}"; cmd += $" -{Key.Setting.ProxyUser}={sf.Settings.UpstreamProxyUser}"; cmd += $" -{Key.Setting.ProxyPass}={sf.Settings.UpstreamProxyPass}"; cmd += $" -{Key.Setting.OnlyBlockedIPs}={sf.Settings.ApplyUpstreamOnlyToBlockedIps}"; } commands.Add(cmd); } if (sf.SettingsSSL != null) { baseCmd = Key.SSLSetting.Name; cmd = $"{baseCmd} -{Key.SSLSetting.Enable}={sf.SettingsSSL.EnableSSL}"; cmd += $" -{Key.SSLSetting.RootCA_Path}=\"{sf.SettingsSSL.RootCA_Path}\""; cmd += $" -{Key.SSLSetting.RootCA_KeyPath}=\"{sf.SettingsSSL.RootCA_KeyPath}\""; cmd += $" -{Key.SSLSetting.Cert_Path}=\"{sf.SettingsSSL.Cert_Path}\""; cmd += $" -{Key.SSLSetting.Cert_KeyPath}=\"{sf.SettingsSSL.Cert_KeyPath}\""; cmd += $" -{Key.SSLSetting.ChangeSni}={sf.SettingsSSL.ChangeSni}"; cmd += $" -{Key.SSLSetting.DefaultSni}={sf.SettingsSSL.DefaultSni}"; commands.Add(cmd); } if (sf.Fragment != null) { baseCmd = $"{Key.Programs.Name} {Key.Programs.Fragment.Name}"; cmd = $"{baseCmd} -{Key.Programs.Fragment.Mode.Name}={sf.Fragment.FragmentMode}"; cmd += $" -{Key.Programs.Fragment.Mode.Program.BeforeSniChunks}={sf.Fragment.BeforeSniChunks}"; cmd += $" -{Key.Programs.Fragment.Mode.Program.ChunkMode.Name}={sf.Fragment.DPIChunkMode}"; cmd += $" -{Key.Programs.Fragment.Mode.Program.SniChunks}={sf.Fragment.SniChunks}"; cmd += $" -{Key.Programs.Fragment.Mode.Program.AntiPatternOffset}={sf.Fragment.AntiPatternOffset}"; cmd += $" -{Key.Programs.Fragment.Mode.Program.FragmentDelay}={sf.Fragment.FragmentDelay}"; commands.Add(cmd); } if (sf.Rules != null) { baseCmd = $"{Key.Programs.Name} {Key.Programs.Rules.Name}"; cmd = $"{baseCmd} -{Key.Programs.Rules.Mode.Name}={sf.Rules.RulesMode}"; cmd += $" -{Key.Programs.Rules.PathOrText}=\"{sf.Rules.PathOrText.Replace(Environment.NewLine, "\\n")}\""; commands.Add(cmd); } if (sf.DnsLimit != null) { baseCmd = $"{Key.Programs.Name} {Key.Programs.DnsLimit.Name}"; cmd = $"{baseCmd} -{Key.Programs.DnsLimit.Enable}={sf.DnsLimit.EnableDnsLimit}"; cmd += $" -{Key.Programs.DnsLimit.DisablePlain}={sf.DnsLimit.DisablePlainDns}"; cmd += $" -{Key.Programs.DnsLimit.DoHPathLimitMode.Name}={sf.DnsLimit.LimitDoHMode}"; cmd += $" -{Key.Programs.DnsLimit.PathOrText}=\"{sf.DnsLimit.PathOrText.Replace(Environment.NewLine, "\\n")}\""; commands.Add(cmd); } // Add A New Line commands.Add(string.Empty); } } if (LoadCommands.Any()) commands.AddRange(LoadCommands); if (commands.Any()) { await commands.SaveToFileAsync(p); await WriteToStdoutAsync("Saved To:", ConsoleColor.Green); await WriteToStdoutAsync(p, ConsoleColor.Green); } else await WriteToStdoutAsync("There Is Nothing To Save.", ConsoleColor.Blue); } else await WriteToStdoutAsync("Failed To Find The Path.", ConsoleColor.Red); } catch (Exception ex) { Debug.WriteLine("SaveCommandsToFileAsync: " + ex.Message); } } private static async Task LoadCommandsFromFileAsync() { try { string? p = ConsoleTools.GetCommandsPath(); if (!string.IsNullOrEmpty(p)) { if (File.Exists(p)) { List<string> commands = new(); await commands.LoadFromFileAsync(p, true, true); if (commands.Any()) { LoadCommands.Clear(); for (int n = 0; n < commands.Count; n++) { string command = commands[n]; if (!command.StartsWith("//") || !command.ToLower().StartsWith("load")) await ExecuteCommandsAsync(command); } } await WriteToStdoutAsync($"\nLoaded From:", ConsoleColor.Green); await WriteToStdoutAsync(p, ConsoleColor.Green); } else await WriteToStdoutAsync("File Not Exist.", ConsoleColor.Blue); } else await WriteToStdoutAsync("Failed To Find The Path.", ConsoleColor.Red); } catch (Exception ex) { Debug.WriteLine("LoadCommandsFromFileAsync: " + ex.Message); } } private static async Task WriteToStdoutAsync(string msg, ConsoleColor consoleColor = ConsoleColor.White, bool resetColor = true, string confirmMsg = "") { try { Console.ForegroundColor = consoleColor; List<string> lines = msg.ReplaceLineEndings().Split(Environment.NewLine).ToList(); foreach (string line in lines) await Console.Out.WriteLineAsync(line); if (resetColor) Console.ResetColor(); // Send Confirm Msg if (!string.IsNullOrWhiteSpace(confirmMsg)) { string confirm = $"Confirmed: {confirmMsg}"; await Console.Out.WriteLineAsync(confirm); } } catch (Exception ex) { try { await Console.Out.WriteLineAsync(ex.Message); } catch (Exception ex2) { Debug.WriteLine("WriteToStdoutAsync: " + ex2.Message); } } } private static async Task WriteToStderrAsync(string msg, ConsoleColor consoleColor = ConsoleColor.White, bool resetColor = true) { try { Console.ForegroundColor = consoleColor; List<string> lines = msg.ReplaceLineEndings().Split(Environment.NewLine).ToList(); foreach (string line in lines) await Console.Error.WriteLineAsync(line); if (resetColor) Console.ResetColor(); } catch (Exception) { } } }