From 9de9c73f18ab54d28d5b0cdc21599a74ef636026 Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:26:23 +0200 Subject: [PATCH 1/6] poc --- .../Commands/PowershellCommand.cs | 9 ++--- .../Managers/PowershellManager.cs | 39 +++++++++++-------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/HomeAssistant/Commands/PowershellCommand.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/HomeAssistant/Commands/PowershellCommand.cs index e9076055..187cade1 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/HomeAssistant/Commands/PowershellCommand.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/HomeAssistant/Commands/PowershellCommand.cs @@ -45,7 +45,7 @@ public override void TurnOn() } var executed = _isScript - ? PowershellManager.ExecuteScriptHeadless(Command) + ? PowershellManager.ExecuteScriptHeadless(Command, string.Empty) : PowershellManager.ExecuteCommandHeadless(Command); if (!executed) Log.Error("[POWERSHELL] [{name}] Executing {descriptor} failed", Name, _descriptor, Name); @@ -57,12 +57,9 @@ public override void TurnOnWithAction(string action) { State = "ON"; - // prepare command - var command = string.IsNullOrWhiteSpace(Command) ? action : $"{Command} {action}"; - var executed = _isScript - ? PowershellManager.ExecuteScriptHeadless(command) - : PowershellManager.ExecuteCommandHeadless(command); + ? PowershellManager.ExecuteScriptHeadless(Command, action) + : PowershellManager.ExecuteCommandHeadless(Command); if (!executed) Log.Error("[POWERSHELL] [{name}] Launching PS {descriptor} with action '{action}' failed", Name, _descriptor, action); diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs index 869873a8..348bc7c1 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text; +using CliWrap; using Serilog; namespace HASS.Agent.Shared.Managers @@ -17,16 +18,29 @@ public static class PowershellManager /// /// /// - public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, false); + public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, string.Empty, false); /// /// Executes a Powershell script without waiting for or checking results /// /// + /// /// - public static bool ExecuteScriptHeadless(string script) => ExecuteHeadless(script, true); + public static bool ExecuteScriptHeadless(string script, string parameters) => ExecuteHeadless(script, parameters, true); - private static bool ExecuteHeadless(string command, bool isScript) + private static string GetProcessArguments(string command, string parameters, bool isScript) + { + if (isScript) + { + return string.IsNullOrWhiteSpace(parameters) ? $"-File \"{command}\"" : $"-File \"{command}\" \"{parameters}\""; + } + else + { + return $@"& {{{command}}}"; //NOTE: place to fix any potential future issues with "command part of the command" + } + } + + private static bool ExecuteHeadless(string command, string parameters, bool isScript) { var descriptor = isScript ? "script" : "command"; @@ -50,14 +64,10 @@ private static bool ExecuteHeadless(string command, bool isScript) WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, FileName = psExec, - WorkingDirectory = workingDir + WorkingDirectory = workingDir, + Arguments = GetProcessArguments(command, parameters, isScript) }; - // set the right type of arguments - processInfo.Arguments = isScript ? - $@"& '{command}'" - : $@"& {{{command}}}"; - // launch using var process = new Process(); process.StartInfo = processInfo; @@ -85,7 +95,7 @@ private static bool ExecuteHeadless(string command, bool isScript) /// /// /// - public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, false, timeout); + public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, string.Empty, false, timeout); /// /// Executes a Powershell script, logs the output if it fails @@ -93,9 +103,9 @@ private static bool ExecuteHeadless(string command, bool isScript) /// /// /// - public static bool ExecuteScript(string script, TimeSpan timeout) => Execute(script, true, timeout); + public static bool ExecuteScript(string script, string parameters, TimeSpan timeout) => Execute(script, parameters, true, timeout); - private static bool Execute(string command, bool isScript, TimeSpan timeout) + private static bool Execute(string command, string parameters, bool isScript, TimeSpan timeout) { var descriptor = isScript ? "script" : "command"; @@ -121,10 +131,7 @@ private static bool Execute(string command, bool isScript, TimeSpan timeout) RedirectStandardOutput = true, UseShellExecute = false, WorkingDirectory = workingDir, - // set the right type of arguments - Arguments = isScript - ? $@"& '{command}'" - : $@"& {{{command}}}" + Arguments = GetProcessArguments(command, parameters, isScript) }; // launch From e5ff1e1c1ee8f790b1f1917bba140f5d3d3c9960 Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:35:28 +0200 Subject: [PATCH 2/6] added precheck for application message being null/empty (for cases when payload from HA is "") --- .../HASS.Agent/MQTT/MqttManager.cs | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent/MQTT/MqttManager.cs b/src/HASS.Agent.Staging/HASS.Agent/MQTT/MqttManager.cs index d4f08916..875a46de 100644 --- a/src/HASS.Agent.Staging/HASS.Agent/MQTT/MqttManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent/MQTT/MqttManager.cs @@ -37,7 +37,7 @@ public class MqttManager : IMqttManager private bool _disconnectionNotified = false; private bool _connectingFailureNotified = false; - + private MqttStatus _status = MqttStatus.Connecting; /// @@ -82,7 +82,7 @@ public void Initialize() // create our device's config model if (Variables.DeviceConfig == null) CreateDeviceConfigModel(); - + // create a new mqtt client _mqttClient = Variables.MqttFactory.CreateManagedMqttClient(); @@ -348,7 +348,7 @@ public async Task PublishAsync(MqttApplicationMessage message) if (Variables.ExtendedLogging) Log.Warning("[MQTT] Not connected, message dropped (won't report again for 5 minutes)"); return false; } - + // publish away var published = await _mqttClient.PublishAsync(message); if (published.ReasonCode == MqttClientPublishReasonCode.Success) return true; @@ -390,12 +390,12 @@ public async Task AnnounceAutoDiscoveryConfigAsync(AbstractDiscoverable discover // prepare topic var topic = $"{Variables.AppSettings.MqttDiscoveryPrefix}/{domain}/{Variables.DeviceConfig.Name}/{discoverable.ObjectId}/config"; - + // build config message var messageBuilder = new MqttApplicationMessageBuilder() .WithTopic(topic) .WithRetainFlag(Variables.AppSettings.MqttUseRetainFlag); - + // add payload if (clearConfig) messageBuilder.WithPayload(Array.Empty()); else messageBuilder.WithPayload(JsonSerializer.Serialize(discoverable.GetAutoDiscoveryConfig(), discoverable.GetAutoDiscoveryConfig().GetType(), JsonSerializerOptions)); @@ -420,7 +420,7 @@ public async Task AnnounceAutoDiscoveryConfigAsync(AbstractDiscoverable discover /// private DateTime _lastAvailableAnnouncement = DateTime.MinValue; private DateTime _lastAvailableAnnouncementFailedLogged = DateTime.MinValue; - + /// /// JSON serializer options (camelcase, casing, ignore condition, converters) /// @@ -516,7 +516,7 @@ public async Task ClearDeviceConfigAsync() .WithTopic($"{Variables.AppSettings.MqttDiscoveryPrefix}/sensor/{Variables.DeviceConfig.Name}/availability") .WithPayload(Array.Empty()) .WithRetainFlag(Variables.AppSettings.MqttUseRetainFlag); - + // publish await _mqttClient.PublishAsync(messageBuilder.Build()); } @@ -600,20 +600,20 @@ public async Task UnubscribeAsync(AbstractCommand command) private static ManagedMqttClientOptions GetOptions() { if (string.IsNullOrEmpty(Variables.AppSettings.MqttAddress)) return null; - + // id can be random, but we'll store it for consistency (unless user-defined) if (string.IsNullOrEmpty(Variables.AppSettings.MqttClientId)) { Variables.AppSettings.MqttClientId = Guid.NewGuid().ToString()[..8]; SettingsManager.StoreAppSettings(); } - + // configure last will message var lastWillMessageBuilder = new MqttApplicationMessageBuilder() .WithTopic($"{Variables.AppSettings.MqttDiscoveryPrefix}/sensor/{Variables.DeviceConfig.Name}/availability") .WithPayload("offline") .WithRetainFlag(Variables.AppSettings.MqttUseRetainFlag); - + // prepare message var lastWillMessage = lastWillMessageBuilder.Build(); @@ -687,7 +687,7 @@ private static void HandleMessageReceived(MqttApplicationMessage applicationMess var notification = JsonSerializer.Deserialize(applicationMessage.Payload, JsonSerializerOptions)!; _ = Task.Run(() => NotificationManager.ShowNotification(notification)); return; - } + } if (applicationMessage.Topic == $"hass.agent/media_player/{HelperFunctions.GetConfiguredDeviceName()}/cmd") { @@ -745,12 +745,12 @@ private static void HandleCommandReceived(MqttApplicationMessage applicationMess if (payload.Contains("on")) command.TurnOn(); else if (payload.Contains("off")) command.TurnOff(); else switch (payload) - { - case "press": - case "lock": - command.TurnOn(); - break; - } + { + case "press": + case "lock": + command.TurnOn(); + break; + } } /// @@ -760,8 +760,12 @@ private static void HandleCommandReceived(MqttApplicationMessage applicationMess /// private static void HandleActionReceived(MqttApplicationMessage applicationMessage, AbstractCommand command) { + if (applicationMessage.Payload == null) + return; + var payload = Encoding.UTF8.GetString(applicationMessage.Payload); - if (string.IsNullOrWhiteSpace(payload)) return; + if (string.IsNullOrWhiteSpace(payload)) + return; command.TurnOnWithAction(payload); } From 8c6e1436bf86f2d4d44fad90b6caff3f82273ca0 Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:44:41 +0200 Subject: [PATCH 3/6] added check for edge case where OEMCodePage returns "1" --- .../Managers/PowershellManager.cs | 546 +++++++++--------- 1 file changed, 275 insertions(+), 271 deletions(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs index 348bc7c1..24ac9a40 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -8,275 +8,279 @@ namespace HASS.Agent.Shared.Managers { - /// - /// Performs powershell-related actions - /// - public static class PowershellManager - { - /// - /// Execute a Powershell command without waiting for or checking results - /// - /// - /// - public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, string.Empty, false); - - /// - /// Executes a Powershell script without waiting for or checking results - /// - /// - /// - /// - public static bool ExecuteScriptHeadless(string script, string parameters) => ExecuteHeadless(script, parameters, true); - - private static string GetProcessArguments(string command, string parameters, bool isScript) - { - if (isScript) - { - return string.IsNullOrWhiteSpace(parameters) ? $"-File \"{command}\"" : $"-File \"{command}\" \"{parameters}\""; - } - else - { - return $@"& {{{command}}}"; //NOTE: place to fix any potential future issues with "command part of the command" - } - } - - private static bool ExecuteHeadless(string command, string parameters, bool isScript) - { - var descriptor = isScript ? "script" : "command"; - - try - { - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - // find the powershell executable - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; - - // prepare the executing process - var processInfo = new ProcessStartInfo - { - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = psExec, - WorkingDirectory = workingDir, - Arguments = GetProcessArguments(command, parameters, isScript) - }; - - // launch - using var process = new Process(); - process.StartInfo = processInfo; - var start = process.Start(); - - if (!start) - { - Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {command}", descriptor, command); - return false; - } - - // done - return true; - } - catch (Exception ex) - { - Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); - return false; - } - } - - /// - /// Execute a Powershell command, logs the output if it fails - /// - /// - /// - /// - public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, string.Empty, false, timeout); - - /// - /// Executes a Powershell script, logs the output if it fails - /// - /// - /// - /// - public static bool ExecuteScript(string script, string parameters, TimeSpan timeout) => Execute(script, parameters, true, timeout); - - private static bool Execute(string command, string parameters, bool isScript, TimeSpan timeout) - { - var descriptor = isScript ? "script" : "command"; - - try - { - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - // find the powershell executable - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; - - // prepare the executing process - var processInfo = new ProcessStartInfo - { - FileName = psExec, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - WorkingDirectory = workingDir, - Arguments = GetProcessArguments(command, parameters, isScript) - }; - - // launch - using var process = new Process(); - process.StartInfo = processInfo; - var start = process.Start(); - - if (!start) - { - Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {script}", descriptor, command); - return false; - } - - // execute and wait - process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); - - if (process.ExitCode == 0) - { - // done, all good - return true; - } - - // non-zero exitcode, process as failed - Log.Error("[POWERSHELL] The {descriptor} returned non-zero exitcode: {code}", descriptor, process.ExitCode); - - var errors = process.StandardError.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(errors)) Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); - else - { - var console = process.StandardOutput.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(console)) Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); - else Log.Error("[POWERSHELL] No error and no console output"); - } - - // done - return false; - } - catch (Exception ex) - { - Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); - return false; - } - } - - /// - /// Executes the command or script, and returns the standard and error output - /// - /// - /// - /// - /// - /// - internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out string output, out string errors) - { - output = string.Empty; - errors = string.Empty; - - try - { - // check whether we're executing a script - var isScript = command.ToLower().EndsWith(".ps1"); - - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - // find the powershell executable - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; - - // prepare the executing process - var processInfo = new ProcessStartInfo - { - FileName = psExec, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = workingDir, - // attempt to set the right encoding - StandardOutputEncoding = Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage), - StandardErrorEncoding = Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage), - // set the right type of arguments - Arguments = isScript - ? $@"& '{command}'" - : $@"& {{{command}}}" - }; - - // execute and wait - using var process = new Process(); - process.StartInfo = processInfo; - - var start = process.Start(); - if (!start) - { - Log.Error("[POWERSHELL] Unable to begin executing the {type}: {cmd}", isScript ? "script" : "command", command); - return false; - } - - // wait for completion - var completed = process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); - if (!completed) Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); - - // read the streams - output = process.StandardOutput.ReadToEnd().Trim(); - errors = process.StandardError.ReadToEnd().Trim(); - - // dispose of them - process.StandardOutput.Dispose(); - process.StandardError.Dispose(); - - // make sure the process ends - process.Kill(); - - // done - return completed; - } - catch (Exception ex) - { - Log.Fatal(ex, ex.Message); - return false; - } - } - - /// - /// Attempt to locate powershell.exe - /// - /// - public static string GetPsExecutable() - { - // try regular location - var psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) return psExec; - - // try specific - psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) return psExec; - - // not found - Log.Error("[POWERSHELL] PS executable not found, make sure you have powershell installed on your system"); - return string.Empty; - } - } + /// + /// Performs powershell-related actions + /// + public static class PowershellManager + { + /// + /// Execute a Powershell command without waiting for or checking results + /// + /// + /// + public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, string.Empty, false); + + /// + /// Executes a Powershell script without waiting for or checking results + /// + /// + /// + /// + public static bool ExecuteScriptHeadless(string script, string parameters) => ExecuteHeadless(script, parameters, true); + + private static string GetProcessArguments(string command, string parameters, bool isScript) + { + if (isScript) + { + return string.IsNullOrWhiteSpace(parameters) ? $"-File \"{command}\"" : $"-File \"{command}\" \"{parameters}\""; + } + else + { + return $@"& {{{command}}}"; //NOTE: place to fix any potential future issues with "command part of the command" + } + } + + private static bool ExecuteHeadless(string command, string parameters, bool isScript) + { + var descriptor = isScript ? "script" : "command"; + + try + { + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + // find the powershell executable + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) return false; + + // prepare the executing process + var processInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = psExec, + WorkingDirectory = workingDir, + Arguments = GetProcessArguments(command, parameters, isScript) + }; + + // launch + using var process = new Process(); + process.StartInfo = processInfo; + var start = process.Start(); + + if (!start) + { + Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {command}", descriptor, command); + return false; + } + + // done + return true; + } + catch (Exception ex) + { + Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + return false; + } + } + + /// + /// Execute a Powershell command, logs the output if it fails + /// + /// + /// + /// + public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, string.Empty, false, timeout); + + /// + /// Executes a Powershell script, logs the output if it fails + /// + /// + /// + /// + public static bool ExecuteScript(string script, string parameters, TimeSpan timeout) => Execute(script, parameters, true, timeout); + + private static bool Execute(string command, string parameters, bool isScript, TimeSpan timeout) + { + var descriptor = isScript ? "script" : "command"; + + try + { + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + // find the powershell executable + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) return false; + + // prepare the executing process + var processInfo = new ProcessStartInfo + { + FileName = psExec, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDir, + Arguments = GetProcessArguments(command, parameters, isScript) + }; + + // launch + using var process = new Process(); + process.StartInfo = processInfo; + var start = process.Start(); + + if (!start) + { + Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {script}", descriptor, command); + return false; + } + + // execute and wait + process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); + + if (process.ExitCode == 0) + { + // done, all good + return true; + } + + // non-zero exitcode, process as failed + Log.Error("[POWERSHELL] The {descriptor} returned non-zero exitcode: {code}", descriptor, process.ExitCode); + + var errors = process.StandardError.ReadToEnd().Trim(); + if (!string.IsNullOrEmpty(errors)) Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); + else + { + var console = process.StandardOutput.ReadToEnd().Trim(); + if (!string.IsNullOrEmpty(console)) Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); + else Log.Error("[POWERSHELL] No error and no console output"); + } + + // done + return false; + } + catch (Exception ex) + { + Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + return false; + } + } + + /// + /// Executes the command or script, and returns the standard and error output + /// + /// + /// + /// + /// + /// + internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out string output, out string errors) + { + output = string.Empty; + errors = string.Empty; + + try + { + // check whether we're executing a script + var isScript = command.ToLower().EndsWith(".ps1"); + + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + // find the powershell executable + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) return false; + + // attempt to set the right encoding + var encoding = CultureInfo.CurrentCulture.TextInfo.OEMCodePage == 1 + ? Encoding.Unicode + : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + + // prepare the executing process + var processInfo = new ProcessStartInfo + { + FileName = psExec, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDir, + StandardOutputEncoding = encoding, + StandardErrorEncoding = encoding, + // set the right type of arguments + Arguments = isScript + ? $@"& '{command}'" + : $@"& {{{command}}}" + }; + + // execute and wait + using var process = new Process(); + process.StartInfo = processInfo; + + var start = process.Start(); + if (!start) + { + Log.Error("[POWERSHELL] Unable to begin executing the {type}: {cmd}", isScript ? "script" : "command", command); + return false; + } + + // wait for completion + var completed = process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); + if (!completed) Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); + + // read the streams + output = process.StandardOutput.ReadToEnd().Trim(); + errors = process.StandardError.ReadToEnd().Trim(); + + // dispose of them + process.StandardOutput.Dispose(); + process.StandardError.Dispose(); + + // make sure the process ends + process.Kill(); + + // done + return completed; + } + catch (Exception ex) + { + Log.Fatal(ex, ex.Message); + return false; + } + } + + /// + /// Attempt to locate powershell.exe + /// + /// + public static string GetPsExecutable() + { + // try regular location + var psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell\\v1.0\\powershell.exe"); + if (File.Exists(psExec)) return psExec; + + // try specific + psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "WindowsPowerShell\\v1.0\\powershell.exe"); + if (File.Exists(psExec)) return psExec; + + // not found + Log.Error("[POWERSHELL] PS executable not found, make sure you have powershell installed on your system"); + return string.Empty; + } + } } From 239fa7034cc3980e493818e7aa4675e2871fc8ce Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:00:17 +0200 Subject: [PATCH 4/6] changed fallback from utf-16 to utf-8 --- .../HASS.Agent.Shared/Managers/PowershellManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs index 24ac9a40..39cf9b2c 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -208,7 +208,7 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str // attempt to set the right encoding var encoding = CultureInfo.CurrentCulture.TextInfo.OEMCodePage == 1 - ? Encoding.Unicode + ? Encoding.UTF8 : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); // prepare the executing process From 0a540f88df782099b3d1b4ec17303f2adb3f0fa8 Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:08:52 +0200 Subject: [PATCH 5/6] added multi-step way to parse proper text encoding with fallback to UTF-8 --- .../Managers/PowershellManager.cs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs index 39cf9b2c..d4ad59ef 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using CliWrap; +using Newtonsoft.Json; using Serilog; namespace HASS.Agent.Shared.Managers @@ -176,6 +177,49 @@ private static bool Execute(string command, string parameters, bool isScript, Ti } } + private static Encoding TryParseCodePage(int codePage) + { + Encoding encoding = null; + try + { + encoding = Encoding.GetEncoding(codePage); + } + catch + { + // best effort + } + + return encoding; + } + + private static Encoding GetEncoding() + { + var encoding = TryParseCodePage(CultureInfo.InstalledUICulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.CurrentUICulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.InvariantCulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + Log.Warning("[POWERSHELL] Cannot parse system text culture to encoding, returning UTF-8 as a fallback, please report this as a GitHub issue"); + + Log.Debug("[POWERSHELL] currentInstalledUICulture {c}", JsonConvert.SerializeObject(CultureInfo.InstalledUICulture.TextInfo)); + Log.Debug("[POWERSHELL] currentCulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentCulture.TextInfo)); + Log.Debug("[POWERSHELL] currentUICulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentUICulture.TextInfo)); + Log.Debug("[POWERSHELL] invariantCulture {c}", JsonConvert.SerializeObject(CultureInfo.InvariantCulture.TextInfo)); + + return Encoding.UTF8; + } + /// /// Executes the command or script, and returns the standard and error output /// @@ -207,9 +251,7 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str if (string.IsNullOrEmpty(psExec)) return false; // attempt to set the right encoding - var encoding = CultureInfo.CurrentCulture.TextInfo.OEMCodePage == 1 - ? Encoding.UTF8 - : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + var encoding = GetEncoding(); // prepare the executing process var processInfo = new ProcessStartInfo From bad8c50bfa90e95d6b82c8551201ffd888e89dc2 Mon Sep 17 00:00:00 2001 From: Amadeo Alex <68441479+amadeo-alex@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:48:30 +0200 Subject: [PATCH 6/6] cleanup --- .../Managers/PowershellManager.cs | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs index d4ad59ef..576823dc 100644 --- a/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent.Staging/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -10,7 +10,7 @@ namespace HASS.Agent.Shared.Managers { /// - /// Performs powershell-related actions + /// Performs Powershell-related actions /// public static class PowershellManager { @@ -33,7 +33,9 @@ private static string GetProcessArguments(string command, string parameters, boo { if (isScript) { - return string.IsNullOrWhiteSpace(parameters) ? $"-File \"{command}\"" : $"-File \"{command}\" \"{parameters}\""; + return string.IsNullOrWhiteSpace(parameters) + ? $"-File \"{command}\"" + : $"-File \"{command}\" \"{parameters}\""; } else { @@ -55,11 +57,10 @@ private static bool ExecuteHeadless(string command, string parameters, bool isSc workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; } - // find the powershell executable var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; + if (string.IsNullOrEmpty(psExec)) + return false; - // prepare the executing process var processInfo = new ProcessStartInfo { WindowStyle = ProcessWindowStyle.Hidden, @@ -69,7 +70,6 @@ private static bool ExecuteHeadless(string command, string parameters, bool isSc Arguments = GetProcessArguments(command, parameters, isScript) }; - // launch using var process = new Process(); process.StartInfo = processInfo; var start = process.Start(); @@ -77,15 +77,16 @@ private static bool ExecuteHeadless(string command, string parameters, bool isSc if (!start) { Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {command}", descriptor, command); + return false; } - // done return true; } catch (Exception ex) { Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + return false; } } @@ -120,11 +121,9 @@ private static bool Execute(string command, string parameters, bool isScript, Ti workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; } - // find the powershell executable var psExec = GetPsExecutable(); if (string.IsNullOrEmpty(psExec)) return false; - // prepare the executing process var processInfo = new ProcessStartInfo { FileName = psExec, @@ -135,7 +134,6 @@ private static bool Execute(string command, string parameters, bool isScript, Ti Arguments = GetProcessArguments(command, parameters, isScript) }; - // launch using var process = new Process(); process.StartInfo = processInfo; var start = process.Start(); @@ -143,36 +141,38 @@ private static bool Execute(string command, string parameters, bool isScript, Ti if (!start) { Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {script}", descriptor, command); + return false; } - // execute and wait process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); if (process.ExitCode == 0) - { - // done, all good return true; - } // non-zero exitcode, process as failed Log.Error("[POWERSHELL] The {descriptor} returned non-zero exitcode: {code}", descriptor, process.ExitCode); var errors = process.StandardError.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(errors)) Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); + if (!string.IsNullOrEmpty(errors)) + { + Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); + } else { var console = process.StandardOutput.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(console)) Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); - else Log.Error("[POWERSHELL] No error and no console output"); + if (!string.IsNullOrEmpty(console)) + Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); + else + Log.Error("[POWERSHELL] No error and no console output"); } - // done return false; } catch (Exception ex) { Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + return false; } } @@ -216,7 +216,7 @@ private static Encoding GetEncoding() Log.Debug("[POWERSHELL] currentCulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentCulture.TextInfo)); Log.Debug("[POWERSHELL] currentUICulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentUICulture.TextInfo)); Log.Debug("[POWERSHELL] invariantCulture {c}", JsonConvert.SerializeObject(CultureInfo.InvariantCulture.TextInfo)); - + return Encoding.UTF8; } @@ -235,7 +235,6 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str try { - // check whether we're executing a script var isScript = command.ToLower().EndsWith(".ps1"); var workingDir = string.Empty; @@ -246,14 +245,12 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; } - // find the powershell executable var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; + if (string.IsNullOrEmpty(psExec)) + return false; - // attempt to set the right encoding var encoding = GetEncoding(); - // prepare the executing process var processInfo = new ProcessStartInfo { FileName = psExec, @@ -270,7 +267,6 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str : $@"& {{{command}}}" }; - // execute and wait using var process = new Process(); process.StartInfo = processInfo; @@ -278,30 +274,28 @@ internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out str if (!start) { Log.Error("[POWERSHELL] Unable to begin executing the {type}: {cmd}", isScript ? "script" : "command", command); + return false; } - // wait for completion var completed = process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); - if (!completed) Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); + if (!completed) + Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); - // read the streams output = process.StandardOutput.ReadToEnd().Trim(); errors = process.StandardError.ReadToEnd().Trim(); - // dispose of them process.StandardOutput.Dispose(); process.StandardError.Dispose(); - // make sure the process ends process.Kill(); - // done return completed; } catch (Exception ex) { Log.Fatal(ex, ex.Message); + return false; } } @@ -314,13 +308,14 @@ public static string GetPsExecutable() { // try regular location var psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) return psExec; + if (File.Exists(psExec)) + return psExec; // try specific psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) return psExec; + if (File.Exists(psExec)) + return psExec; - // not found Log.Error("[POWERSHELL] PS executable not found, make sure you have powershell installed on your system"); return string.Empty; }