From 04b0fd555061ae347b3604ef10d5a7d95a352331 Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Tue, 15 Apr 2025 12:45:29 -0400 Subject: [PATCH] Fixed additional threading issues in PowerShell cmdlets to ensure WriteObject and WriteError are only called from the main thread --- .../Cmdlets/DisableOPNSenseCronJobCmdlet.cs | 43 +-- .../Cmdlets/EnableOPNSenseTailscaleCmdlet.cs | 249 ++++++++++-------- .../GetOPNSenseTailscaleStatusCmdlet.cs | 107 ++++---- .../NewOPNSenseDNSForwardingHostCmdlet.cs | 59 +++-- .../Cmdlets/SetOPNSenseCronJobCmdlet.cs | 120 +++++---- src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 | 2 +- 6 files changed, 323 insertions(+), 257 deletions(-) diff --git a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs index 4472a62..c359914 100644 --- a/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/DisableOPNSenseCronJobCmdlet.cs @@ -34,29 +34,34 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void ProcessRecord() + protected override void ProcessRecordInternal() { - try + var cronService = new CronService(ApiClient, Logger); + + // Use our safe execution method + var result = ExecuteAsyncTask(() => cronService.ToggleJobAsync(Uuid, false)); + + // Only continue if no exception occurred + if (ProcessingException != null || result == null) { - var cronService = new CronService(ApiClient, Logger); - - var task = Task.Run(async () => await cronService.ToggleJobAsync(Uuid, false)); - var result = task.GetAwaiter().GetResult(); - - WriteVerbose($"Cron job {Uuid} disabled: {result.Result}"); - - // Apply the changes if requested - if (Apply.IsPresent) - { - var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); - var applyResult = applyTask.GetAwaiter().GetResult(); - - WriteVerbose($"Cron changes applied: {applyResult.Status}"); - } + return; } - catch (Exception ex) + + WriteVerbose($"Cron job {Uuid} disabled: {result.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) { - HandleException(ex); + // Use our safe execution method + var applyResult = ExecuteAsyncTask(() => cronService.ApplyChangesAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || applyResult == null) + { + return; + } + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); } } } diff --git a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs index 6e28778..53db5de 100644 --- a/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/EnableOPNSenseTailscaleCmdlet.cs @@ -105,131 +105,164 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void ProcessRecord() + protected override void ProcessRecordInternal() { - try + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalled = ExecuteAsyncTask(() => tailscaleService.IsPluginInstalledAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null) { - var tailscaleService = new TailscaleService(ApiClient, Logger); + return; + } - // Check if the plugin is installed - var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); - var isInstalled = isInstalledTask.GetAwaiter().GetResult(); - - if (!isInstalled) + if (!isInstalled) + { + if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Install Tailscale plugin")) { - if (!Force.IsPresent && !ShouldProcess("OPNSense firewall", "Install Tailscale plugin")) - { - WriteWarning("Tailscale plugin is not installed and -Force was not specified. Operation cancelled."); - return; - } - - WriteVerbose("Tailscale plugin is not installed. Installing..."); - var installTask = Task.Run(async () => await tailscaleService.InstallPluginAsync()); - var installResult = installTask.GetAwaiter().GetResult(); - - if (!installResult) - { - WriteError(new ErrorRecord( - new Exception("Failed to install Tailscale plugin."), - "TailscalePluginInstallFailed", - ErrorCategory.InvalidOperation, - null)); - return; - } - - WriteVerbose("Tailscale plugin installed successfully."); + WriteWarning("Tailscale plugin is not installed and -Force was not specified. Operation cancelled."); + return; } - // Get current settings - var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); - var currentSettings = settingsTask.GetAwaiter().GetResult().General; + WriteVerbose("Tailscale plugin is not installed. Installing..."); + var installResult = ExecuteAsyncTask(() => tailscaleService.InstallPluginAsync()); - // Process subnet routes if provided - string routesToAdvertise = currentSettings.RoutesToAdvertise; - if (SubnetRoutes != null && SubnetRoutes.Length > 0) - { - routesToAdvertise = string.Join(",", SubnetRoutes); - WriteVerbose($"Advertising subnet routes: {routesToAdvertise}"); - - // If routes are specified but AdvertiseRoutes is not set, enable it automatically - if (!AdvertiseRoutes.IsPresent) - { - WriteVerbose("Automatically enabling route advertisement because subnet routes were specified."); - AdvertiseRoutes = true; - } - } - - // Create new settings - var settings = new TailscaleSettings - { - Enabled = "1", - AcceptDns = AcceptDns.IsPresent ? "1" : "0", - AcceptRoutes = AcceptRoutes.IsPresent ? "1" : "0", - AdvertiseExitNode = AdvertiseExitNode.IsPresent ? "1" : "0", - AdvertiseRoutes = AdvertiseRoutes.IsPresent ? "1" : "0", - RoutesToAdvertise = routesToAdvertise, - Hostname = Hostname ?? currentSettings.Hostname, - LoginServer = LoginServer ?? currentSettings.LoginServer, - Ssh = EnableSsh.IsPresent ? "1" : "0", - Ephemeral = Ephemeral.IsPresent ? "1" : "0", - ResetOnStart = ResetOnStart.IsPresent ? "1" : "0" - }; - - if (!ShouldProcess("OPNSense firewall", "Enable and configure Tailscale")) + // Only continue if no exception occurred + if (ProcessingException != null) { return; } - // Update settings - var updateTask = Task.Run(async () => await tailscaleService.UpdateSettingsAsync(settings)); - var updateResult = updateTask.GetAwaiter().GetResult(); - - WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); - - // Get current status - var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); - var status = statusTask.GetAwaiter().GetResult(); - - // Start or restart the service if requested - if (Restart.IsPresent || (Start.IsPresent && !status.Running)) + if (!installResult) { - if (status.Running) - { - if (Restart.IsPresent) - { - WriteVerbose("Restarting Tailscale service..."); - var restartTask = Task.Run(async () => await tailscaleService.RestartServiceAsync()); - var restartResult = restartTask.GetAwaiter().GetResult(); - WriteVerbose($"Tailscale service restarted: {restartResult.Status}"); - } - } - else if (Start.IsPresent) - { - WriteVerbose("Starting Tailscale service..."); - var startTask = Task.Run(async () => await tailscaleService.StartServiceAsync()); - var startResult = startTask.GetAwaiter().GetResult(); - WriteVerbose($"Tailscale service started: {startResult.Status}"); - } + ProcessingException = new Exception("Failed to install Tailscale plugin."); + return; } - // Get updated status - statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); - status = statusTask.GetAwaiter().GetResult(); - - // Create result object - var result = new PSObject(); - result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); - result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); - result.Properties.Add(new PSNoteProperty("Running", status.Running)); - result.Properties.Add(new PSNoteProperty("Status", status.Status)); - result.Properties.Add(new PSNoteProperty("Settings", settings)); - - WriteObject(result); + WriteVerbose("Tailscale plugin installed successfully."); } - catch (Exception ex) + + // Get current settings + var settingsResult = ExecuteAsyncTask(() => tailscaleService.GetSettingsAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || settingsResult == null) { - HandleException(ex); + return; } + + var currentSettings = settingsResult.General; + + // Process subnet routes if provided + string routesToAdvertise = currentSettings.RoutesToAdvertise; + if (SubnetRoutes != null && SubnetRoutes.Length > 0) + { + routesToAdvertise = string.Join(",", SubnetRoutes); + WriteVerbose($"Advertising subnet routes: {routesToAdvertise}"); + + // If routes are specified but AdvertiseRoutes is not set, enable it automatically + if (!AdvertiseRoutes.IsPresent) + { + WriteVerbose("Automatically enabling route advertisement because subnet routes were specified."); + AdvertiseRoutes = true; + } + } + + // Create new settings + var settings = new TailscaleSettings + { + Enabled = "1", + AcceptDns = AcceptDns.IsPresent ? "1" : "0", + AcceptRoutes = AcceptRoutes.IsPresent ? "1" : "0", + AdvertiseExitNode = AdvertiseExitNode.IsPresent ? "1" : "0", + AdvertiseRoutes = AdvertiseRoutes.IsPresent ? "1" : "0", + RoutesToAdvertise = routesToAdvertise, + Hostname = Hostname ?? currentSettings.Hostname, + LoginServer = LoginServer ?? currentSettings.LoginServer, + Ssh = EnableSsh.IsPresent ? "1" : "0", + Ephemeral = Ephemeral.IsPresent ? "1" : "0", + ResetOnStart = ResetOnStart.IsPresent ? "1" : "0" + }; + + if (!ShouldProcess("OPNSense firewall", "Enable and configure Tailscale")) + { + return; + } + + // Update settings + var updateResult = ExecuteAsyncTask(() => tailscaleService.UpdateSettingsAsync(settings)); + + // Only continue if no exception occurred + if (ProcessingException != null || updateResult == null) + { + return; + } + + WriteVerbose($"Tailscale settings updated: {updateResult.Result}"); + + // Get current status + var status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || status == null) + { + return; + } + + // Start or restart the service if requested + if (Restart.IsPresent || (Start.IsPresent && !status.Running)) + { + if (status.Running) + { + if (Restart.IsPresent) + { + WriteVerbose("Restarting Tailscale service..."); + var restartResult = ExecuteAsyncTask(() => tailscaleService.RestartServiceAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || restartResult == null) + { + return; + } + + WriteVerbose($"Tailscale service restarted: {restartResult.Status}"); + } + } + else if (Start.IsPresent) + { + WriteVerbose("Starting Tailscale service..."); + var startResult = ExecuteAsyncTask(() => tailscaleService.StartServiceAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || startResult == null) + { + return; + } + + WriteVerbose($"Tailscale service started: {startResult.Status}"); + } + } + + // Get updated status + status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || status == null) + { + return; + } + + // Create result object + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + result.Properties.Add(new PSNoteProperty("Settings", settings)); + + WriteObject(result); } } } diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs index a25f17b..4c96cbe 100644 --- a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseTailscaleStatusCmdlet.cs @@ -33,64 +33,77 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void ProcessRecord() + protected override void ProcessRecordInternal() { - try + var tailscaleService = new TailscaleService(ApiClient, Logger); + + // Check if the plugin is installed + var isInstalled = ExecuteAsyncTask(() => tailscaleService.IsPluginInstalledAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null) { - var tailscaleService = new TailscaleService(ApiClient, Logger); + return; + } - // Check if the plugin is installed - var isInstalledTask = Task.Run(async () => await tailscaleService.IsPluginInstalledAsync()); - var isInstalled = isInstalledTask.GetAwaiter().GetResult(); + if (!isInstalled) + { + WriteWarning("Tailscale plugin is not installed on the OPNSense firewall."); - if (!isInstalled) + var statusResult = new PSObject(); + statusResult.Properties.Add(new PSNoteProperty("PluginInstalled", false)); + statusResult.Properties.Add(new PSNoteProperty("Running", false)); + statusResult.Properties.Add(new PSNoteProperty("Enabled", false)); + statusResult.Properties.Add(new PSNoteProperty("Status", "Plugin not installed")); + + WriteObject(statusResult); + return; + } + + // Get the status + var status = ExecuteAsyncTask(() => tailscaleService.GetStatusAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || status == null) + { + return; + } + + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); + result.Properties.Add(new PSNoteProperty("Running", status.Running)); + result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); + result.Properties.Add(new PSNoteProperty("Status", status.Status)); + + // Get interfaces if requested + if (IncludeInterfaces.IsPresent) + { + var interfaces = ExecuteAsyncTask(() => tailscaleService.GetInterfacesAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || interfaces == null) { - WriteWarning("Tailscale plugin is not installed on the OPNSense firewall."); - - var statusResult = new PSObject(); - statusResult.Properties.Add(new PSNoteProperty("PluginInstalled", false)); - statusResult.Properties.Add(new PSNoteProperty("Running", false)); - statusResult.Properties.Add(new PSNoteProperty("Enabled", false)); - statusResult.Properties.Add(new PSNoteProperty("Status", "Plugin not installed")); - - WriteObject(statusResult); return; } - // Get the status - var statusTask = Task.Run(async () => await tailscaleService.GetStatusAsync()); - var status = statusTask.GetAwaiter().GetResult(); - - var result = new PSObject(); - result.Properties.Add(new PSNoteProperty("PluginInstalled", true)); - result.Properties.Add(new PSNoteProperty("Running", status.Running)); - result.Properties.Add(new PSNoteProperty("Enabled", status.Enabled)); - result.Properties.Add(new PSNoteProperty("Status", status.Status)); - - // Get interfaces if requested - if (IncludeInterfaces.IsPresent) - { - var interfacesTask = Task.Run(async () => await tailscaleService.GetInterfacesAsync()); - var interfaces = interfacesTask.GetAwaiter().GetResult(); - - result.Properties.Add(new PSNoteProperty("Interfaces", interfaces.Interfaces)); - } - - // Get settings if requested - if (IncludeSettings.IsPresent) - { - var settingsTask = Task.Run(async () => await tailscaleService.GetSettingsAsync()); - var settings = settingsTask.GetAwaiter().GetResult(); - - result.Properties.Add(new PSNoteProperty("Settings", settings.General)); - } - - WriteObject(result); + result.Properties.Add(new PSNoteProperty("Interfaces", interfaces.Interfaces)); } - catch (Exception ex) + + // Get settings if requested + if (IncludeSettings.IsPresent) { - HandleException(ex); + var settings = ExecuteAsyncTask(() => tailscaleService.GetSettingsAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || settings == null) + { + return; + } + + result.Properties.Add(new PSNoteProperty("Settings", settings.General)); } + + WriteObject(result); } } } diff --git a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs index 1f35a97..a0e86a9 100644 --- a/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/NewOPNSenseDNSForwardingHostCmdlet.cs @@ -53,40 +53,45 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void ProcessRecord() + protected override void ProcessRecordInternal() { - try + var dnsService = new DNSService(ApiClient, Logger); + + var host = new DNSForwardingHostConfig { - var dnsService = new DNSService(ApiClient, Logger); + Domain = Domain, + Server = Server, + Description = Description, + Enabled = Enabled.IsPresent ? "1" : "0" + }; - var host = new DNSForwardingHostConfig + // Use our safe execution method + var createResult = ExecuteAsyncTask(() => dnsService.CreateDNSForwardingHostAsync(host)); + + // Only continue if no exception occurred + if (ProcessingException != null || createResult == null) + { + return; + } + + WriteVerbose($"Created DNS forwarding host with UUID {createResult.Uuid}"); + + // Apply changes if requested + if (Apply.IsPresent) + { + // Use our safe execution method + var applyResult = ExecuteAsyncTask(() => dnsService.ApplyDNSChangesAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || applyResult == null) { - Domain = Domain, - Server = Server, - Description = Description, - Enabled = Enabled.IsPresent ? "1" : "0" - }; - - var createTask = Task.Run(async () => await dnsService.CreateDNSForwardingHostAsync(host)); - var createResult = createTask.GetAwaiter().GetResult(); - - WriteVerbose($"Created DNS forwarding host with UUID {createResult.Uuid}"); - - // Apply changes if requested - if (Apply.IsPresent) - { - var applyTask = Task.Run(async () => await dnsService.ApplyDNSChangesAsync()); - var applyResult = applyTask.GetAwaiter().GetResult(); - - WriteVerbose($"DNS changes applied: {applyResult.Status}"); + return; } - WriteObject(createResult.Uuid); - } - catch (Exception ex) - { - HandleException(ex); + WriteVerbose($"DNS changes applied: {applyResult.Status}"); } + + WriteObject(createResult.Uuid); } } } diff --git a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs index c5c413e..21a7601 100644 --- a/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/SetOPNSenseCronJobCmdlet.cs @@ -93,65 +93,75 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void ProcessRecord() + protected override void ProcessRecordInternal() { - try + var cronService = new CronService(ApiClient, Logger); + + // First, get the current job + var getResult = ExecuteAsyncTask(() => cronService.GetJobAsync(Uuid)); + + // Only continue if no exception occurred + if (ProcessingException != null || getResult == null) { - var cronService = new CronService(ApiClient, Logger); - - // First, get the current job - var getTask = Task.Run(async () => await cronService.GetJobAsync(Uuid)); - var currentJob = getTask.GetAwaiter().GetResult().Job; - - // Create the updated job - var job = new CronJobConfig - { - Description = Description ?? currentJob.Description, - Command = Command ?? currentJob.Command, - Minutes = Minutes ?? currentJob.Minutes, - Hours = Hours ?? currentJob.Hours, - Days = Days ?? currentJob.Days, - Months = Months ?? currentJob.Months, - Weekdays = Weekdays ?? currentJob.Weekdays - }; - - // Handle enabled/disabled state - if (Enabled.IsPresent && Disabled.IsPresent) - { - WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); - job.Enabled = "1"; - } - else if (Enabled.IsPresent) - { - job.Enabled = "1"; - } - else if (Disabled.IsPresent) - { - job.Enabled = "0"; - } - else - { - job.Enabled = currentJob.Enabled; - } - - // Update the job - var updateTask = Task.Run(async () => await cronService.UpdateJobAsync(Uuid, job)); - var updateResult = updateTask.GetAwaiter().GetResult(); - - WriteVerbose($"Cron job {Uuid} updated: {updateResult.Result}"); - - // Apply the changes if requested - if (Apply.IsPresent) - { - var applyTask = Task.Run(async () => await cronService.ApplyChangesAsync()); - var applyResult = applyTask.GetAwaiter().GetResult(); - - WriteVerbose($"Cron changes applied: {applyResult.Status}"); - } + return; } - catch (Exception ex) + + var currentJob = getResult.Job; + + // Create the updated job + var job = new CronJobConfig { - HandleException(ex); + Description = Description ?? currentJob.Description, + Command = Command ?? currentJob.Command, + Minutes = Minutes ?? currentJob.Minutes, + Hours = Hours ?? currentJob.Hours, + Days = Days ?? currentJob.Days, + Months = Months ?? currentJob.Months, + Weekdays = Weekdays ?? currentJob.Weekdays + }; + + // Handle enabled/disabled state + if (Enabled.IsPresent && Disabled.IsPresent) + { + WriteWarning("Both -Enabled and -Disabled parameters were specified. Using -Enabled."); + job.Enabled = "1"; + } + else if (Enabled.IsPresent) + { + job.Enabled = "1"; + } + else if (Disabled.IsPresent) + { + job.Enabled = "0"; + } + else + { + job.Enabled = currentJob.Enabled; + } + + // Update the job + var updateResult = ExecuteAsyncTask(() => cronService.UpdateJobAsync(Uuid, job)); + + // Only continue if no exception occurred + if (ProcessingException != null || updateResult == null) + { + return; + } + + WriteVerbose($"Cron job {Uuid} updated: {updateResult.Result}"); + + // Apply the changes if requested + if (Apply.IsPresent) + { + var applyResult = ExecuteAsyncTask(() => cronService.ApplyChangesAsync()); + + // Only continue if no exception occurred + if (ProcessingException != null || applyResult == null) + { + return; + } + + WriteVerbose($"Cron changes applied: {applyResult.Status}"); } } } diff --git a/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 index 4d7dcf9..096df98 100644 --- a/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 +++ b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 @@ -3,7 +3,7 @@ RootModule = 'lib\PSOPNSenseAPI.dll' # Version number of this module. - ModuleVersion = '2025.04.15.1234' + ModuleVersion = '2025.04.15.1245' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core')