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;
}