Skip to content

Commit 8d7b152

Browse files
committed
Fix debugger hang when you stop execution while input prompt is active
This change fixes an issue where the debugger will hang when the user tries to stop execution while there is an active input or choice prompt. The solution is to use a more robust method for cancelling the prompt task which doesn't depend on the Console.ReadKey() call to return. Fixes PowerShell#428.
1 parent 6dbb6a6 commit 8d7b152

File tree

4 files changed

+71
-17
lines changed

4 files changed

+71
-17
lines changed

src/PowerShellEditorServices/Console/ChoicePromptHandler.cs

+28-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Linq;
9+
using System.Management.Automation;
910
using System.Threading;
1011
using System.Threading.Tasks;
1112

@@ -31,7 +32,7 @@ public enum PromptStyle
3132
}
3233

3334
/// <summary>
34-
/// Provides a base implementation for IPromptHandler classes
35+
/// Provides a base implementation for IPromptHandler classes
3536
/// that present the user a set of options from which a selection
3637
/// should be made.
3738
/// </summary>
@@ -41,6 +42,8 @@ public abstract class ChoicePromptHandler : PromptHandler
4142

4243
private CancellationTokenSource promptCancellationTokenSource =
4344
new CancellationTokenSource();
45+
private TaskCompletionSource<Dictionary<string, object>> cancelTask =
46+
new TaskCompletionSource<Dictionary<string, object>>();
4447

4548
#endregion
4649

@@ -98,7 +101,7 @@ public abstract class ChoicePromptHandler : PromptHandler
98101
/// A Task instance that can be monitored for completion to get
99102
/// the user's choice.
100103
/// </returns>
101-
public Task<int> PromptForChoice(
104+
public async Task<int> PromptForChoice(
102105
string promptCaption,
103106
string promptMessage,
104107
ChoiceDetails[] choices,
@@ -120,7 +123,7 @@ public Task<int> PromptForChoice(
120123
cancellationToken.Register(this.CancelPrompt, true);
121124

122125
// Convert the int[] result to int
123-
return
126+
return await this.WaitForTask(
124127
this.StartPromptLoop(this.promptCancellationTokenSource.Token)
125128
.ContinueWith(
126129
task =>
@@ -135,7 +138,7 @@ public Task<int> PromptForChoice(
135138
}
136139

137140
return this.GetSingleResult(task.Result);
138-
});
141+
}));
139142
}
140143

141144
/// <summary>
@@ -161,7 +164,7 @@ public Task<int> PromptForChoice(
161164
/// A Task instance that can be monitored for completion to get
162165
/// the user's choices.
163166
/// </returns>
164-
public Task<int[]> PromptForChoice(
167+
public async Task<int[]> PromptForChoice(
165168
string promptCaption,
166169
string promptMessage,
167170
ChoiceDetails[] choices,
@@ -179,7 +182,24 @@ public Task<int[]> PromptForChoice(
179182
// Cancel the TaskCompletionSource if the caller cancels the task
180183
cancellationToken.Register(this.CancelPrompt, true);
181184

182-
return this.StartPromptLoop(this.promptCancellationTokenSource.Token);
185+
return await this.WaitForTask(
186+
this.StartPromptLoop(
187+
this.promptCancellationTokenSource.Token));
188+
}
189+
190+
private async Task<T> WaitForTask<T>(Task<T> taskToWait)
191+
{
192+
Task finishedTask =
193+
await Task.WhenAny(
194+
this.cancelTask.Task,
195+
taskToWait);
196+
197+
if (this.cancelTask.Task.IsCanceled)
198+
{
199+
throw new PipelineStoppedException();
200+
}
201+
202+
return taskToWait.Result;
183203
}
184204

185205
private async Task<int[]> StartPromptLoop(
@@ -232,7 +252,7 @@ private async Task<int[]> StartPromptLoop(
232252
/// </summary>
233253
/// <param name="responseString">The string representing the user's response.</param>
234254
/// <returns>
235-
/// True if the prompt is complete, false if the prompt is
255+
/// True if the prompt is complete, false if the prompt is
236256
/// still waiting for a valid response.
237257
/// </returns>
238258
protected virtual int[] HandleResponse(string responseString)
@@ -280,6 +300,7 @@ protected override void OnPromptCancelled()
280300
{
281301
// Cancel the prompt task
282302
this.promptCancellationTokenSource.Cancel();
303+
this.cancelTask.TrySetCanceled();
283304
}
284305

285306
#endregion

src/PowerShellEditorServices/Console/ConsoleService.cs

+15-3
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,21 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
447447
{
448448
if (this.EnableConsoleRepl)
449449
{
450-
// Any command which writes output to the host will affect
451-
// the display of the prompt
452-
if (eventArgs.ExecutionOptions.WriteOutputToHost ||
450+
if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted)
451+
{
452+
// When aborted, cancel any lingering prompts
453+
if (this.activePromptHandler != null)
454+
{
455+
this.activePromptHandler.CancelPrompt();
456+
this.WriteOutput(string.Empty);
457+
}
458+
}
459+
else if (
460+
eventArgs.ExecutionOptions.WriteOutputToHost ||
453461
eventArgs.ExecutionOptions.InterruptCommandPrompt)
454462
{
463+
// Any command which writes output to the host will affect
464+
// the display of the prompt
455465
if (eventArgs.ExecutionStatus != ExecutionStatus.Running)
456466
{
457467
// Execution has completed, start the input prompt
@@ -461,6 +471,7 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
461471
{
462472
// A new command was started, cancel the input prompt
463473
this.CancelReadLoop();
474+
this.WriteOutput(string.Empty);
464475
}
465476
}
466477
else if (
@@ -469,6 +480,7 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
469480
eventArgs.HadErrors))
470481
{
471482
this.CancelReadLoop();
483+
this.WriteOutput(string.Empty);
472484
this.StartReadLoop();
473485
}
474486
}

src/PowerShellEditorServices/Console/InputPromptHandler.cs

+23-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace Microsoft.PowerShell.EditorServices.Console
1515
{
1616
/// <summary>
17-
/// Provides a base implementation for IPromptHandler classes
17+
/// Provides a base implementation for IPromptHandler classes
1818
/// that present the user a set of fields for which values
1919
/// should be entered.
2020
/// </summary>
@@ -26,6 +26,8 @@ public abstract class InputPromptHandler : PromptHandler
2626
private FieldDetails currentField;
2727
private CancellationTokenSource promptCancellationTokenSource =
2828
new CancellationTokenSource();
29+
private TaskCompletionSource<Dictionary<string, object>> cancelTask =
30+
new TaskCompletionSource<Dictionary<string, object>>();
2931

3032
#endregion
3133

@@ -57,7 +59,7 @@ public Task<string> PromptForInput(
5759
new FieldDetails[] { new FieldDetails("", "", typeof(string), false, "") },
5860
cancellationToken);
5961

60-
return
62+
return
6163
innerTask.ContinueWith<string>(
6264
task =>
6365
{
@@ -69,7 +71,7 @@ public Task<string> PromptForInput(
6971
{
7072
throw new TaskCanceledException(task);
7173
}
72-
74+
7375
// Return the value of the sole field
7476
return (string)task.Result[""];
7577
});
@@ -95,7 +97,7 @@ public Task<string> PromptForInput(
9597
/// A Task instance that can be monitored for completion to get
9698
/// the user's input.
9799
/// </returns>
98-
public Task<Dictionary<string, object>> PromptForInput(
100+
public async Task<Dictionary<string, object>> PromptForInput(
99101
string promptCaption,
100102
string promptMessage,
101103
FieldDetails[] fields,
@@ -108,7 +110,20 @@ public Task<Dictionary<string, object>> PromptForInput(
108110

109111
this.ShowPromptMessage(promptCaption, promptMessage);
110112

111-
return this.StartPromptLoop(this.promptCancellationTokenSource.Token);
113+
Task<Dictionary<string, object>> promptTask =
114+
this.StartPromptLoop(this.promptCancellationTokenSource.Token);
115+
116+
Task finishedTask =
117+
await Task.WhenAny(
118+
cancelTask.Task,
119+
promptTask);
120+
121+
if (this.cancelTask.Task.IsCanceled)
122+
{
123+
throw new PipelineStoppedException();
124+
}
125+
126+
return promptTask.Result;
112127
}
113128

114129
/// <summary>
@@ -128,7 +143,7 @@ public Task<SecureString> PromptForSecureInput(
128143
new FieldDetails[] { new FieldDetails("", "", typeof(SecureString), false, "") },
129144
cancellationToken);
130145

131-
return
146+
return
132147
innerTask.ContinueWith(
133148
task =>
134149
{
@@ -140,7 +155,7 @@ public Task<SecureString> PromptForSecureInput(
140155
{
141156
throw new TaskCanceledException(task);
142157
}
143-
158+
144159
// Return the value of the sole field
145160
return (SecureString)task.Result?[""];
146161
});
@@ -153,6 +168,7 @@ protected override void OnPromptCancelled()
153168
{
154169
// Cancel the prompt task
155170
this.promptCancellationTokenSource.Cancel();
171+
this.cancelTask.TrySetCanceled();
156172
}
157173

158174
#endregion

src/PowerShellEditorServices/Session/PowerShellContext.cs

+5
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,11 @@ public void AbortExecution()
870870
this.powerShell.BeginStop(null, null);
871871

872872
this.SessionState = PowerShellContextState.Aborting;
873+
874+
this.OnExecutionStatusChanged(
875+
ExecutionStatus.Aborted,
876+
null,
877+
false);
873878
}
874879
else
875880
{

0 commit comments

Comments
 (0)