Fixed threading issues in PowerShell cmdlets to ensure WriteObject and WriteError are only called from the main thread

This commit is contained in:
GraceSolutions
2025-04-15 12:17:09 -04:00
parent 013f1ba4a9
commit edb95d1d4a
6 changed files with 217 additions and 129 deletions
@@ -23,7 +23,7 @@ namespace PSOPNSenseAPI.Cmdlets
/// </summary>
[Cmdlet(VerbsCommunications.Connect, "OPNSense")]
[OutputType(typeof(void))]
public class ConnectOPNSenseCmdlet : PSCmdlet
public class ConnectOPNSenseCmdlet : OPNSenseBaseCmdlet
{
/// <summary>
/// <para type="description">The URL of the OPNSense firewall.</para>
@@ -61,39 +61,28 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
protected override void ProcessRecordInternal()
{
try
// Check if already connected
if (OPNSenseSession.IsConnected && !Force.IsPresent)
{
// Check if already connected
if (OPNSenseSession.IsConnected && !Force.IsPresent)
{
WriteWarning($"Already connected to {OPNSenseSession.BaseUrl}. Use -Force to reconnect.");
return;
}
// Dispose existing connection if there is one
OPNSenseSession.Current?.Dispose();
// Create a new logger
var logger = new PowerShellLogger(this);
// Create a new API client
var client = new OPNSenseApiClient(Server, ApiKey, ApiSecret, SkipCertificateCheck.IsPresent, logger);
// Set the current session
OPNSenseSession.Current = client;
WriteVerbose($"Connected to OPNSense firewall at {Server}");
}
catch (Exception ex)
{
WriteError(new ErrorRecord(
ex,
"ConnectionFailed",
ErrorCategory.ConnectionError,
Server));
WriteWarning($"Already connected to {OPNSenseSession.BaseUrl}. Use -Force to reconnect.");
return;
}
// Dispose existing connection if there is one
OPNSenseSession.Current?.Dispose();
// Create a new logger
var logger = new PowerShellLogger(this);
// Create a new API client
var client = new OPNSenseApiClient(Server, ApiKey, ApiSecret, SkipCertificateCheck.IsPresent, logger);
// Set the current session
OPNSenseSession.Current = client;
WriteVerbose($"Connected to OPNSense firewall at {Server}");
}
}
}
@@ -26,21 +26,17 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
protected override void ProcessRecordInternal()
{
try
var dhcpService = new DHCPService(ApiClient, Logger);
var result = ExecuteAsyncTask(() => dhcpService.GetLeasesAsync());
// Only write output if no exception occurred
if (ProcessingException == null && result != null)
{
var dhcpService = new DHCPService(ApiClient, Logger);
var task = Task.Run(async () => await dhcpService.GetLeasesAsync());
var result = task.GetAwaiter().GetResult();
WriteObject(result.Rows, true);
}
catch (Exception ex)
{
HandleException(ex);
}
}
}
}
@@ -54,67 +54,72 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
protected override void ProcessRecordInternal()
{
try
if (!ShouldProcess(Name, "Install plugin"))
{
if (!ShouldProcess(Name, "Install plugin"))
{
return;
}
var pluginService = new PluginService(ApiClient, Logger);
var task = Task.Run(async () => await pluginService.InstallPluginAsync(Name));
var result = task.GetAwaiter().GetResult();
WriteVerbose($"Plugin installation initiated: {result.Status}");
WriteObject($"Plugin installation initiated: {result.Status}");
if (Wait.IsPresent)
{
WriteVerbose($"Waiting for plugin installation to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)");
WriteObject("Waiting for plugin installation to complete...");
DateTime startTime = DateTime.Now;
bool completed = false;
while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout))
{
var statusTask = Task.Run(async () => await pluginService.GetPluginStatusAsync());
var statusResult = statusTask.GetAwaiter().GetResult();
if (statusResult.Status == "done")
{
WriteVerbose("Plugin installation completed successfully");
WriteObject("Plugin installation completed successfully");
completed = true;
break;
}
else if (statusResult.Status == "error")
{
WriteError(new ErrorRecord(
new Exception($"Plugin installation failed: {statusResult.Log}"),
"PluginInstallationFailed",
ErrorCategory.InvalidOperation,
Name));
completed = true;
break;
}
WriteVerbose($"Plugin installation status: {statusResult.Status}");
Thread.Sleep(Interval * 1000);
}
if (!completed)
{
WriteWarning($"Timed out waiting for plugin installation to complete after {Timeout} seconds");
}
}
return;
}
catch (Exception ex)
var pluginService = new PluginService(ApiClient, Logger);
// Use our safe execution method
var result = ExecuteAsyncTask(() => pluginService.InstallPluginAsync(Name));
// Only continue if no exception occurred
if (ProcessingException != null || result == null)
{
HandleException(ex);
return;
}
WriteVerbose($"Plugin installation initiated: {result.Status}");
WriteObject($"Plugin installation initiated: {result.Status}");
if (Wait.IsPresent)
{
WriteVerbose($"Waiting for plugin installation to complete (timeout: {Timeout} seconds, interval: {Interval} seconds)");
WriteObject("Waiting for plugin installation to complete...");
DateTime startTime = DateTime.Now;
bool completed = false;
while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout))
{
// Use our safe execution method
var statusResult = ExecuteAsyncTask(() => pluginService.GetPluginStatusAsync());
// Break if an exception occurred
if (ProcessingException != null || statusResult == null)
{
break;
}
if (statusResult.Status == "done")
{
WriteVerbose("Plugin installation completed successfully");
WriteObject("Plugin installation completed successfully");
completed = true;
break;
}
else if (statusResult.Status == "error")
{
WriteError(new ErrorRecord(
new Exception($"Plugin installation failed: {statusResult.Log}"),
"PluginInstallationFailed",
ErrorCategory.InvalidOperation,
Name));
completed = true;
break;
}
WriteVerbose($"Plugin installation status: {statusResult.Status}");
Thread.Sleep(Interval * 1000);
}
if (!completed && ProcessingException == null)
{
WriteWarning($"Timed out waiting for plugin installation to complete after {Timeout} seconds");
}
}
}
}
+124 -24
View File
@@ -1,5 +1,6 @@
using System;
using System.Management.Automation;
using System.Threading.Tasks;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models;
using PSOPNSenseAPI.Services;
@@ -12,17 +13,7 @@ namespace PSOPNSenseAPI.Cmdlets
public abstract class OPNSenseBaseCmdlet : PSCmdlet
{
/// <summary>
/// Gets the API client
/// </summary>
protected OPNSenseApiClient ApiClient => OPNSenseSession.Current;
/// <summary>
/// Gets the logger
/// </summary>
protected ILogger Logger { get; private set; }
/// <summary>
/// Initializes the cmdlet
/// Begins the processing of the cmdlet
/// </summary>
protected override void BeginProcessing()
{
@@ -40,26 +31,135 @@ namespace PSOPNSenseAPI.Cmdlets
}
/// <summary>
/// Handles exceptions
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
{
try
{
ProcessRecordInternal();
}
catch (Exception ex)
{
HandleException(ex);
}
finally
{
// Process any exceptions that occurred
ProcessExceptions();
}
}
/// <summary>
/// Internal method for processing the cmdlet
/// </summary>
protected virtual void ProcessRecordInternal()
{
// Override in derived classes
}
/// <summary>
/// Ends the processing of the cmdlet
/// </summary>
protected override void EndProcessing()
{
base.EndProcessing();
}
/// <summary>
/// Gets the API client
/// </summary>
protected OPNSenseApiClient ApiClient => OPNSenseSession.Current;
/// <summary>
/// Gets the logger
/// </summary>
protected ILogger Logger { get; private set; }
/// <summary>
/// Exception to be processed in ProcessRecord
/// </summary>
protected Exception ProcessingException { get; private set; }
/// <summary>
/// Handles exceptions by storing them for later processing
/// </summary>
/// <param name="ex">The exception to handle</param>
protected void HandleException(Exception ex)
{
if (ex is OPNSenseApiException apiEx)
// Store the exception to be processed in ProcessRecord
ProcessingException = ex;
WriteWarning($"Error occurred: {ex.Message}");
}
/// <summary>
/// Processes any stored exceptions
/// </summary>
protected void ProcessExceptions()
{
if (ProcessingException != null)
{
WriteError(new ErrorRecord(
apiEx,
"OPNSenseApiError",
ErrorCategory.InvalidOperation,
null));
if (ProcessingException is OPNSenseApiException apiEx)
{
WriteError(new ErrorRecord(
apiEx,
"OPNSenseApiError",
ErrorCategory.InvalidOperation,
null));
}
else
{
WriteError(new ErrorRecord(
ProcessingException,
"OPNSenseError",
ErrorCategory.NotSpecified,
null));
}
// Clear the exception after processing
ProcessingException = null;
}
else
}
/// <summary>
/// Executes an async task synchronously and safely
/// </summary>
/// <typeparam name="T">The return type of the task</typeparam>
/// <param name="asyncFunc">The async function to execute</param>
/// <returns>The result of the async function</returns>
protected T ExecuteAsyncTask<T>(Func<Task<T>> asyncFunc)
{
try
{
WriteError(new ErrorRecord(
ex,
"OPNSenseError",
ErrorCategory.NotSpecified,
null));
// Use ConfigureAwait(false) to avoid deadlocks
return asyncFunc().ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (Exception ex)
{
// Store the exception to be processed in ProcessRecord
ProcessingException = ex;
WriteWarning($"Error occurred: {ex.Message}");
return default;
}
}
/// <summary>
/// Executes an async task synchronously and safely (no return value)
/// </summary>
/// <param name="asyncAction">The async action to execute</param>
protected void ExecuteAsyncTask(Func<Task> asyncAction)
{
try
{
// Use ConfigureAwait(false) to avoid deadlocks
asyncAction().ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (Exception ex)
{
// Store the exception to be processed in ProcessRecord
ProcessingException = ex;
WriteWarning($"Error occurred: {ex.Message}");
}
}
}
@@ -51,11 +51,9 @@ namespace PSOPNSenseAPI.Logging
/// <param name="message">The message to log</param>
public void Error(string message)
{
_cmdlet.WriteError(new ErrorRecord(
new System.Exception(message),
"OPNSenseApiError",
ErrorCategory.NotSpecified,
null));
// Store the error message but don't call WriteError directly
// The cmdlet will handle writing the error in ProcessRecord
_cmdlet.WriteWarning($"Error: {message}");
}
/// <summary>
+1 -1
View File
@@ -3,7 +3,7 @@
RootModule = 'lib\PSOPNSenseAPI.dll'
# Version number of this module.
ModuleVersion = '2025.04.15.1033'
ModuleVersion = '2025.04.15.1216'
# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')