612 lines
22 KiB
C#
612 lines
22 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Management.Automation;
|
|
using System.Management.Automation.Host;
|
|
using System.Management.Automation.Internal;
|
|
using System.Text;
|
|
|
|
using Microsoft.PowerShell.Commands.Internal.Format;
|
|
|
|
using Dbg = System.Management.Automation.Diagnostics;
|
|
|
|
namespace Microsoft.PowerShell
|
|
{
|
|
/// <summary>
|
|
/// ProgressNode is an augmentation of the ProgressRecord type that adds extra fields for the purposes of tracking
|
|
/// outstanding activities received by the host, and rendering them in the console.
|
|
/// </summary>
|
|
internal
|
|
class
|
|
ProgressNode : ProgressRecord
|
|
{
|
|
/// <summary>
|
|
/// Indicates the various layouts for rendering a particular node.
|
|
/// </summary>
|
|
internal
|
|
enum
|
|
RenderStyle
|
|
{
|
|
Invisible = 0,
|
|
Minimal = 1,
|
|
Compact = 2,
|
|
|
|
/// <summary>
|
|
/// Allocate only one line for displaying the StatusDescription or the CurrentOperation,
|
|
/// truncate the rest if the StatusDescription or CurrentOperation doesn't fit in one line.
|
|
/// </summary>
|
|
Full = 3,
|
|
|
|
/// <summary>
|
|
/// The node will be displayed the same as Full, plus, the whole StatusDescription and CurrentOperation will be displayed (in multiple lines if needed).
|
|
/// </summary>
|
|
FullPlus = 4,
|
|
|
|
/// <summary>
|
|
/// The node will be displayed using ANSI escape sequences.
|
|
/// </summary>
|
|
Ansi = 5,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs an instance from a ProgressRecord.
|
|
/// </summary>
|
|
internal
|
|
ProgressNode(long sourceId, ProgressRecord record)
|
|
: base(record.ActivityId, record.Activity, record.StatusDescription)
|
|
{
|
|
Dbg.Assert(record.RecordType == ProgressRecordType.Processing, "should only create node for Processing records");
|
|
|
|
this.ParentActivityId = record.ParentActivityId;
|
|
this.CurrentOperation = record.CurrentOperation;
|
|
this.PercentComplete = Math.Min(record.PercentComplete, 100);
|
|
this.SecondsRemaining = record.SecondsRemaining;
|
|
this.RecordType = record.RecordType;
|
|
|
|
this.Style = IsMinimalProgressRenderingEnabled()
|
|
? RenderStyle.Ansi
|
|
: this.Style = RenderStyle.FullPlus;
|
|
|
|
this.SourceId = sourceId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a single progress node as strings of text according to that node's style. The text is appended to the
|
|
/// supplied list of strings.
|
|
/// </summary>
|
|
/// <param name="strCollection">
|
|
/// List of strings to which the node's rendering will be appended.
|
|
/// </param>
|
|
/// <param name="indentation">
|
|
/// The indentation level (in BufferCells) at which the node should be rendered.
|
|
/// </param>
|
|
/// <param name="maxWidth">
|
|
/// The maximum number of BufferCells that the rendering is allowed to consume.
|
|
/// </param>
|
|
/// <param name="rawUI">
|
|
/// The PSHostRawUserInterface used to gauge string widths in the rendering.
|
|
/// </param>
|
|
internal
|
|
void
|
|
Render(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
|
|
{
|
|
Dbg.Assert(strCollection != null, "strCollection should not be null");
|
|
Dbg.Assert(indentation >= 0, "indentation is negative");
|
|
Dbg.Assert(this.RecordType != ProgressRecordType.Completed, "should never render completed records");
|
|
|
|
switch (Style)
|
|
{
|
|
case RenderStyle.FullPlus:
|
|
RenderFull(strCollection, indentation, maxWidth, rawUI, isFullPlus: true);
|
|
break;
|
|
case RenderStyle.Full:
|
|
RenderFull(strCollection, indentation, maxWidth, rawUI, isFullPlus: false);
|
|
break;
|
|
case RenderStyle.Compact:
|
|
RenderCompact(strCollection, indentation, maxWidth, rawUI);
|
|
break;
|
|
case RenderStyle.Minimal:
|
|
RenderMinimal(strCollection, indentation, maxWidth, rawUI);
|
|
break;
|
|
case RenderStyle.Ansi:
|
|
RenderAnsi(strCollection, indentation, maxWidth);
|
|
break;
|
|
case RenderStyle.Invisible:
|
|
// do nothing
|
|
break;
|
|
default:
|
|
Dbg.Assert(false, "unrecognized RenderStyle value");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a node in the "Full" style.
|
|
/// </summary>
|
|
/// <param name="strCollection">
|
|
/// List of strings to which the node's rendering will be appended.
|
|
/// </param>
|
|
/// <param name="indentation">
|
|
/// The indentation level (in BufferCells) at which the node should be rendered.
|
|
/// </param>
|
|
/// <param name="maxWidth">
|
|
/// The maximum number of BufferCells that the rendering is allowed to consume.
|
|
/// </param>
|
|
/// <param name="rawUI">
|
|
/// The PSHostRawUserInterface used to gauge string widths in the rendering.
|
|
/// </param>
|
|
/// <param name="isFullPlus">
|
|
/// Indicate if the full StatusDescription and CurrentOperation should be displayed.
|
|
/// </param>
|
|
private
|
|
void
|
|
RenderFull(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI, bool isFullPlus)
|
|
{
|
|
string indent = StringUtil.Padding(indentation);
|
|
|
|
// First line: the activity
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI, StringUtil.Format(" {0}{1} ", indent, this.Activity), maxWidth));
|
|
|
|
indentation += 3;
|
|
indent = StringUtil.Padding(indentation);
|
|
|
|
// Second line: the status description
|
|
|
|
RenderFullDescription(this.StatusDescription, indent, maxWidth, rawUI, strCollection, isFullPlus);
|
|
|
|
// Third line: the percentage thermometer. The size of this is proportional to the width we're allowed
|
|
// to consume. -2 for the whitespace, -2 again for the brackets around thermo, -5 to not be too big
|
|
|
|
if (PercentComplete >= 0)
|
|
{
|
|
int thermoWidth = Math.Max(3, maxWidth - indentation - 2 - 2 - 5);
|
|
int mercuryWidth = 0;
|
|
mercuryWidth = PercentComplete * thermoWidth / 100;
|
|
if (PercentComplete < 100 && mercuryWidth == thermoWidth)
|
|
{
|
|
// back off a tad unless we're totally complete to prevent the appearance of completion before
|
|
// the fact.
|
|
|
|
--mercuryWidth;
|
|
}
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
StringUtil.Format(
|
|
" {0}[{1}{2}] ",
|
|
indent,
|
|
new string('o', mercuryWidth),
|
|
StringUtil.Padding(thermoWidth - mercuryWidth)),
|
|
maxWidth));
|
|
}
|
|
|
|
// Fourth line: the seconds remaining
|
|
|
|
if (SecondsRemaining >= 0)
|
|
{
|
|
TimeSpan span = new TimeSpan(0, 0, this.SecondsRemaining);
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
" "
|
|
+ StringUtil.Format(
|
|
ProgressNodeStrings.SecondsRemaining,
|
|
indent,
|
|
span)
|
|
+ " ",
|
|
maxWidth));
|
|
}
|
|
|
|
// Fifth and Sixth lines: The current operation
|
|
|
|
if (!string.IsNullOrEmpty(CurrentOperation))
|
|
{
|
|
strCollection.Add(" ");
|
|
RenderFullDescription(this.CurrentOperation, indent, maxWidth, rawUI, strCollection, isFullPlus);
|
|
}
|
|
}
|
|
|
|
private static void RenderFullDescription(string description, string indent, int maxWidth, PSHostRawUserInterface rawUi, ArrayList strCollection, bool isFullPlus)
|
|
{
|
|
string oldDescription = StringUtil.Format(" {0}{1} ", indent, description);
|
|
string newDescription;
|
|
|
|
do
|
|
{
|
|
newDescription = StringUtil.TruncateToBufferCellWidth(rawUi, oldDescription, maxWidth);
|
|
strCollection.Add(newDescription);
|
|
|
|
if (oldDescription.Length == newDescription.Length)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
oldDescription = StringUtil.Format(" {0}{1}", indent, oldDescription.Substring(newDescription.Length));
|
|
}
|
|
} while (isFullPlus);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a node in the "Compact" style.
|
|
/// </summary>
|
|
/// <param name="strCollection">
|
|
/// List of strings to which the node's rendering will be appended.
|
|
/// </param>
|
|
/// <param name="indentation">
|
|
/// The indentation level (in BufferCells) at which the node should be rendered.
|
|
/// </param>
|
|
/// <param name="maxWidth">
|
|
/// The maximum number of BufferCells that the rendering is allowed to consume.
|
|
/// </param>
|
|
/// <param name="rawUI">
|
|
/// The PSHostRawUserInterface used to gauge string widths in the rendering.
|
|
/// </param>
|
|
private
|
|
void
|
|
RenderCompact(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
|
|
{
|
|
string indent = StringUtil.Padding(indentation);
|
|
|
|
// First line: the activity
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
StringUtil.Format(" {0}{1} ", indent, this.Activity), maxWidth));
|
|
|
|
indentation += 3;
|
|
indent = StringUtil.Padding(indentation);
|
|
|
|
// Second line: the status description with percentage and time remaining, if applicable.
|
|
|
|
string percent = string.Empty;
|
|
if (PercentComplete >= 0)
|
|
{
|
|
percent = StringUtil.Format("{0}% ", PercentComplete);
|
|
}
|
|
|
|
string secRemain = string.Empty;
|
|
if (SecondsRemaining >= 0)
|
|
{
|
|
TimeSpan span = new TimeSpan(0, 0, SecondsRemaining);
|
|
secRemain = span.ToString() + " ";
|
|
}
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
StringUtil.Format(
|
|
" {0}{1}{2}{3} ",
|
|
indent,
|
|
percent,
|
|
secRemain,
|
|
StatusDescription),
|
|
maxWidth));
|
|
|
|
// Third line: The current operation
|
|
|
|
if (!string.IsNullOrEmpty(CurrentOperation))
|
|
{
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
StringUtil.Format(" {0}{1} ", indent, this.CurrentOperation), maxWidth));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a node in the "Minimal" style.
|
|
/// </summary>
|
|
/// <param name="strCollection">
|
|
/// List of strings to which the node's rendering will be appended.
|
|
/// </param>
|
|
/// <param name="indentation">
|
|
/// The indentation level (in BufferCells) at which the node should be rendered.
|
|
/// </param>
|
|
/// <param name="maxWidth">
|
|
/// The maximum number of BufferCells that the rendering is allowed to consume.
|
|
/// </param>
|
|
/// <param name="rawUI">
|
|
/// The PSHostRawUserInterface used to gauge string widths in the rendering.
|
|
/// </param>
|
|
private
|
|
void
|
|
RenderMinimal(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
|
|
{
|
|
string indent = StringUtil.Padding(indentation);
|
|
|
|
// First line: Everything mushed into one line
|
|
|
|
string percent = string.Empty;
|
|
if (PercentComplete >= 0)
|
|
{
|
|
percent = StringUtil.Format("{0}% ", PercentComplete);
|
|
}
|
|
|
|
string secRemain = string.Empty;
|
|
if (SecondsRemaining >= 0)
|
|
{
|
|
TimeSpan span = new TimeSpan(0, 0, SecondsRemaining);
|
|
secRemain = span.ToString() + " ";
|
|
}
|
|
|
|
strCollection.Add(
|
|
StringUtil.TruncateToBufferCellWidth(
|
|
rawUI,
|
|
StringUtil.Format(
|
|
" {0}{1} {2}{3}{4} ",
|
|
indent,
|
|
Activity,
|
|
percent,
|
|
secRemain,
|
|
StatusDescription),
|
|
maxWidth));
|
|
}
|
|
|
|
internal static bool IsMinimalProgressRenderingEnabled()
|
|
{
|
|
return ExperimentalFeature.IsEnabled(ExperimentalFeature.PSAnsiProgressFeatureName) && PSStyle.Instance.Progress.View == ProgressView.Minimal;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a node in the "ANSI" style.
|
|
/// </summary>
|
|
/// <param name="strCollection">
|
|
/// List of strings to which the node's rendering will be appended.
|
|
/// </param>
|
|
/// <param name="indentation">
|
|
/// The indentation level in chars at which the node should be rendered.
|
|
/// </param>
|
|
/// <param name="maxWidth">
|
|
/// The maximum number of chars that the rendering is allowed to consume.
|
|
/// </param>
|
|
private
|
|
void
|
|
RenderAnsi(ArrayList strCollection, int indentation, int maxWidth)
|
|
{
|
|
string indent = StringUtil.Padding(indentation);
|
|
string secRemain = string.Empty;
|
|
if (SecondsRemaining >= 0)
|
|
{
|
|
secRemain = SecondsRemaining.ToString() + "s";
|
|
}
|
|
|
|
int secRemainLength = secRemain.Length + 1;
|
|
|
|
// limit progress bar to 120 chars as no need to render full width
|
|
if (PSStyle.Instance.Progress.MaxWidth > 0 && maxWidth > PSStyle.Instance.Progress.MaxWidth)
|
|
{
|
|
maxWidth = PSStyle.Instance.Progress.MaxWidth;
|
|
}
|
|
|
|
// if the activity is really long, only use up to half the width
|
|
string activity;
|
|
if (Activity.Length > maxWidth / 2)
|
|
{
|
|
activity = Activity.Substring(0, maxWidth / 2) + PSObjectHelper.Ellipsis;
|
|
}
|
|
else
|
|
{
|
|
activity = Activity;
|
|
}
|
|
|
|
// 4 is for the extra space and square brackets below and one extra space
|
|
int barWidth = maxWidth - activity.Length - indentation - 4;
|
|
|
|
var sb = new StringBuilder();
|
|
int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length;
|
|
sb.Append(PSStyle.Instance.Reverse);
|
|
|
|
int maxStatusLength = barWidth - secRemainLength - 1;
|
|
if (maxStatusLength > 0 && StatusDescription.Length > barWidth - secRemainLength)
|
|
{
|
|
sb.Append(StatusDescription.AsSpan(0, barWidth - secRemainLength - 1));
|
|
sb.Append(PSObjectHelper.Ellipsis);
|
|
}
|
|
else
|
|
{
|
|
sb.Append(StatusDescription);
|
|
}
|
|
|
|
int emptyPadLength = barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength;
|
|
if (emptyPadLength > 0)
|
|
{
|
|
sb.Append(string.Empty.PadRight(emptyPadLength));
|
|
}
|
|
|
|
sb.Append(secRemain);
|
|
|
|
if (PercentComplete > 0 && PercentComplete < 100 && barWidth > 0)
|
|
{
|
|
int barLength = PercentComplete * barWidth / 100;
|
|
if (barLength >= barWidth)
|
|
{
|
|
barLength = barWidth - 1;
|
|
}
|
|
|
|
if (barLength < sb.Length)
|
|
{
|
|
sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.Append(PSStyle.Instance.ReverseOff);
|
|
}
|
|
|
|
strCollection.Add(
|
|
StringUtil.Format(
|
|
"{0}{1}{2} [{3}]{4}",
|
|
indent,
|
|
PSStyle.Instance.Progress.Style,
|
|
activity,
|
|
sb.ToString(),
|
|
PSStyle.Instance.Reset)
|
|
.PadRight(padding));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The nodes that have this node as their parent.
|
|
/// </summary>
|
|
internal
|
|
ArrayList
|
|
Children;
|
|
|
|
/// <summary>
|
|
/// The "age" of the node. A node's age is incremented by PendingProgress.Update each time a new ProgressRecord is
|
|
/// received by the host. A node's age is reset when a corresponding ProgressRecord is received. Thus, the age of
|
|
/// a node reflects the number of ProgressRecord that have been received since the node was last updated.
|
|
///
|
|
/// The age is used by PendingProgress.Render to determine which nodes should be rendered on the display, and how. As the
|
|
/// display has finite size, it may be possible to have many more outstanding progress activities than will fit in that
|
|
/// space. The rendering of nodes can be progressively "compressed" into a more terse format, or not rendered at all in
|
|
/// order to fit as many nodes as possible in the available space. The oldest nodes are compressed or skipped first.
|
|
/// </summary>
|
|
internal
|
|
int
|
|
Age;
|
|
|
|
/// <summary>
|
|
/// The style in which this node should be rendered.
|
|
/// </summary>
|
|
internal
|
|
RenderStyle
|
|
Style = RenderStyle.FullPlus;
|
|
|
|
/// <summary>
|
|
/// Identifies the source of the progress record.
|
|
/// </summary>
|
|
internal
|
|
long
|
|
SourceId;
|
|
|
|
/// <summary>
|
|
/// The number of vertical BufferCells that are required to render the node in its current style.
|
|
/// </summary>
|
|
/// <value></value>
|
|
internal int LinesRequiredMethod(PSHostRawUserInterface rawUi, int maxWidth)
|
|
{
|
|
Dbg.Assert(this.RecordType != ProgressRecordType.Completed, "should never render completed records");
|
|
|
|
switch (Style)
|
|
{
|
|
case RenderStyle.FullPlus:
|
|
return LinesRequiredInFullStyleMethod(rawUi, maxWidth, isFullPlus: true);
|
|
|
|
case RenderStyle.Full:
|
|
return LinesRequiredInFullStyleMethod(rawUi, maxWidth, isFullPlus: false);
|
|
|
|
case RenderStyle.Compact:
|
|
return LinesRequiredInCompactStyle;
|
|
|
|
case RenderStyle.Minimal:
|
|
return 1;
|
|
|
|
case RenderStyle.Invisible:
|
|
return 0;
|
|
|
|
case RenderStyle.Ansi:
|
|
return 1;
|
|
|
|
default:
|
|
Dbg.Assert(false, "Unknown RenderStyle value");
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of vertical BufferCells that are required to render the node in the Full style.
|
|
/// </summary>
|
|
/// <value></value>
|
|
private int LinesRequiredInFullStyleMethod(PSHostRawUserInterface rawUi, int maxWidth, bool isFullPlus)
|
|
{
|
|
// Since the fields of this instance could have been changed, we compute this on-the-fly.
|
|
|
|
// NTRAID#Windows OS Bugs-1062104-2004/12/15-sburns we assume 1 line for each field. If we ever need to
|
|
// word-wrap text fields, then this calculation will need updating.
|
|
|
|
// Start with 1 for the Activity
|
|
int lines = 1;
|
|
// Use 5 spaces as the heuristic indent. 5 spaces stand for the indent for the CurrentOperation of the first-level child node
|
|
var indent = StringUtil.Padding(5);
|
|
var temp = new ArrayList();
|
|
|
|
if (isFullPlus)
|
|
{
|
|
temp.Clear();
|
|
RenderFullDescription(StatusDescription, indent, maxWidth, rawUi, temp, isFullPlus: true);
|
|
lines += temp.Count;
|
|
}
|
|
else
|
|
{
|
|
// 1 for the Status
|
|
lines++;
|
|
}
|
|
|
|
if (PercentComplete >= 0)
|
|
{
|
|
++lines;
|
|
}
|
|
|
|
if (SecondsRemaining >= 0)
|
|
{
|
|
++lines;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(CurrentOperation))
|
|
{
|
|
if (isFullPlus)
|
|
{
|
|
lines += 1;
|
|
temp.Clear();
|
|
RenderFullDescription(CurrentOperation, indent, maxWidth, rawUi, temp, isFullPlus: true);
|
|
lines += temp.Count;
|
|
}
|
|
else
|
|
{
|
|
lines += 2;
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of vertical BufferCells that are required to render the node in the Compact style.
|
|
/// </summary>
|
|
/// <value></value>
|
|
private
|
|
int
|
|
LinesRequiredInCompactStyle
|
|
{
|
|
get
|
|
{
|
|
// Since the fields of this instance could have been changed, we compute this on-the-fly.
|
|
|
|
// NTRAID#Windows OS Bugs-1062104-2004/12/15-sburns we assume 1 line for each field. If we ever need to
|
|
// word-wrap text fields, then this calculation will need updating.
|
|
|
|
// Start with 1 for the Activity, and 1 for the Status.
|
|
|
|
int lines = 2;
|
|
if (!string.IsNullOrEmpty(CurrentOperation))
|
|
{
|
|
++lines;
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|