/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System.Runtime.Serialization; using Dbg = System.Management.Automation.Diagnostics; namespace System.Management.Automation { /// /// /// Defines a data structure used to represent the status of an ongoing operation at a point in time. /// /// /// /// /// ProgressRecords are passed to , /// which, according to user preference, forwards that information on to the host for rendering to the user. /// /// /// [DataContract()] public class ProgressRecord { #region Public API /// /// /// Initializes a new instance of the ProgressRecord class and defines the activity Id, /// activity description, and status description. /// /// /// /// /// A unique numeric key that identifies the activity to which this record applies. /// /// /// /// /// A description of the activity for which progress is being reported. /// /// /// /// /// A description of the status of the activity. /// /// public ProgressRecord(int activityId, string activity, string statusDescription) { if (activityId < 0) { // negative Ids are reserved to indicate "no id" for parent Ids. throw PSTraceSource.NewArgumentOutOfRangeException("activityId", activityId, ProgressRecordStrings.ArgMayNotBeNegative, "activityId"); } if (String.IsNullOrEmpty(activity)) { throw PSTraceSource.NewArgumentException("activity", ProgressRecordStrings.ArgMayNotBeNullOrEmpty, "activity"); } if (String.IsNullOrEmpty(statusDescription)) { throw PSTraceSource.NewArgumentException("activity", ProgressRecordStrings.ArgMayNotBeNullOrEmpty, "statusDescription"); } _id = activityId; _activity = activity; _status = statusDescription; } /// /// Cloning constructor (all fields are value types - can treat our implementation of cloning as "deep" copy) /// /// internal ProgressRecord(ProgressRecord other) { _activity = other._activity; _currentOperation = other._currentOperation; _id = other._id; _parentId = other._parentId; _percent = other._percent; _secondsRemaining = other._secondsRemaining; _status = other._status; _type = other._type; } /// /// Added to enable ClrFacade.GetUninitializedObject to instantiate an uninitialized version of this class. /// internal ProgressRecord() { } /// /// /// Gets the Id of the activity to which this record corresponds. Used as a 'key' for the /// linking of subordinate activities. /// /// public int ActivityId { get { return _id; } } /// /// /// Gets and sets the Id of the activity for which this record is a subordinate. /// /// /// /// /// Used to allow chaining of progress records (such as when one installation invokes a child installation). UI: /// normally not directly visible except as already displayed as its own activity. Usually a sub-activity will be /// positioned below and to the right of its parent. /// /// A negative value (the default) indicates that the activity is not a subordinate. /// /// May not be the same as ActivityId. /// /// /// /// public int ParentActivityId { get { return _parentId; } set { if (value == ActivityId) { throw PSTraceSource.NewArgumentException("value", ProgressRecordStrings.ParentActivityIdCantBeActivityId); } _parentId = value; } } /// /// /// Gets and sets the description of the activity for which progress is being reported. /// /// /// /// /// States the overall intent of whats being accomplished, such as "Recursively removing item c:\temp." Typically /// displayed in conjunction with a progress bar. /// /// public string Activity { get { return _activity; } set { if (String.IsNullOrEmpty(value)) { throw PSTraceSource.NewArgumentException("value", ProgressRecordStrings.ArgMayNotBeNullOrEmpty, "value"); } _activity = value; } } /// /// /// Gets and sets the current status of the operation, e.g., "35 of 50 items Copied." or "95% completed." or "100 files purged." /// /// public string StatusDescription { get { return _status; } set { if (String.IsNullOrEmpty(value)) { throw PSTraceSource.NewArgumentException("value", ProgressRecordStrings.ArgMayNotBeNullOrEmpty, "value"); } _status = value; } } /// /// /// Gets and sets the current operation of the many required to accomplish the activity (such as "copying foo.txt"). Normally displayed /// below its associated progress bar, e.g., "deleting file foo.bar" /// Set to null or empty in the case a sub-activity will be used to show the current operation. /// /// public string CurrentOperation { get { return _currentOperation; } set { // null or empty string is allowed _currentOperation = value; } } /// /// /// Gets and sets the estimate of the percentage of total work for the activity that is completed. Typically displayed as a progress bar. /// Set to a negative value to indicate that the percentage completed should not be displayed. /// /// public int PercentComplete { get { return _percent; } set { // negative values are allowed if (value > 100) { throw PSTraceSource.NewArgumentOutOfRangeException( "value", value, ProgressRecordStrings.PercentMayNotBeMoreThan100, "PercentComplete"); } _percent = value; } } /// /// /// Gets and sets the estimate of time remaining until this activity is completed. This can be based upon a measurement of time since /// started and the percent complete or another approach deemed appropriate by the caller. /// /// Normally displayed beside the progress bar, as "N seconds remaining." /// /// /// /// /// A value less than 0 means "don't display a time remaining." /// /// public int SecondsRemaining { get { return _secondsRemaining; } set { // negative values are allowed _secondsRemaining = value; } } /// /// /// Gets and sets the type of record represented by this instance. /// /// public ProgressRecordType RecordType { get { return _type; } set { if (value != ProgressRecordType.Completed && value != ProgressRecordType.Processing) { throw PSTraceSource.NewArgumentException("value"); } _type = value; } } /// /// /// Overrides /// /// /// /// /// "parent = a id = b act = c stat = d cur = e pct = f sec = g type = h" where /// a, b, c, d, e, f, and g are the values of ParentActivityId, ActivityId, Activity, StatusDescription, /// CurrentOperation, PercentComplete, SecondsRemaining and RecordType properties. /// /// public override string ToString() { return String.Format( System.Globalization.CultureInfo.CurrentCulture, "parent = {0} id = {1} act = {2} stat = {3} cur = {4} pct = {5} sec = {6} type = {7}", _parentId, _id, _activity, _status, _currentOperation, _percent, _secondsRemaining, _type); } #endregion #region Helper methods internal static int? GetSecondsRemaining(DateTime startTime, double percentageComplete) { Dbg.Assert(percentageComplete >= 0.0, "Caller should verify percentageComplete >= 0.0"); Dbg.Assert(percentageComplete <= 1.0, "Caller should verify percentageComplete <= 1.0"); Dbg.Assert( startTime.Kind == DateTimeKind.Utc, "DateTime arithmetic should always be done in utc mode [to avoid problems when some operands are calculated right before and right after switching to /from a daylight saving time"); if ((percentageComplete < 0.00001) || double.IsNaN(percentageComplete)) { return null; } DateTime now = DateTime.UtcNow; Dbg.Assert(startTime <= now, "Caller should pass a valid startTime"); TimeSpan elapsedTime = now - startTime; TimeSpan totalTime; try { totalTime = TimeSpan.FromMilliseconds(elapsedTime.TotalMilliseconds / percentageComplete); } catch (OverflowException) { return null; } catch (ArgumentException) { return null; } TimeSpan remainingTime = totalTime - elapsedTime; return (int)(remainingTime.TotalSeconds); } /// /// Returns percentage complete when it is impossible to predict how long an operation might take. /// The percentage complete will slowly converge toward 100%. /// At the the percentage complete will be 90%. /// /// When did the operation start /// How long does the operation usually take /// Estimated percentage complete of the operation (always between 0 and 99% - never returns 100%) /// /// Thrown when /// 1) is in the future /// 2) is negative or zero /// internal static int GetPercentageComplete(DateTime startTime, TimeSpan expectedDuration) { DateTime now = DateTime.UtcNow; Dbg.Assert( startTime.Kind == DateTimeKind.Utc, "DateTime arithmetic should always be done in utc mode [to avoid problems when some operands are calculated right before and right after switching to /from a daylight saving time"); if (startTime > now) { throw new ArgumentOutOfRangeException("startTime"); } if (expectedDuration <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException("expectedDuration"); } /* * According to the spec of Checkpoint-Computer * (http://cmdletdesigner/SpecViewer/Default.aspx?Project=PowerShell&Cmdlet=Checkpoint-Computer) * we have percentage remaining = f(t) where * f(inf) = 0% * f(0) = 100% * f(90) = = 10% * * The spec talks about exponential decay, but function based on 1/x seems better: * f(t) = a / (T + b) * * This by definition has f(inf) = 0, so we have to find a and b for the last 2 cases: * E1: f(0) = a / (0 + b) = 100 * E2: f(T = 90) = a / (T + b) = 10 * * From E1 we have a = 100 * b, which we can use in E2: * (100 * b) / (T + b) = 10 * 100 * b = 10 * T + 10 * b * 90 * b = 10 * T * b = T / 9 * * Some sample values (for T=90): * t | %rem * ----------- * 0 | 100.0% * 5 | 66.6% * 10 | 50.0% * 30 | 25.0% * 70 | 12.5% * 90 | 10.0% * 300 | 3.2% * 600 | 1.6% * 3600| 0.2% */ TimeSpan timeElapsed = now - startTime; double b = expectedDuration.TotalSeconds / 9.0; double a = 100.0 * b; double percentageRemaining = a / (timeElapsed.TotalSeconds + b); double percentageCompleted = 100.0 - percentageRemaining; return (int)Math.Floor(percentageCompleted); } #endregion [DataMemberAttribute()] private int _id; [DataMemberAttribute()] private int _parentId = -1; [DataMemberAttribute()] private string _activity; [DataMemberAttribute()] private string _status; [DataMemberAttribute()] private string _currentOperation; [DataMemberAttribute()] private int _percent = -1; [DataMemberAttribute()] private int _secondsRemaining = -1; [DataMemberAttribute()] private ProgressRecordType _type = ProgressRecordType.Processing; #region Serialization / deserialization for remoting /// /// Creates a ProgressRecord object from a PSObject property bag. /// PSObject has to be in the format returned by ToPSObjectForRemoting method. /// /// PSObject to rehydrate /// /// ProgressRecord rehydrated from a PSObject property bag /// /// /// Thrown if the PSObject is null. /// /// /// Thrown when the PSObject is not in the expected format /// internal static ProgressRecord FromPSObjectForRemoting(PSObject progressAsPSObject) { if (progressAsPSObject == null) { throw PSTraceSource.NewArgumentNullException("progressAsPSObject"); } string activity = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_Activity); int activityId = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_ActivityId); string statusDescription = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_StatusDescription); ProgressRecord result = new ProgressRecord(activityId, activity, statusDescription); result.CurrentOperation = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_CurrentOperation); result.ParentActivityId = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_ParentActivityId); result.PercentComplete = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_PercentComplete); result.RecordType = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_Type); result.SecondsRemaining = RemotingDecoder.GetPropertyValue(progressAsPSObject, RemoteDataNameStrings.ProgressRecord_SecondsRemaining); return result; } /// /// Returns this object as a PSObject property bag /// that can be used in a remoting protocol data object. /// /// This object as a PSObject property bag internal PSObject ToPSObjectForRemoting() { PSObject progressAsPSObject = RemotingEncoder.CreateEmptyPSObject(); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_Activity, this.Activity)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_ActivityId, this.ActivityId)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_StatusDescription, this.StatusDescription)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_CurrentOperation, this.CurrentOperation)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_ParentActivityId, this.ParentActivityId)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_PercentComplete, this.PercentComplete)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_Type, this.RecordType)); progressAsPSObject.Properties.Add(new PSNoteProperty(RemoteDataNameStrings.ProgressRecord_SecondsRemaining, this.SecondsRemaining)); return progressAsPSObject; } #endregion } //ProgressRecord /// /// /// Defines two types of progress record that refer to the beginning (or middle) and end of an operation. /// /// public enum ProgressRecordType { /// /// Operation just started or is not yet complete /// /// /// A cmdlet can call WriteProgress with ProgressRecordType.Processing /// as many times as it wishes. However, at the end of the operation, /// it should call once more with ProgressRecordType.Completed. /// /// The first time that a host receives a progress record /// for a given activity, it will typically display a progress /// indicator for that activity. For each subsequent record /// of the same Id, the host will update that display. /// Finally, when the host receives a 'completed' record /// for that activity, it will remove the progress indicator. /// Processing, /// /// Operation is complete /// /// /// If a cmdlet uses WriteProgress, it should use /// ProgressRecordType.Completed exactly once, in the last call /// to WriteProgress. /// Completed } }