diff --git a/src/Common/Helpers/ServiceBusHelper.cs b/src/Common/Helpers/ServiceBusHelper.cs index 62a8cee0..22928352 100644 --- a/src/Common/Helpers/ServiceBusHelper.cs +++ b/src/Common/Helpers/ServiceBusHelper.cs @@ -5337,7 +5337,7 @@ public string GetAddressRelativeToNamespace(string address) public ServiceBusHelper2 GetServiceBusHelper2() { - var serviceBusHelper2 = new ServiceBusHelper2(); + var serviceBusHelper2 = new ServiceBusHelper2(writeToLog); serviceBusHelper2.ConnectionString = ConnectionString; serviceBusHelper2.TransportType = UseAmqpWebSockets ? Azure.Messaging.ServiceBus.ServiceBusTransportType.AmqpWebSockets diff --git a/src/ServiceBus/Helpers/CancelScheduledMessagesHelper.cs b/src/ServiceBus/Helpers/CancelScheduledMessagesHelper.cs new file mode 100644 index 00000000..eb40876f --- /dev/null +++ b/src/ServiceBus/Helpers/CancelScheduledMessagesHelper.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +using Azure.Messaging.ServiceBus; + +using ServiceBusExplorer.Utilities.Helpers; + +namespace ServiceBusExplorer.ServiceBus.Helpers +{ + public static class CancelScheduledMessagesHelper + { + class OperationStatus + { + public int Successes; + public int Failures; + } + + public static async Task CancelScheduledMessages(ServiceBusHelper2 serviceBusHelper, + string queueName, List sequenceNumbersToCancel) + { + var client = serviceBusHelper.CreateServiceBusClient(); + + try + { + var sender = client.CreateSender(queueName); + + serviceBusHelper.WriteToLog($"Starting cancellation of scheduled messages on queue {queueName}."); + + var operationStatus = new OperationStatus(); + + var stopwatch = Stopwatch.StartNew(); + var semaphore = new SemaphoreSlim(40); // As recommended by https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-transfers-locks-settlement#settling-send-operations + var tasks = new List(sequenceNumbersToCancel.Count); + + foreach (long sequenceNumber in sequenceNumbersToCancel) + { + await semaphore.WaitAsync(); + tasks.Add(CancelScheduledMessageWithLog(sender, sequenceNumber, serviceBusHelper.WriteToLog, operationStatus) + .ContinueWith((t, state) => ((SemaphoreSlim)state)?.Release(), semaphore)); + } + + await Task.WhenAll(tasks); + stopwatch.Stop(); + + Func singleOrPlural = (c) => c > 1 ? "messages" : "message"; + + serviceBusHelper.WriteToLog($"Successfully cancelled {operationStatus.Successes} " + + $"scheduled {singleOrPlural(operationStatus.Successes)} in {stopwatch.Elapsed}."); + + if (operationStatus.Failures > 0) + { + serviceBusHelper.WriteToLog($"Failed to cancel {operationStatus.Failures} " + + $"{singleOrPlural(operationStatus.Failures)}."); + } + } + finally + { + await client.DisposeAsync(); + } + } + + static Task CancelScheduledMessageWithLog(ServiceBusSender sender, + long sequenceNumber, WriteToLogDelegate writeToLog, OperationStatus operationStatus) + { + Task task = sender.CancelScheduledMessageAsync(sequenceNumber) + .ContinueWith(_ => + { + writeToLog($"Cancelled scheduled message with sequence number {sequenceNumber}."); + Interlocked.Increment(ref operationStatus.Successes); + }, + TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously) + .ContinueWith(_ => + { + writeToLog($"Failed to cancel scheduled message with sequence number {sequenceNumber}."); + Interlocked.Increment(ref operationStatus.Failures); + }, + TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously); + + return task; + } + } +} diff --git a/src/ServiceBus/Helpers/ServiceBusHelper2.cs b/src/ServiceBus/Helpers/ServiceBusHelper2.cs index 468fafd0..b81a397e 100644 --- a/src/ServiceBus/Helpers/ServiceBusHelper2.cs +++ b/src/ServiceBus/Helpers/ServiceBusHelper2.cs @@ -20,22 +20,40 @@ #endregion using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; +using ServiceBusExplorer.Utilities.Helpers; + // ReSharper disable CheckNamespace namespace ServiceBusExplorer.ServiceBus.Helpers // ReSharper restore CheckNamespace { public class ServiceBusHelper2 { + readonly WriteToLogDelegate writeToLog; + public string ConnectionString { get; set; } public ServiceBusTransportType TransportType { get; set; } + public WriteToLogDelegate WriteToLog + { + get + { + return writeToLog; + } + } + + public ServiceBusHelper2(WriteToLogDelegate writeToLog) + { + this.writeToLog = writeToLog; + } + public bool ConnectionStringContainsEntityPath() { var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(ConnectionString); - + if (connectionStringProperties?.EntityPath != null) { return true; @@ -44,6 +62,17 @@ public bool ConnectionStringContainsEntityPath() return false; } + /// + /// Dispose of the returned ServiceBusClient object by calling DisposeAsync(). + /// + /// An Azure.Messaging.ServiceBus.ServiceBusClient + public ServiceBusClient CreateServiceBusClient() + { + return new ServiceBusClient( + ConnectionString, + new ServiceBusClientOptions { TransportType = this.TransportType }); + } + public async Task IsPremiumNamespace() { var administrationClient = new ServiceBusAdministrationClient(ConnectionString); diff --git a/src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs b/src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs index f486679c..c5c6c99d 100644 --- a/src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs +++ b/src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs @@ -117,7 +117,9 @@ private void InitializeComponent() this.messagesContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); this.repairAndResubmitMessageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.resubmitMessageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.selectAllMessagesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.resubmitSelectedMessagesInBatchModeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.cancelScheduledMessageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.saveSelectedMessageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.saveSelectedMessageBodyAsFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -127,6 +129,7 @@ private void InitializeComponent() this.repairAndResubmitSharedDeadletterToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.resubmitSharedDeadletterToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.resubmitSelectedSharedDeadletterInBatchModeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.selectAllSharedDeadletterMessagesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.saveSelectedSharedDeadletteredMessageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -2085,14 +2088,16 @@ private void InitializeComponent() this.messagesContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.repairAndResubmitMessageToolStripMenuItem, this.resubmitMessageToolStripMenuItem, + this.selectAllMessagesToolStripMenuItem, this.resubmitSelectedMessagesInBatchModeToolStripMenuItem, + this.cancelScheduledMessageToolStripMenuItem, this.toolStripSeparator1, this.saveSelectedMessageToolStripMenuItem, this.saveSelectedMessageBodyAsFileToolStripMenuItem, this.saveSelectedMessagesToolStripMenuItem, this.saveSelectedMessagesBodyAsFileToolStripMenuItem}); this.messagesContextMenuStrip.Name = "registrationContextMenuStrip"; - this.messagesContextMenuStrip.Size = new System.Drawing.Size(306, 164); + this.messagesContextMenuStrip.Size = new System.Drawing.Size(306, 208); // // repairAndResubmitMessageToolStripMenuItem // @@ -2109,6 +2114,14 @@ private void InitializeComponent() this.resubmitMessageToolStripMenuItem.ToolTipText = "Resubmits the message with unchanged body."; this.resubmitMessageToolStripMenuItem.Click += new System.EventHandler(this.resubmitMessageToolStripMenuItem_Click); // + // selectAllMessagesToolStripMenuItem + // + this.selectAllMessagesToolStripMenuItem.Name = "selectAllMessagesToolStripMenuItem"; + this.selectAllMessagesToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.A))); + this.selectAllMessagesToolStripMenuItem.Size = new System.Drawing.Size(305, 22); + this.selectAllMessagesToolStripMenuItem.Text = "Select All Messages"; + this.selectAllMessagesToolStripMenuItem.Click += new System.EventHandler(this.selectAllMessagesToolStripMenuItem_Click); + // // resubmitSelectedMessagesInBatchModeToolStripMenuItem // this.resubmitSelectedMessagesInBatchModeToolStripMenuItem.Name = "resubmitSelectedMessagesInBatchModeToolStripMenuItem"; @@ -2116,6 +2129,14 @@ private void InitializeComponent() this.resubmitSelectedMessagesInBatchModeToolStripMenuItem.Text = "Resubmit Selected Messages In Batch Mode"; this.resubmitSelectedMessagesInBatchModeToolStripMenuItem.Click += new System.EventHandler(this.resubmitSelectedMessagesInBatchModeToolStripMenuItem_Click); // + // cancelScheduledMessageToolStripMenuItem + // + this.cancelScheduledMessageToolStripMenuItem.Name = "cancelScheduledMessageToolStripMenuItem"; + this.cancelScheduledMessageToolStripMenuItem.Size = new System.Drawing.Size(305, 22); + this.cancelScheduledMessageToolStripMenuItem.Text = "Cancel Selected Scheduled Message"; + this.cancelScheduledMessageToolStripMenuItem.Visible = false; + this.cancelScheduledMessageToolStripMenuItem.Click += new System.EventHandler(this.cancelScheduledMessageToolStripMenuItem_Click); + // // toolStripSeparator1 // this.toolStripSeparator1.Name = "toolStripSeparator1"; @@ -2156,6 +2177,7 @@ private void InitializeComponent() this.repairAndResubmitSharedDeadletterToolStripMenuItem, this.resubmitSharedDeadletterToolStripMenuItem, this.resubmitSelectedSharedDeadletterInBatchModeToolStripMenuItem, + this.selectAllSharedDeadletterMessagesToolStripMenuItem, this.toolStripSeparator2, this.saveSelectedSharedDeadletteredMessageToolStripMenuItem, this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem, @@ -2181,6 +2203,14 @@ private void InitializeComponent() this.resubmitSharedDeadletterToolStripMenuItem.ToolTipText = "Resubmits the deadletter message with unchanged body."; this.resubmitSharedDeadletterToolStripMenuItem.Click += new System.EventHandler(this.resubmitSharedDeadletterMessageToolStripMenuItem_Click); // + // selectAllSharedDeadletterMessagesToolStripMenuItem + // + this.selectAllSharedDeadletterMessagesToolStripMenuItem.Name = "selectAllSharedDeadletterMessagesToolStripMenuItem"; + this.selectAllSharedDeadletterMessagesToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.A))); + this.selectAllSharedDeadletterMessagesToolStripMenuItem.Size = new System.Drawing.Size(305, 22); + this.selectAllSharedDeadletterMessagesToolStripMenuItem.Text = "Select All Messages"; + this.selectAllSharedDeadletterMessagesToolStripMenuItem.Click += new System.EventHandler(this.selectAllDeadletterMessagesToolStripMenuItem_Click); + // // resubmitSelectedSharedDeadletterInBatchModeToolStripMenuItem // this.resubmitSelectedSharedDeadletterInBatchModeToolStripMenuItem.Name = "resubmitSelectedSharedDeadletterInBatchModeToolStripMenuItem"; @@ -2500,5 +2530,8 @@ private void InitializeComponent() private System.Windows.Forms.PropertyGrid transferDeadletterCustomPropertyGrid; private System.Windows.Forms.ToolStripMenuItem resubmitSharedDeadletterToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem resubmitMessageToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem cancelScheduledMessageToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem selectAllMessagesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem selectAllSharedDeadletterMessagesToolStripMenuItem; } } diff --git a/src/ServiceBusExplorer/Controls/HandleQueueControl.cs b/src/ServiceBusExplorer/Controls/HandleQueueControl.cs index 216fa77e..c590fd2d 100644 --- a/src/ServiceBusExplorer/Controls/HandleQueueControl.cs +++ b/src/ServiceBusExplorer/Controls/HandleQueueControl.cs @@ -288,7 +288,7 @@ public partial class HandleQueueControl : UserControl #endregion - #region Public Constructors + #region Public Constructor public HandleQueueControl(WriteToLogDelegate writeToLog, ServiceBusHelper serviceBusHelper, QueueDescription queueDescription, string path, bool duplicateQueue) @@ -520,7 +520,17 @@ public void GetMessageSessions() #endregion - #region Private Methods + #region Private Static Methods + + static bool AreAllSelectedMessageScheduled(DataGridViewSelectedRowCollection selectedRows) + { + return selectedRows.Cast() + .All(row => (row.DataBoundItem as BrokeredMessage)?.State == MessageState.Scheduled); + } + + #endregion + + #region Private Instance Methods private void InitializeControls(bool initialCall) { @@ -2875,14 +2885,17 @@ private void messagesDataGridView_RowEnter(object sender, DataGridViewCellEventA { var bindingList = messagesBindingSource.DataSource as BindingList; currentMessageRowIndex = e.RowIndex; + if (bindingList == null) { return; } + if (brokeredMessage == bindingList[e.RowIndex]) { return; } + brokeredMessage = bindingList[e.RowIndex]; LanguageDetector.SetFormattedMessage(serviceBusHelper, brokeredMessage, txtMessageText); @@ -3244,7 +3257,6 @@ private void grouperDeadletterSystemProperties_CustomPaint(PaintEventArgs obj) deadletterPropertyGrid.Location.X); } - private void grouperTransferDeadletterText_CustomPaint(PaintEventArgs obj) { txtTransferDeadletterText.Size = new Size(grouperTransferDeadletterText.Size.Width - txtTransferDeadletterText.Location.X * 2, @@ -3286,15 +3298,30 @@ private void messagesDataGridView_CellMouseDown(object sender, DataGridViewCellM { return; } + messagesDataGridView.Rows[e.RowIndex].Selected = true; + var multipleSelectedRows = messagesDataGridView.SelectedRows.Count > 1; + repairAndResubmitMessageToolStripMenuItem.Visible = !multipleSelectedRows; resubmitMessageToolStripMenuItem.Visible = !multipleSelectedRows; + + if(AreAllSelectedMessageScheduled(messagesDataGridView.SelectedRows)) + { + SetCancelScheduledMessageToolStripMenuItemText(multipleSelectedRows); + cancelScheduledMessageToolStripMenuItem.Visible = true; + } + else + { + cancelScheduledMessageToolStripMenuItem.Visible = false; + } + saveSelectedMessageToolStripMenuItem.Visible = !multipleSelectedRows; saveSelectedMessageBodyAsFileToolStripMenuItem.Visible = !multipleSelectedRows; resubmitSelectedMessagesInBatchModeToolStripMenuItem.Visible = multipleSelectedRows; saveSelectedMessagesToolStripMenuItem.Visible = multipleSelectedRows; saveSelectedMessagesBodyAsFileToolStripMenuItem.Visible = multipleSelectedRows; + messagesContextMenuStrip.Show(Cursor.Position); } @@ -3314,6 +3341,58 @@ private void resubmitSelectedMessagesInBatchModeToolStripMenuItem_Click(object s ResubmitSelectedMessages(); } + async void cancelScheduledMessageToolStripMenuItem_Click(object sender, EventArgs e) + { + if (messagesDataGridView.SelectedRows.Count <= 0) + { + return; + } + + var configuration = TwoFilesConfiguration.Create(TwoFilesConfiguration.GetCurrentConfigFileUse(), writeToLog); + bool disableAccidentalDeletionPrevention = configuration.GetBoolValue( + ConfigurationParameters.DisableAccidentalDeletionPrevention, + defaultValue: false); + + var thisForm = FindForm(); + + if (!disableAccidentalDeletionPrevention) + { + if(MessageBox.Show(owner: thisForm, + text: "Are you sure you want to cancel the scheduled message(s)\n\n" + + "They will be permanently removed.\n\n" + + "You can disable this check by changing the Disable Accidental Deletion Prevention setting.", + caption: "Cancel Scheduled Message(s)", + buttons: MessageBoxButtons.YesNo, + icon: MessageBoxIcon.Warning, + defaultButton: MessageBoxDefaultButton.Button2) + == DialogResult.No) + { + return; + } + } + + + IEnumerable messages = messagesDataGridView.SelectedRows.Cast() + .Select(r => (BrokeredMessage)r.DataBoundItem).Where(m => m != null); + + List sequenceNumbersToCancel = messages.Select(s => s.SequenceNumber).ToList(); + + + try + { + thisForm.UseWaitCursor = true; + + var serviceBusHelper2 = serviceBusHelper.GetServiceBusHelper2(); + + await CancelScheduledMessagesHelper.CancelScheduledMessages( + serviceBusHelper2, this.queueDescription.Path, sequenceNumbersToCancel); + } + finally + { + thisForm.UseWaitCursor = false; + } + } + private void ResubmitSelectedMessages() { if (messagesDataGridView.SelectedRows.Count <= 0) @@ -3332,6 +3411,13 @@ private void ResubmitSelectedMessages() } } + void SetCancelScheduledMessageToolStripMenuItemText(bool multipleSelectedRows) + { + cancelScheduledMessageToolStripMenuItem.Text = multipleSelectedRows + ? "Cancel Selected Scheduled Messages" + : "Cancel Selected Scheduled Message"; + } + void deleteSelectedSharedDeadLetterMessageToolStripMenuItem_Click(object sender, EventArgs e) { deleteSelectedSharedDeadLetterMessagesToolStripMenuItem_Click(sender, e); @@ -3857,6 +3943,26 @@ protected override void Dispose(bool disposing) } } + private string CreateFileName() + { + return string.Format(MessageFileFormat, + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(serviceBusHelper.Namespace), + DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '-')); + } + + private string CreateFileNameAutoRecognize() + { + return string.Format(MessageFileFormatAutoRecognize, + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(serviceBusHelper.Namespace), + DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '-')); + } + + private async void btnPurgeMessages_Click(object sender, EventArgs e) + { + await PurgeMessagesAsync(); + } + #endregion + #region Save Messages void saveSelectedMessageToolStripMenuItem_Click(object sender, EventArgs e) @@ -4068,7 +4174,6 @@ void saveSelectedSharedDeadletteredMessagesBodyAsFileToolStripMenuItem_Click(obj SaveSelectedMessages(SaveInJsonFormat: false); } - void SaveSelectedMessage(bool SaveInJsonFormat) { var activeGridView = GetActiveDeadletterGridView(); @@ -4227,24 +4332,25 @@ void SaveSelectedMessages(bool SaveInJsonFormat) } } - private string CreateFileName() - { - return string.Format(MessageFileFormat, - CultureInfo.CurrentCulture.TextInfo.ToTitleCase(serviceBusHelper.Namespace), - DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '-')); - } + #endregion Save Messages - private string CreateFileNameAutoRecognize() + private void selectAllMessagesToolStripMenuItem_Click(object sender, EventArgs e) { - return string.Format(MessageFileFormatAutoRecognize, - CultureInfo.CurrentCulture.TextInfo.ToTitleCase(serviceBusHelper.Namespace), - DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace('/', '-').Replace(':', '-')); + messagesDataGridView.SelectAll(); } - #endregion Save Messages - private async void btnPurgeMessages_Click(object sender, EventArgs e) + private void selectAllDeadletterMessagesToolStripMenuItem_Click(object sender, EventArgs e) { - await PurgeMessagesAsync(); + var activeGridView = GetActiveDeadletterGridView(); + + if (activeGridView == deadletterDataGridView) + { + deadletterDataGridView.SelectAll(); + } + else if (activeGridView == transferDeadletterDataGridView) + { + transferDeadletterDataGridView.SelectAll(); + } } private async void btnPurgeDeadletterQueueMessages_Click(object sender, EventArgs e) @@ -4341,5 +4447,4 @@ void RepairAndResubmitSharedDeadletterMessage(DataGridViewCellEventArgs e) } } } - #endregion }