mirror of
https://github.com/freedbygrace/PSOPNSenseAPI.git
synced 2026-08-18 06:13:50 +00:00
Fixed threading issues in PowerShell cmdlets to ensure WriteObject and WriteError are only called from the main thread
This commit is contained in:
@@ -23,7 +23,7 @@ namespace PSOPNSenseAPI.Cmdlets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Cmdlet(VerbsCommunications.Connect, "OPNSense")]
|
[Cmdlet(VerbsCommunications.Connect, "OPNSense")]
|
||||||
[OutputType(typeof(void))]
|
[OutputType(typeof(void))]
|
||||||
public class ConnectOPNSenseCmdlet : PSCmdlet
|
public class ConnectOPNSenseCmdlet : OPNSenseBaseCmdlet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <para type="description">The URL of the OPNSense firewall.</para>
|
/// <para type="description">The URL of the OPNSense firewall.</para>
|
||||||
@@ -61,39 +61,28 @@ namespace PSOPNSenseAPI.Cmdlets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes the cmdlet
|
/// Processes the cmdlet
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecordInternal()
|
||||||
{
|
{
|
||||||
try
|
// Check if already connected
|
||||||
|
if (OPNSenseSession.IsConnected && !Force.IsPresent)
|
||||||
{
|
{
|
||||||
// Check if already connected
|
WriteWarning($"Already connected to {OPNSenseSession.BaseUrl}. Use -Force to reconnect.");
|
||||||
if (OPNSenseSession.IsConnected && !Force.IsPresent)
|
return;
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
/// <summary>
|
||||||
/// Processes the cmdlet
|
/// Processes the cmdlet
|
||||||
/// </summary>
|
/// </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);
|
WriteObject(result.Rows, true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
HandleException(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,67 +54,72 @@ namespace PSOPNSenseAPI.Cmdlets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes the cmdlet
|
/// Processes the cmdlet
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecordInternal()
|
||||||
{
|
{
|
||||||
try
|
if (!ShouldProcess(Name, "Install plugin"))
|
||||||
{
|
{
|
||||||
if (!ShouldProcess(Name, "Install plugin"))
|
return;
|
||||||
{
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using PSOPNSenseAPI.Logging;
|
using PSOPNSenseAPI.Logging;
|
||||||
using PSOPNSenseAPI.Models;
|
using PSOPNSenseAPI.Models;
|
||||||
using PSOPNSenseAPI.Services;
|
using PSOPNSenseAPI.Services;
|
||||||
@@ -12,17 +13,7 @@ namespace PSOPNSenseAPI.Cmdlets
|
|||||||
public abstract class OPNSenseBaseCmdlet : PSCmdlet
|
public abstract class OPNSenseBaseCmdlet : PSCmdlet
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the API client
|
/// Begins the processing of the cmdlet
|
||||||
/// </summary>
|
|
||||||
protected OPNSenseApiClient ApiClient => OPNSenseSession.Current;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger
|
|
||||||
/// </summary>
|
|
||||||
protected ILogger Logger { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes the cmdlet
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void BeginProcessing()
|
protected override void BeginProcessing()
|
||||||
{
|
{
|
||||||
@@ -40,26 +31,135 @@ namespace PSOPNSenseAPI.Cmdlets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="ex">The exception to handle</param>
|
/// <param name="ex">The exception to handle</param>
|
||||||
protected void HandleException(Exception ex)
|
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(
|
if (ProcessingException is OPNSenseApiException apiEx)
|
||||||
apiEx,
|
{
|
||||||
"OPNSenseApiError",
|
WriteError(new ErrorRecord(
|
||||||
ErrorCategory.InvalidOperation,
|
apiEx,
|
||||||
null));
|
"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(
|
// Use ConfigureAwait(false) to avoid deadlocks
|
||||||
ex,
|
return asyncFunc().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
"OPNSenseError",
|
}
|
||||||
ErrorCategory.NotSpecified,
|
catch (Exception ex)
|
||||||
null));
|
{
|
||||||
|
// 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>
|
/// <param name="message">The message to log</param>
|
||||||
public void Error(string message)
|
public void Error(string message)
|
||||||
{
|
{
|
||||||
_cmdlet.WriteError(new ErrorRecord(
|
// Store the error message but don't call WriteError directly
|
||||||
new System.Exception(message),
|
// The cmdlet will handle writing the error in ProcessRecord
|
||||||
"OPNSenseApiError",
|
_cmdlet.WriteWarning($"Error: {message}");
|
||||||
ErrorCategory.NotSpecified,
|
|
||||||
null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
RootModule = 'lib\PSOPNSenseAPI.dll'
|
RootModule = 'lib\PSOPNSenseAPI.dll'
|
||||||
|
|
||||||
# Version number of this module.
|
# Version number of this module.
|
||||||
ModuleVersion = '2025.04.15.1033'
|
ModuleVersion = '2025.04.15.1216'
|
||||||
|
|
||||||
# Supported PSEditions
|
# Supported PSEditions
|
||||||
CompatiblePSEditions = @('Desktop', 'Core')
|
CompatiblePSEditions = @('Desktop', 'Core')
|
||||||
|
|||||||
Reference in New Issue
Block a user