Skip to content

Fix DebuggerSetsVariablesWithConversion test #1664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 19, 2022
Merged

Conversation

andyleejordan
Copy link
Member

With these two bugs fixed, we can now set variables which are strongly typed (and so need a conversion) through the debugger. This was a great unit test that indicated a bug, and it was easily reproducible.

While testing manually, the scope bug revealed itself when I attempted to set a variable in the "automatic variables" container (the test is using the local variables container). I think assuming a default scope of "0" is sane because if it's an automatic variable, it must be in the local scope, and every other container is mapped to a named scope. As we learned previously, we cannot "guess" at a scope because they do not map one-to-one with stack frames.

The conversion bug was due to SessionStateProxy.GetVariable("ExecutionContext") throwing NoSessionStateProxyWhenPipelineInProgress. Initially I thought it could be solved by running it forcibly in the foreground (as the stack-trace showed it was being run via OnIdle); however, the same exception was thrown even then. I am not certain, but I think it is because the debugger is currently in a stopped state, which has left the pipeline running, and so this concurrency check fails. I don't know why it can't work concurrently, because this alternative means to get the EngineIntrinsics value works, and indeed, was the way we did it previously:

if (argTypeConverterAttr != null)
{
// PSVariable is strongly typed. Need to apply the conversion/transform to the new value.
psCommand.Commands.Clear();
psCommand = new PSCommand();
psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable");
psCommand.AddParameter("Name", "ExecutionContext");
psCommand.AddParameter("ValueOnly");
errorMessages.Clear();
var getExecContextResults =
await this.powerShellContext.ExecuteCommandAsync<object>(
psCommand,
errorMessages,
sendErrorToHost: false).ConfigureAwait(false);
EngineIntrinsics executionContext = getExecContextResults.OfType<EngineIntrinsics>().FirstOrDefault();
var msg = $"Setting variable '{name}' using conversion to value: {psobject ?? "<null>"}";
this.logger.LogTrace(msg);
psVariable.Value = argTypeConverterAttr.Transform(executionContext, psobject);

Fixes #1661

// variable in the debugger and so change it through this interface, it's either in the
// top-most scope or in one of the following named scopes. The default scope is most
// likely in the case of changing from the "auto variables" container.
string scope = "0";
Copy link
Member Author

Choose a reason for hiding this comment

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

@SeeminglyScience This seem sane to you?

// run concurrently (threw 'NoSessionStateProxyWhenPipelineInProgress').
IReadOnlyList<EngineIntrinsics> results = await _executionService.ExecutePSCommandAsync<EngineIntrinsics>(
new PSCommand()
.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable")
Copy link
Member Author

Choose a reason for hiding this comment

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

@rjmholt Do you have a better idea or explanation as to why that exception was being thrown?

Choose a reason for hiding this comment

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

This looks correct to me. The debugger knows it is running in an active pipeline and runs commands as 'nested' on the target (locally or remotely), whereas SessionStateProxy does not.

Comment on lines +476 to +477
// TODO: This is almost (but not quite) the same as 'LanguagePrimitives.Convert()',
// which does not require the pipeline thread. We should investigate changing it.
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 looked into this, but I don't think it will quite as "well" as the transformation, and I'm unsure actually how to go about it because LanguagePrimitives.ConvertTo() doesn't accept an ArgumentTypeConverterAttribute.

Choose a reason for hiding this comment

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

This will only work for local debugging sessions. Does this code make local/remote distinctions?

Copy link
Member Author

Choose a reason for hiding this comment

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

It does not make that distinction. Also, do you mean that LanguagePrimitives would only work for local, or the currently proposed change?

Choose a reason for hiding this comment

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

The ExecutionContext variable (engineIntrinsics) passes through the serialization system for the remoting case, and is just a PSObject property bag and not a System.Management.Automation.EngineIntrinsics type. The serialization system does not rehydrate returned object into their original types, except for a small set of special cases.

So if you have script code that determines variable scope, that script should run entirely on the remote target, so that it can run in the correct context and 'live' type instances.

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'm not quite sure I'm following. Can I ask: will the proposed change behave differently than the existing code in the remoting scenario? If not, then let's kick that down the road a bit.

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.e. are both implementations broken in the same way?)

Choose a reason for hiding this comment

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

Yes, I think so (remoting may be broken). But I need to look more closely at the code (and not just the changes).

@@ -641,7 +645,7 @@ private Task<VariableContainerDetails> FetchVariableContainerAsync(string scope)

private async Task<VariableContainerDetails> FetchVariableContainerAsync(string scope, bool autoVarsOnly)
{
PSCommand psCommand = new PSCommand().AddCommand("Get-Variable").AddParameter("Scope", scope);
PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable").AddParameter("Scope", scope);
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'm not sure why we consistently use the fully qualified name, but since we do, fixed this one too.

@@ -628,7 +628,7 @@ public async Task DebuggerSetsVariablesWithConversion()
VariableDetailsBase[] variables = GetVariables(VariableContainerDetails.LocalScopeName);

// Test set of a local string variable (not strongly typed but force conversion)
const string newStrValue = "False";
const string newStrValue = "\"False\"";
Copy link
Member Author

Choose a reason for hiding this comment

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

This was erroneously changed by me previously, and is now back to its original value.

consoleReplEnabled: false,
usesLegacyReadLine: false,
bundledModulePath: BundledModulePath);

var psesHost = new PsesInternalHost(loggerFactory, null, testHostDetails);

// NOTE: Because this is used by constructors it can't use await.
// TODO: Should we actually load profiles here?
if (psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = true }, CancellationToken.None).GetAwaiter().GetResult())
Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, now I'm actually wondering if this should be false. I thought I had my answer, and that's that we test we can load the profiles but we actually do the profile loading call explicitly. @rjmholt Do you know why this is explicitly set to true?

* Remove unused field
* Fix some indentation
* Name all arguments
* Delete TODO that's been answered
While we could put the entire task inside the lambda, thus eliminating
the warning, we then lose the ability to assert that the task itself was
cancelled. This feels important to be able to do since the exception
does not necessarily prove that this explicit task was canceled.
consoleReplEnabled: false,
usesLegacyReadLine: false,
bundledModulePath: BundledModulePath);

var psesHost = new PsesInternalHost(loggerFactory, null, testHostDetails);

// NOTE: Because this is used by constructors it can't use await.
// TODO: Should we actually load profiles here?
if (psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = true }, CancellationToken.None).GetAwaiter().GetResult())
if (psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = false }, CancellationToken.None).GetAwaiter().GetResult())
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've now set this to false because the tests which assert profile loading works do so by explicitly running the same code as would otherwise run in startup:

if (startOptions.LoadProfiles)
{
await LoadHostProfilesAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Profiles loaded");
}

I don't think we really need to be calling this delegate for every other test.

Copy link

@PaulHigin PaulHigin left a comment

Choose a reason for hiding this comment

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

Added a couple of comments/questions.

// run concurrently (threw 'NoSessionStateProxyWhenPipelineInProgress').
IReadOnlyList<EngineIntrinsics> results = await _executionService.ExecutePSCommandAsync<EngineIntrinsics>(
new PSCommand()
.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable")

Choose a reason for hiding this comment

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

This looks correct to me. The debugger knows it is running in an active pipeline and runs commands as 'nested' on the target (locally or remotely), whereas SessionStateProxy does not.

Comment on lines +476 to +477
// TODO: This is almost (but not quite) the same as 'LanguagePrimitives.Convert()',
// which does not require the pipeline thread. We should investigate changing it.

Choose a reason for hiding this comment

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

This will only work for local debugging sessions. Does this code make local/remote distinctions?

@andyleejordan andyleejordan merged commit 39d26ee into master Jan 19, 2022
@andyleejordan andyleejordan deleted the andschwa/set-var branch January 19, 2022 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Fix DebuggerSetsVariablesWithConversion test which indicates bug
2 participants