Skip to content

Conversation

@SteveL-MSFT
Copy link
Member

Out-Default was incorrectly caching transcription state as the subsequent use was caching the already changed state

Out-Default needed to implement IDispose pattern so transcription state reverts back on exception in pipeline

Addresses #3390

Copy link
Contributor

@PetSerAl PetSerAl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It ends up Out-Default -Transcript just set TranscribeOnly = true on start (BeginProcessing) and TranscribeOnly = false on end (Dispose).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You ends up restoring TranscribeOnly even if you not capture it before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You never set _transcriptStateChanged back to false. So Out-Default stop capturing TranscribeOnly after first call with -Transcript switch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out-Default lifetime only exists in that pipeline and typically should be the last thing in the pipeline as nothing is emitted if used with -Transcript. Is there a case that's broken?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not going to say that nested or intersected Out-Default -Transcript are useful scenario, but if you are not support it, then (taking in account that OutDefaultCommand is the only place where TranscribeOnly modified) the only value that _savedTranscribeOnly will have is false. So, why bother and define variable, which will be always false?

Copy link
Contributor

@PetSerAl PetSerAl Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You define _transcriptStateChanged as static, so it affect entire AppDomain. That might be not good when multiple PowerShell Runspaces share same AppDomain.

& { [PowerShell]::Create().AddCommand('Out-Default').AddParameter('Transcript').Invoke() } | Out-Default -Transcript

@SteveL-MSFT
Copy link
Member Author

@PetSerAl I appreciate the feedback. I rethought about how BeginProcessing, ProcessRecord, and EndProcessing work and I think I have a solution that remedies your concerns

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just asking - but should TranscribeOnly be a counter that is incremented each time it's true, and decremented in Dispose?

I'm not sure this is the correct semantic, but it seems right - like it shouldn't be possible to turn it off as long as somebody wants it on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only learned that Out-Default had a -Transcript switch when this issues was opened and I started looking into it (documentation and tests are also lacking).

Best as I can tell, although you can have nested Start-Transcript, Out-Default is only used at the end of a pipeline, so the test case of Out-Default -Transcript | Out-Default -Transcript I don't think represents a real use case as the intent of -Transcript (as far as I can tell) is to suppress output to the pipeline and only send it to the transcript. However, it got the console stuck in transcript mode, so I thought it was worth fixing (the other issue with exception being raised is a real use case).

So I don't think we need a counter here as it should really just toggle from true and false when this cmdlet is used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as the intent of -Transcript (as far as I can tell) is to suppress output to the pipeline

Sadly it is exact opposite of how Out-Default -Transcript is implemented. By default Out-Default does not output anything, but display input on host. With -Transcript it pass thru the input, but making following Out-Default/Out-Host to not display their input:

1..10 | Out-Default -Transcript | Out-Host

But by replacing Out-Host with Write-Host you can see, that output is really here:

1..10 | Out-Default -Transcript | Write-Host

Copy link
Member Author

@SteveL-MSFT SteveL-MSFT Mar 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code, the only thing TranscribeOnly does is not write to the console so I mispoke when I said pipeline (should have said host).

@PetSerAl
Copy link
Contributor

@SteveL-MSFT I can not think of any problematic cases now, at least Out-Default that PowerShell console host implicitly adds to the pipeline will restore TranscribeOnly to false at the end of command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need explicitly state IDisposable here? FrontEndCommandBase already implement IDisposable and it is not like you need to reimplement IDisposable.Dispose() here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a leftover from before I realized FrontEndCommandBase implements IDisposable. Will remove.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are not you supposed to call base.InternalDispose() at some point of overriding method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct. Will fix.

…uent use was caching the already changed state

Out-Default needed to implement IDispose pattern so transcription state reverts back on exception in pipeline
addressed feedback from code review on using static
@SteveL-MSFT
Copy link
Member Author

@PetSerAl @lzybkr any other feedback preventing signoff?

@PetSerAl
Copy link
Contributor

@SteveL-MSFT I do not see any obvious wrong things. I still can construct the case where host stuck in TranscribeOnly state, but it looks more likely shooting your own leg:

begin{
    $sp = {Out-Default -Transcript}.GetSteppablePipeline()
    $sp.Begin($false)
    $sp.Process()
}
end{
    $sp.End()
    $sp.Dispose()
}

Possible, the only way to prevent this is to implement TranscribeOnly as counter as @lzybkr suggest.
Maybe you should take into account possibility, that cmdlet can be instantiated not for execution, but for parameter/argument completion (not sure if it is the case here as Out-Default does not implement dynamic parameters). In that case BeginProcessing will not be invoked before disposing, so you will end restoring TranscribeOnly when you are not save it.

@SteveL-MSFT
Copy link
Member Author

I'll rework it to use a counter. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are doing this in process block, once for each input item, not once per Out-Default invocation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving back to BeginProcessing()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not checking if you actually increment TranscribeOnlyCount before. That lead to situation where every call to Out-Default reduce counter. Including ones without -Transcript. Or where begin block of Out-Default does not have opportunity to execute:

& {begin{throw}} | Out-Default -Transcript

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe wrap this with try/finally, so you have opportunity to clean up even if base.InternalDispose() throw exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

… -Trascript stays in TranscibeOnly

Add try..finally in InternalDispose to ensure TranscribeOnlyCount gets decremented
@PetSerAl
Copy link
Contributor

PetSerAl commented Mar 26, 2017

To think about that. Should not it also properly revert TranscribeOnly even if Dispose is not called, but Out-Default collected by garbage collector?

$sp = {Out-Default -Transcript}.GetSteppablePipeline()
$sp.Begin($false)
$sp = $null
[GC]::Collect()

I have idea about implementation. Can you look at it?

@SteveL-MSFT
Copy link
Member Author

@PetSerAl I'll look at this today

@SteveL-MSFT
Copy link
Member Author

@PetSerAl I stepped through the debugger and BeginProcessing is always called and ProcessRecord is called where InputObject is empty. Finally, Dispose() is called. In both your samples, once the script is complete, Host.UI.TranscribeOnlyCount = 0.

@PetSerAl
Copy link
Contributor

@SteveL-MSFT Not sure, what exact code you are referencing as my samples, but I use following code for test:

PS> Add-Type @‘
>>>     using System;
>>>     using System.Management.Automation;
>>>     [Cmdlet("Test", "Methods")]
>>>     public class TestMethods : PSCmdlet, IDisposable {
>>>         public TestMethods() {
>>>             Console.WriteLine("Constructor");
>>>         }
>>>         ~TestMethods() {
>>>             Console.WriteLine("Destructor");
>>>         }
>>>         protected override void BeginProcessing() {
>>>             Console.WriteLine("Begin");
>>>         }
>>>         protected override void ProcessRecord() {
>>>             Console.WriteLine("Process");
>>>         }
>>>         protected override void EndProcessing() {
>>>             Console.WriteLine("End");
>>>         }
>>>         protected override void StopProcessing() {
>>>             Console.WriteLine("Stop");
>>>         }
>>>         public void Dispose() {
>>>             Console.WriteLine("Dispose");
>>>         }
>>>     }
>>> ’@ -PassThru | Select-Object -First 1 -ExpandProperty Assembly | Import-Module
PS> $sp = {Test-Methods}.GetSteppablePipeline();
Constructor
PS> $sp.Begin($false)
Begin
PS> $sp = $null
PS> [GC]::Collect()
Destructor
PS>

As you can see, Dispose is not called here.

@SteveL-MSFT
Copy link
Member Author

SteveL-MSFT commented Mar 27, 2017

@PetSerAl, by samples, I mean:

begin{
    $sp = {Out-Default -Transcript}.GetSteppablePipeline()
    $sp.Begin($false)
    $sp.Process()
}
end{
    $sp.End()
    $sp.Dispose()
}

and

$sp = {Out-Default -Transcript}.GetSteppablePipeline()
$sp.Begin($false)
$sp = $null
[GC]::Collect()

In both of these cases, the Out-Default cmdlet's Begin/Process/Dispose are called (where -Transcript is passed). I was trying to incorporate both of these as Pester tests, but didn't observe the expected failing behavior.

@PetSerAl
Copy link
Contributor

@SteveL-MSFT The first sample got fixed, when you implement TranscribeOnly as a counter. But second one is still lock console in transcribe only mode, when I test in on this commit:

PS> $sp = {Out-Default -Transcript}.GetSteppablePipeline()
PS> $sp.Begin($false)
PS> $sp = $null
PS> [GC]::Collect()
PS> 1..10
PS> [System.Management.Automation.Host.PSHostUserInterface].GetProperty('TranscribeOnlyCount', [System.Reflection.BindingFlags]'Instance, NonPublic').GetValue($Host.UI) | Write-Host
1
PS>

@SteveL-MSFT
Copy link
Member Author

@PetSerAl your proposed change looks good to me, can you submit that as a PR and include the tests I added to this PR? I'll close this one

@PetSerAl
Copy link
Contributor

@SteveL-MSFT OK. I can do that.

@SteveL-MSFT
Copy link
Member Author

@PetSerAl thanks for your help on this

@SteveL-MSFT SteveL-MSFT deleted the outdefault branch June 28, 2017 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants