From 876085c6f0845c8e4588b6c08def94a7bb79f954 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 19 Mar 2018 09:13:05 -0700 Subject: [PATCH] Stop 'ConvertTo-Json' when 'Ctrl+c' is hit (#6392) --- .../utility/WebCmdlet/ConvertToJsonCommand.cs | 288 ++---------------- .../resources/UtilityCommonStrings.resx | 3 + .../ConvertTo-Json.Tests.ps1 | 19 ++ 3 files changed, 43 insertions(+), 267 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs index a51779dc18..d6431fd3ff 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs @@ -107,7 +107,15 @@ namespace Microsoft.PowerShell.Commands object objectToProcess = (_inputObjects.Count > 1) ? (_inputObjects.ToArray() as object) : (_inputObjects[0]); // Pre-process the object so that it serializes the same, except that properties whose // values cannot be evaluated are treated as having the value null. - object preprocessedObject = ProcessValue(objectToProcess, 0); + object preprocessedObject = null; + try + { + preprocessedObject = ProcessValue(objectToProcess, 0); + } + catch (StoppingException) + { + return; + } JsonSerializerSettings jsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None, MaxDepth = 1024 }; if (EnumsAsStrings) { @@ -124,272 +132,6 @@ namespace Microsoft.PowerShell.Commands #endregion overrides - #region convertOutputToPrettierFormat - - /// - /// Convert the Json string to a more readable format - /// - /// - /// - private string ConvertToPrettyJsonString(string json) - { - if (!json.StartsWith("{", StringComparison.OrdinalIgnoreCase) && !json.StartsWith("[", StringComparison.OrdinalIgnoreCase)) - { - return json; - } - - StringBuilder retStr = new StringBuilder(); - if (json.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - { - retStr.Append('{'); - ConvertDictionary(json, 1, retStr, "", 0); - } - else if (json.StartsWith("[", StringComparison.OrdinalIgnoreCase)) - { - retStr.Append('['); - ConvertList(json, 1, retStr, "", 0); - } - - return retStr.ToString(); - } - - /// - /// Convert a Json List, which starts with '['. - /// - /// - /// - /// - /// - /// - /// - private int ConvertList(string json, int index, StringBuilder result, string padString, int numberOfSpaces) - { - result.Append("\r\n"); - StringBuilder newPadString = new StringBuilder(); - newPadString.Append(padString); - AddSpaces(numberOfSpaces, newPadString); - AddIndentations(1, newPadString); - - bool headChar = true; - - for (int i = index; i < json.Length; i++) - { - switch (json[i]) - { - case '{': - result.Append(newPadString.ToString()); - result.Append(json[i]); - i = ConvertDictionary(json, i + 1, result, newPadString.ToString(), 0); - headChar = false; - break; - case '[': - result.Append(newPadString.ToString()); - result.Append(json[i]); - i = ConvertList(json, i + 1, result, newPadString.ToString(), 0); - headChar = false; - break; - case ']': - result.Append("\r\n"); - result.Append(padString); - AddSpaces(numberOfSpaces, result); - result.Append(json[i]); - return i; - case '"': - if (headChar) - { - result.Append(newPadString.ToString()); - } - result.Append(json[i]); - i = ConvertQuotedString(json, i + 1, result); - headChar = false; - break; - case ',': - result.Append(json[i]); - result.Append("\r\n"); - headChar = true; - break; - default: - if (headChar) - { - result.Append(newPadString.ToString()); - } - result.Append(json[i]); - headChar = false; - break; - } - } - - Dbg.Diagnostics.Assert(false, "ConvertDictionary should return when encounter '}'"); - ThrowTerminatingError(NewError()); - return -1; - } - - /// - /// Convert the quoted string. - /// - /// - /// - /// - /// - private int ConvertQuotedString(string json, int index, StringBuilder result) - { - for (int i = index; i < json.Length; i++) - { - result.Append(json[i]); - if (json[i] == '"') - { - // Ensure that the quote is not escaped by iteratively searching backwards for the backslash. - // Examples: - // "a \" b" --> here second quote is escaped - // "c:\\" --> here second quote is not escaped - // - var j = i; - var escaped = false; - while (j > 0 && json[--j] == '\\') - { - escaped = !escaped; - } - - if (!escaped) - { - return i; - } - } - } - - Dbg.Diagnostics.Assert(false, "ConvertDictionary should return when encounter '}'"); - ThrowTerminatingError(NewError()); - return -1; - } - - /// - /// Convert a Json dictionary, which starts with '{'. - /// - /// - /// - /// - /// - /// - /// - private int ConvertDictionary(string json, int index, StringBuilder result, string padString, int numberOfSpaces) - { - result.Append("\r\n"); - StringBuilder newPadString = new StringBuilder(); - newPadString.Append(padString); - AddSpaces(numberOfSpaces, newPadString); - AddIndentations(1, newPadString); - - bool headChar = true; - bool beforeQuote = true; - int newSpaceCount = 0; - const int spaceCountAfterQuoteMark = 1; - - for (int i = index; i < json.Length; i++) - { - switch (json[i]) - { - case '{': - result.Append(json[i]); - i = ConvertDictionary(json, i + 1, result, newPadString.ToString(), newSpaceCount); - headChar = false; - break; - case '[': - result.Append(json[i]); - i = ConvertList(json, i + 1, result, newPadString.ToString(), newSpaceCount); - headChar = false; - break; - case '}': - result.Append("\r\n"); - result.Append(padString); - AddSpaces(numberOfSpaces, result); - result.Append(json[i]); - return i; - case '"': - if (headChar) - { - result.Append(newPadString.ToString()); - } - result.Append(json[i]); - int end = ConvertQuotedString(json, i + 1, result); - if (beforeQuote) - { - newSpaceCount = 0; - } - i = end; - headChar = false; - break; - case ':': - result.Append(json[i]); - AddSpaces(spaceCountAfterQuoteMark, result); - headChar = false; - beforeQuote = false; - break; - case ',': - result.Append(json[i]); - result.Append("\r\n"); - headChar = true; - beforeQuote = true; - newSpaceCount = 0; - break; - default: - if (headChar) - { - result.Append(newPadString.ToString()); - } - result.Append(json[i]); - if (beforeQuote) - { - newSpaceCount += 1; - } - headChar = false; - break; - } - } - - Dbg.Diagnostics.Assert(false, "ConvertDictionary should return when encounter '}'"); - ThrowTerminatingError(NewError()); - return -1; - } - - /// - /// Add tabs to result - /// - /// - /// - private void AddIndentations(int numberOfTabsToReturn, StringBuilder result) - { - int realNumber = numberOfTabsToReturn * 2; - for (int i = 0; i < realNumber; i++) - { - result.Append(' '); - } - } - - /// - /// Add spaces to result - /// - /// - /// - private void AddSpaces(int numberOfSpacesToReturn, StringBuilder result) - { - for (int i = 0; i < numberOfSpacesToReturn; i++) - { - result.Append(' '); - } - } - - private ErrorRecord NewError() - { - ErrorRecord errorRecord = new ErrorRecord( - new InvalidOperationException(WebCmdletStrings.JsonStringInBadFormat), - "JsonStringInBadFormat", - ErrorCategory.InvalidOperation, - InputObject); - return errorRecord; - } - - #endregion convertOutputToPrettierFormat - /// /// Return an alternate representation of the specified object that serializes the same JSON, except /// that properties that cannot be evaluated are treated as having the value null. @@ -401,6 +143,11 @@ namespace Microsoft.PowerShell.Commands /// An object suitable for serializing to JSON private object ProcessValue(object obj, int depth) { + if (Stopping) + { + throw new StoppingException(); + } + PSObject pso = obj as PSObject; if (pso != null) @@ -432,6 +179,8 @@ namespace Microsoft.PowerShell.Commands else { TypeInfo t = obj.GetType().GetTypeInfo(); + WriteVerbose(StringUtil.Format(UtilityCommonStrings.ConvertToJsonProcessValueVerboseMessage, t.Name, depth)); + if (t.IsPrimitive) { @@ -683,5 +432,10 @@ namespace Microsoft.PowerShell.Commands } return result; } + + /// + /// Exception used for Stopping. + /// + private class StoppingException : System.Exception {} } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx index 651d75d1a3..ce98a18260 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx @@ -177,4 +177,7 @@ The file is not blocked: {0} + + Processing object of type [{0}] at depth {1} + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 092a40baa2..8e9336a51f 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -16,4 +16,23 @@ Describe 'ConvertTo-Json' -tags "CI" { $jsonFormat | Should Match '"TestValue2": 78910' $jsonFormat | Should Match '"TestValue3": 99999' } + + It "StopProcessing should succeed" { + $ps = [PowerShell]::Create() + $null = $ps.AddScript({ + $obj = [PSCustomObject]@{P1 = ''; P2 = ''; P3 = ''; P4 = ''; P5 = ''; P6 = ''} + $obj.P1 = $obj.P2 = $obj.P3 = $obj.P4 = $obj.P5 = $obj.P6 = $obj + 1..100 | Foreach-Object { $obj } | ConvertTo-Json -Depth 10 -Verbose + # the conversion is expected to take some time, this throw is in case it doesn't + throw "Should not have thrown exception" + }) + $null = $ps.BeginInvoke() + # wait for verbose message from ConvertTo-Json to ensure cmdlet is processing + Wait-UntilTrue { $ps.Streams.Verbose.Count -gt 0 } + $null = $ps.BeginStop($null, $null) + # wait a bit to ensure state has changed, not using synchronous Stop() to avoid blocking Pester + Start-Sleep -Milliseconds 100 + $ps.InvocationStateInfo.State | Should -BeExactly "Stopped" + $ps.Dispose() + } }