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:29:12 -04:00
parent edb95d1d4a
commit 9c4d3f2eb3
7 changed files with 389 additions and 366 deletions
@@ -76,25 +76,24 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary> /// <summary>
/// Processes the cmdlet /// Processes the cmdlet
/// </summary> /// </summary>
protected override void ProcessRecord() protected override void ProcessRecordInternal()
{
try
{ {
var tailscaleService = new TailscaleService(ApiClient, Logger); var tailscaleService = new TailscaleService(ApiClient, Logger);
// Check if the plugin is installed // Check if the plugin is installed
var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); var isInstalled = ExecuteAsyncTask(() => tailscaleService.IsPluginInstalledAsync());
var isInstalled = isInstalledTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null)
{
return;
}
if (!isInstalled) if (!isInstalled)
{ {
if (!InstallIfMissing.IsPresent) if (!InstallIfMissing.IsPresent)
{ {
WriteError(new ErrorRecord( ProcessingException = new Exception("Tailscale plugin is not installed. Use -InstallIfMissing to install it.");
new Exception("Tailscale plugin is not installed. Use -InstallIfMissing to install it."),
"TailscalePluginNotInstalled",
ErrorCategory.InvalidOperation,
null));
return; return;
} }
@@ -104,16 +103,17 @@ namespace PSOPNSenseAPI.Cmdlets
} }
WriteVerbose("Tailscale plugin is not installed. Installing..."); WriteVerbose("Tailscale plugin is not installed. Installing...");
var installTask = Task.Run(async () => await tailscaleService.InstallPluginAsync()); var installResult = ExecuteAsyncTask(() => tailscaleService.InstallPluginAsync());
var installResult = installTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null)
{
return;
}
if (!installResult) if (!installResult)
{ {
WriteError(new ErrorRecord( ProcessingException = new Exception("Failed to install Tailscale plugin.");
new Exception("Failed to install Tailscale plugin."),
"TailscalePluginInstallFailed",
ErrorCategory.InvalidOperation,
null));
return; return;
} }
@@ -121,8 +121,13 @@ namespace PSOPNSenseAPI.Cmdlets
} }
// Get current status // Get current status
var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); var status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync());
var status = statusTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || status == null)
{
return;
}
// Enable Tailscale if needed // Enable Tailscale if needed
if (!status.Enabled && EnableIfDisabled.IsPresent) if (!status.Enabled && EnableIfDisabled.IsPresent)
@@ -130,8 +135,15 @@ namespace PSOPNSenseAPI.Cmdlets
WriteVerbose("Tailscale is disabled. Enabling..."); WriteVerbose("Tailscale is disabled. Enabling...");
// Get current settings // Get current settings
var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); var settingsResult = ExecuteAsyncTask(() => tailscaleService.GetSettingsAsync());
var currentSettings = settingsTask.GetAwaiter().GetResult().General;
// Only continue if no exception occurred
if (ProcessingException != null || settingsResult == null)
{
return;
}
var currentSettings = settingsResult.General;
// Process subnet routes if provided // Process subnet routes if provided
string routesToAdvertise = currentSettings.RoutesToAdvertise; string routesToAdvertise = currentSettings.RoutesToAdvertise;
@@ -168,47 +180,60 @@ namespace PSOPNSenseAPI.Cmdlets
}; };
// Update settings // Update settings
var updateTask = Task.Run(async () => await tailscaleService.UpdateSettingsAsync(settings)); var updateResult = ExecuteAsyncTask(() => tailscaleService.UpdateSettingsAsync(settings));
var updateResult = updateTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || updateResult == null)
{
return;
}
WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); WriteVerbose($"Tailscale settings updated: {updateResult.Result}");
// Get updated status // Get updated status
statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync());
status = statusTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || status == null)
{
return;
}
} }
// Start the service if needed // Start the service if needed
if (!status.Running && StartIfStopped.IsPresent) if (!status.Running && StartIfStopped.IsPresent)
{ {
WriteVerbose("Tailscale service is not running. Starting..."); WriteVerbose("Tailscale service is not running. Starting...");
var startTask = Task.Run(async () => await tailscaleService.StartServiceAsync()); var startResult = ExecuteAsyncTask(() => tailscaleService.StartServiceAsync());
var startResult = startTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || startResult == null)
{
return;
}
WriteVerbose($"Tailscale service started: {startResult.Status}"); WriteVerbose($"Tailscale service started: {startResult.Status}");
// Get updated status // Get updated status
statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync());
status = statusTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || status == null)
{
return;
}
} }
// Check if Tailscale is enabled and running // Check if Tailscale is enabled and running
if (!status.Enabled) if (!status.Enabled)
{ {
WriteError(new ErrorRecord( ProcessingException = new Exception("Tailscale is not enabled. Use -EnableIfDisabled to enable it.");
new Exception("Tailscale is not enabled. Use -EnableIfDisabled to enable it."),
"TailscaleNotEnabled",
ErrorCategory.InvalidOperation,
null));
return; return;
} }
if (!status.Running) if (!status.Running)
{ {
WriteError(new ErrorRecord( ProcessingException = new Exception("Tailscale service is not running. Use -StartIfStopped to start it.");
new Exception("Tailscale service is not running. Use -StartIfStopped to start it."),
"TailscaleNotRunning",
ErrorCategory.InvalidOperation,
null));
return; return;
} }
@@ -219,19 +244,34 @@ namespace PSOPNSenseAPI.Cmdlets
// Connect to Tailscale // Connect to Tailscale
WriteVerbose("Connecting to Tailscale network..."); WriteVerbose("Connecting to Tailscale network...");
var connectTask = Task.Run(async () => await tailscaleService.ConnectAsync(AuthKey)); var connectResult = ExecuteAsyncTask(() => tailscaleService.ConnectAsync(AuthKey));
var connectResult = connectTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || connectResult == null)
{
return;
}
WriteVerbose($"Tailscale connection status: {connectResult.Status}"); WriteVerbose($"Tailscale connection status: {connectResult.Status}");
WriteVerbose($"Tailscale connection message: {connectResult.Message}"); WriteVerbose($"Tailscale connection message: {connectResult.Message}");
// Get updated status // Get updated status
statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync());
status = statusTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || status == null)
{
return;
}
// Get interfaces // Get interfaces
var interfacesTask = Task.Run(async () => await tailscaleService.GetInterfacesAsync()); var interfaces = ExecuteAsyncTask(() => tailscaleService.GetInterfacesAsync());
var interfaces = interfacesTask.GetAwaiter().GetResult();
// Only continue if no exception occurred
if (ProcessingException != null || interfaces == null)
{
return;
}
// Create result object // Create result object
var result = new PSObject(); var result = new PSObject();
@@ -244,10 +284,5 @@ namespace PSOPNSenseAPI.Cmdlets
WriteObject(result); WriteObject(result);
} }
catch (Exception ex)
{
HandleException(ex);
}
}
} }
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.Management.Automation; using System.Management.Automation;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models; using PSOPNSenseAPI.Models;
namespace PSOPNSenseAPI.Cmdlets namespace PSOPNSenseAPI.Cmdlets
@@ -15,14 +16,20 @@ namespace PSOPNSenseAPI.Cmdlets
/// </summary> /// </summary>
[Cmdlet(VerbsCommon.Get, "OPNSenseConnection")] [Cmdlet(VerbsCommon.Get, "OPNSenseConnection")]
[OutputType(typeof(PSObject))] [OutputType(typeof(PSObject))]
public class GetOPNSenseConnectionCmdlet : PSCmdlet public class GetOPNSenseConnectionCmdlet : OPNSenseBaseCmdlet
{ {
/// <summary> /// <summary>
/// Processes the cmdlet /// Processes the cmdlet
/// </summary> /// </summary>
protected override void ProcessRecord() protected override void BeginProcessing()
{ {
try // Override the base implementation to avoid checking for connection
// since this cmdlet is used to check the connection status
base.BeginProcessing();
Logger = new PowerShellLogger(this);
}
protected override void ProcessRecordInternal()
{ {
if (!OPNSenseSession.IsConnected) if (!OPNSenseSession.IsConnected)
{ {
@@ -36,14 +43,5 @@ namespace PSOPNSenseAPI.Cmdlets
WriteObject(connectionInfo); WriteObject(connectionInfo);
} }
catch (Exception ex)
{
WriteError(new ErrorRecord(
ex,
"GetConnectionFailed",
ErrorCategory.ConnectionError,
null));
}
}
} }
} }
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Management.Automation; using System.Management.Automation;
using System.Net; using System.Net;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Utilities; using PSOPNSenseAPI.Utilities;
namespace PSOPNSenseAPI.Cmdlets namespace PSOPNSenseAPI.Cmdlets
@@ -27,7 +28,7 @@ namespace PSOPNSenseAPI.Cmdlets
/// </summary> /// </summary>
[Cmdlet(VerbsLifecycle.Invoke, "OPNSenseNetworkCalculation")] [Cmdlet(VerbsLifecycle.Invoke, "OPNSenseNetworkCalculation")]
[OutputType(typeof(PSObject), typeof(bool), typeof(IPNetwork2[]))] [OutputType(typeof(PSObject), typeof(bool), typeof(IPNetwork2[]))]
public class InvokeOPNSenseNetworkCalculationCmdlet : PSCmdlet public class InvokeOPNSenseNetworkCalculationCmdlet : OPNSenseBaseCmdlet
{ {
/// <summary> /// <summary>
/// <para type="description">The network in CIDR notation to perform calculations on.</para> /// <para type="description">The network in CIDR notation to perform calculations on.</para>
@@ -72,12 +73,27 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary> /// <summary>
/// Processes the cmdlet /// Processes the cmdlet
/// </summary> /// </summary>
protected override void ProcessRecord() protected override void BeginProcessing()
{ {
try // Override the base implementation to avoid checking for connection
// since this cmdlet doesn't require a connection
base.BeginProcessing();
Logger = new PowerShellLogger(this);
}
protected override void ProcessRecordInternal()
{ {
// Parse the network // Parse the network
IPNetwork2 network = NetworkUtility.ParseCIDR(Network); IPNetwork2 network;
try
{
network = NetworkUtility.ParseCIDR(Network);
}
catch (Exception ex)
{
ProcessingException = new ArgumentException($"Invalid network format: {Network}", ex);
return;
}
switch (Operation) switch (Operation)
{ {
@@ -88,11 +104,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "Subnet": case "Subnet":
if (!PrefixLength.HasValue) if (!PrefixLength.HasValue)
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("PrefixLength is required for Subnet operations.");
new ArgumentException("PrefixLength is required for Subnet operations."),
"MissingPrefixLength",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -102,11 +114,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "SubnetByCount": case "SubnetByCount":
if (!SubnetCount.HasValue) if (!SubnetCount.HasValue)
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("SubnetCount is required for SubnetByCount operations.");
new ArgumentException("SubnetCount is required for SubnetByCount operations."),
"MissingSubnetCount",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -116,11 +124,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "Contains": case "Contains":
if (string.IsNullOrEmpty(IPAddress)) if (string.IsNullOrEmpty(IPAddress))
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("IPAddress is required for Contains operations.");
new ArgumentException("IPAddress is required for Contains operations."),
"MissingIPAddress",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -130,11 +134,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "Overlaps": case "Overlaps":
if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) if (AdditionalNetworks == null || AdditionalNetworks.Length == 0)
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("AdditionalNetworks is required for Overlaps operations.");
new ArgumentException("AdditionalNetworks is required for Overlaps operations."),
"MissingAdditionalNetworks",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -144,11 +144,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "Supernet": case "Supernet":
if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) if (AdditionalNetworks == null || AdditionalNetworks.Length == 0)
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("AdditionalNetworks is required for Supernet operations.");
new ArgumentException("AdditionalNetworks is required for Supernet operations."),
"MissingAdditionalNetworks",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -158,11 +154,7 @@ namespace PSOPNSenseAPI.Cmdlets
case "SupernetSummarize": case "SupernetSummarize":
if (AdditionalNetworks == null || AdditionalNetworks.Length == 0) if (AdditionalNetworks == null || AdditionalNetworks.Length == 0)
{ {
WriteError(new ErrorRecord( ProcessingException = new ArgumentException("AdditionalNetworks is required for SupernetSummarize operations.");
new ArgumentException("AdditionalNetworks is required for SupernetSummarize operations."),
"MissingAdditionalNetworks",
ErrorCategory.InvalidArgument,
null));
return; return;
} }
@@ -170,15 +162,6 @@ namespace PSOPNSenseAPI.Cmdlets
break; break;
} }
} }
catch (Exception ex)
{
WriteError(new ErrorRecord(
ex,
"NetworkCalculationError",
ErrorCategory.InvalidOperation,
null));
}
}
private void WriteNetworkInfo(IPNetwork2 network) private void WriteNetworkInfo(IPNetwork2 network)
{ {
@@ -82,9 +82,7 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary> /// <summary>
/// Processes the cmdlet /// Processes the cmdlet
/// </summary> /// </summary>
protected override void ProcessRecord() protected override void ProcessRecordInternal()
{
try
{ {
var cronService = new CronService(ApiClient, Logger); var cronService = new CronService(ApiClient, Logger);
@@ -100,26 +98,33 @@ namespace PSOPNSenseAPI.Cmdlets
Enabled = Enabled.IsPresent ? "1" : "0" Enabled = Enabled.IsPresent ? "1" : "0"
}; };
var createTask = Task.Run(async () => await cronService.CreateJobAsync(job)); // Use our safe execution method
var createResult = createTask.GetAwaiter().GetResult(); var createResult = ExecuteAsyncTask(() => cronService.CreateJobAsync(job));
// Only continue if no exception occurred
if (ProcessingException != null || createResult == null)
{
return;
}
WriteVerbose($"Created cron job with UUID {createResult.Uuid}"); WriteVerbose($"Created cron job with UUID {createResult.Uuid}");
// Apply the changes if requested // Apply the changes if requested
if (Apply.IsPresent) if (Apply.IsPresent)
{ {
var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); // Use our safe execution method
var applyResult = applyTask.GetAwaiter().GetResult(); var applyResult = ExecuteAsyncTask(() => cronService.ApplyChangesAsync());
// Only continue if no exception occurred
if (ProcessingException != null || applyResult == null)
{
return;
}
WriteVerbose($"Cron changes applied: {applyResult.Status}"); WriteVerbose($"Cron changes applied: {applyResult.Status}");
} }
WriteObject(createResult.Uuid); WriteObject(createResult.Uuid);
} }
catch (Exception ex)
{
HandleException(ex);
}
}
} }
} }
@@ -73,14 +73,14 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary> /// <summary>
/// Gets the logger /// Gets the logger
/// </summary> /// </summary>
protected ILogger Logger { get; private set; } protected ILogger Logger { get; set; }
/// <summary> /// <summary>
/// Exception to be processed in ProcessRecord /// Exception to be processed in ProcessRecord
/// </summary> /// </summary>
protected Exception ProcessingException { get; private set; } protected Exception ProcessingException { get; set; }
/// <summary> /// <summary>
/// Handles exceptions by storing them for later processing /// Handles exceptions by storing them for later processing
@@ -60,9 +60,7 @@ namespace PSOPNSenseAPI.Cmdlets
/// <summary> /// <summary>
/// Processes the cmdlet /// Processes the cmdlet
/// </summary> /// </summary>
protected override void ProcessRecord() protected override void ProcessRecordInternal()
{
try
{ {
if (!Force.IsPresent && !ShouldProcess(Name, "Uninstall plugin")) if (!Force.IsPresent && !ShouldProcess(Name, "Uninstall plugin"))
{ {
@@ -71,8 +69,14 @@ namespace PSOPNSenseAPI.Cmdlets
var pluginService = new PluginService(ApiClient, Logger); var pluginService = new PluginService(ApiClient, Logger);
var task = Task.Run(async () => await pluginService.UninstallPluginAsync(Name)); // Use our safe execution method
var result = task.GetAwaiter().GetResult(); var result = ExecuteAsyncTask(() => pluginService.UninstallPluginAsync(Name));
// Only continue if no exception occurred
if (ProcessingException != null || result == null)
{
return;
}
WriteVerbose($"Plugin uninstallation initiated: {result.Status}"); WriteVerbose($"Plugin uninstallation initiated: {result.Status}");
WriteObject($"Plugin uninstallation initiated: {result.Status}"); WriteObject($"Plugin uninstallation initiated: {result.Status}");
@@ -87,8 +91,14 @@ namespace PSOPNSenseAPI.Cmdlets
while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout)) while (DateTime.Now - startTime < TimeSpan.FromSeconds(Timeout))
{ {
var statusTask = Task.Run(async () => await pluginService.GetPluginStatusAsync()); // Use our safe execution method
var statusResult = statusTask.GetAwaiter().GetResult(); var statusResult = ExecuteAsyncTask(() => pluginService.GetPluginStatusAsync());
// Break if an exception occurred
if (ProcessingException != null || statusResult == null)
{
break;
}
if (statusResult.Status == "done") if (statusResult.Status == "done")
{ {
@@ -99,11 +109,8 @@ namespace PSOPNSenseAPI.Cmdlets
} }
else if (statusResult.Status == "error") else if (statusResult.Status == "error")
{ {
WriteError(new ErrorRecord( // Store the exception to be processed in ProcessRecord
new Exception($"Plugin uninstallation failed: {statusResult.Log}"), ProcessingException = new Exception($"Plugin uninstallation failed: {statusResult.Log}");
"PluginUninstallationFailed",
ErrorCategory.InvalidOperation,
Name));
completed = true; completed = true;
break; break;
} }
@@ -112,16 +119,11 @@ namespace PSOPNSenseAPI.Cmdlets
Thread.Sleep(Interval * 1000); Thread.Sleep(Interval * 1000);
} }
if (!completed) if (!completed && ProcessingException == null)
{ {
WriteWarning($"Timed out waiting for plugin uninstallation to complete after {Timeout} seconds"); WriteWarning($"Timed out waiting for plugin uninstallation to complete after {Timeout} seconds");
} }
} }
} }
catch (Exception ex)
{
HandleException(ex);
}
}
} }
} }
+1 -1
View File
@@ -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.1216' ModuleVersion = '2025.04.15.1228'
# Supported PSEditions # Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core') CompatiblePSEditions = @('Desktop', 'Core')