Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ public SwitchParameter UseMinimalHeader
get; set;
}

/// <summary>
/// Gets or sets whether to prefix each transcribed line of command
/// input and output with the time at which it was recorded to the
/// transcript. Standalone native command output on Windows is captured
/// from the console buffer only after the process exits, so all lines
/// of such output receive the completion time of the command. Piping
/// or redirecting native command output records each line as it is
/// produced.
/// </summary>
[Parameter]
public SwitchParameter IncludeTimeStamp
{
get; set;
}

/// <summary>
/// Starts the transcription.
/// </summary>
Expand Down Expand Up @@ -234,7 +249,7 @@ protected override void BeginProcessing()

System.Management.Automation.Remoting.PSSenderInfo psSenderInfo =
this.SessionState.PSVariable.GetValue("PSSenderInfo") as System.Management.Automation.Remoting.PSSenderInfo;
Host.UI.StartTranscribing(effectiveFilePath, psSenderInfo, IncludeInvocationHeader.ToBool(), UseMinimalHeader.IsPresent);
Host.UI.StartTranscribing(effectiveFilePath, psSenderInfo, IncludeInvocationHeader.ToBool(), UseMinimalHeader.IsPresent, IncludeTimeStamp.IsPresent);

// ch.StartTranscribing(effectiveFilePath, Append);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,12 @@ internal override void Complete()
bufferText.Append(lineContents.ToString().TrimEnd(Utils.Separators.SpaceOrTab));
}

// Screen-scraped output is only available after the process exits, and the
// console buffer does not track when each row was written. When transcript
// timestamps are enabled (Start-Transcript -IncludeTimeStamp), all lines of
// this block are therefore recorded with the completion time of the command.
// Per-line production times can only be captured when the native command
// output is piped or redirected, which streams each line as it is produced.
this.Command.Context.InternalHost.UI.TranscribeResult(bufferText.ToString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,11 @@ internal void TranscribeCommand(string commandText, InvocationInfo invocation)
transcript.OutputToLog.Add("**********************");
}

transcript.OutputToLog.Add(TranscriptionData.PromptText + commandText);
transcript.AddOutput(TranscriptionData.PromptText + commandText);
}
else
{
transcript.OutputToLog.Add(">> " + commandText);
transcript.AddOutput(">> " + commandText);
}
}
}
Expand Down Expand Up @@ -588,11 +588,12 @@ private void CheckSystemTranscript()
}
}

internal void StartTranscribing(string path, System.Management.Automation.Remoting.PSSenderInfo senderInfo, bool includeInvocationHeader, bool useMinimalHeader)
internal void StartTranscribing(string path, System.Management.Automation.Remoting.PSSenderInfo senderInfo, bool includeInvocationHeader, bool useMinimalHeader, bool includeTimeStamp)
{
TranscriptionOption transcript = new TranscriptionOption();
transcript.Path = path;
transcript.IncludeInvocationHeader = includeInvocationHeader;
transcript.IncludeTimeStamp = includeTimeStamp;
TranscriptionData.Transcripts.Add(transcript);

LogTranscriptHeader(senderInfo, transcript, useMinimalHeader);
Expand Down Expand Up @@ -773,7 +774,7 @@ internal void TranscribeResult(Runspace sourceRunspace, string resultText)
{
lock (transcript.OutputToLog)
{
transcript.OutputToLog.Add(resultText);
transcript.AddOutput(resultText);
}
}
}
Expand Down Expand Up @@ -1239,6 +1240,45 @@ internal TranscriptionOption()
/// </summary>
internal bool IncludeInvocationHeader { get; set; }

/// <summary>
/// Whether to prefix each transcribed line of command input and
/// output with the time at which it was transcribed.
/// </summary>
internal bool IncludeTimeStamp { get; set; }

/// <summary>
/// The format used for the timestamp prefix, i.e. '2024-08-23 11:04:52 '.
/// </summary>
private const string TimeStampFormat = "yyyy-MM-dd HH:mm:ss";

private static readonly string[] s_newLineSeparators = { "\r\n", "\n", "\r" };

/// <summary>
/// Adds output to be logged to this transcript. When <see cref="IncludeTimeStamp"/>
/// is set, each non-empty line of the output is prefixed with the current time.
/// </summary>
/// <param name="text">The text to be logged.</param>
internal void AddOutput(string text)
{
if (!IncludeTimeStamp || string.IsNullOrEmpty(text))
{
OutputToLog.Add(text);
return;
}

string timeStamp = DateTime.Now.ToString(TimeStampFormat, CultureInfo.InvariantCulture);
Comment thread
soroshsabz marked this conversation as resolved.
string[] lines = text.Split(s_newLineSeparators, StringSplitOptions.None);
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Length > 0)
{
lines[i] = string.Concat(timeStamp, " ", lines[i]);
}
}

OutputToLog.Add(string.Join(Environment.NewLine, lines));
}

/// <summary>
/// Logs buffered content to disk. We use this instead of File.AppendAllLines
/// so that we don't need to pay seek penalties all the time, and so that we
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ Describe "Start-Transcript, Stop-Transcript tests" -tags "CI" {
$transcriptFilePath | Should -FileContentMatch $machineName
}

It "Transcription should prefix native command output with a timestamp when 'IncludeTimeStamp' parameter is used" {
# The native command output is piped so that it is transcribed through the
# regular output stream. Standalone native command output is only captured by
# screen scraping when a physical console is available, which is not guaranteed
# in test environments.
$script = {
Start-Transcript -Path $transcriptFilePath -IncludeTimeStamp
hostname | ForEach-Object { Write-Host -Message $_ }
Stop-Transcript
}

& $script

$transcriptFilePath | Should -Exist
$machineName = [System.Environment]::MachineName
(Get-Content $transcriptFilePath | Where-Object { $_ -match "^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .*$machineName" }).Count | Should -BeGreaterThan 0
}

It "Transcription should record Write-Information output when InformationAction is set to Continue" {
[String]$message = New-Guid
$script = {
Expand Down Expand Up @@ -321,4 +339,35 @@ Describe "Start-Transcript, Stop-Transcript tests" -tags "CI" {

$transcriptMinHeaderLength | Should -BeLessThan $transcriptLength
}

It "Transcription should prefix recorded output with a timestamp when 'IncludeTimeStamp' parameter is used" {
$message = "TimeStampTestMessage"
$script = {
Start-Transcript -Path $transcriptFilePath -IncludeTimeStamp
Write-Host -Message $message
Write-Host -Message "$message second`n$message third"
Stop-Transcript
}

& $script

$transcriptFilePath | Should -Exist
$content = Get-Content $transcriptFilePath
($content | Where-Object { $_ -match "^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .*$message" }).Count | Should -Be 3
}

It "Transcription should not prefix recorded output with a timestamp by default" {
$message = "NoTimeStampTestMessage"
$script = {
Start-Transcript -Path $transcriptFilePath
Write-Host -Message $message
Stop-Transcript
}

& $script

$transcriptFilePath | Should -Exist
$transcriptFilePath | Should -FileContentMatch $message
(Get-Content $transcriptFilePath | Where-Object { $_ -match "^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} " }).Count | Should -Be 0
}
}