private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) { await debugInfoHandle.WaitAsync().ConfigureAwait(false); try { nextVariableId = VariableDetailsBase.FirstVariableId; variables = new List <VariableDetailsBase> { // Create a dummy variable for index 0, should never see this. new VariableDetails("Dummy", null) }; // Must retrieve in order of broadest to narrowest scope for efficient // deduplication: global, script, local. globalScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false); scriptScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false); localScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false); await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); } finally { debugInfoHandle.Release(); } }
private async Task FetchGlobalAndScriptVariablesAsync() { // Retrieve globals first as script variable retrieval needs to search globals. this.globalScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null).ConfigureAwait(false); this.scriptScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); }
private async Task FetchStackFramesAsync(string scriptNameOverride) { PSCommand psCommand = new PSCommand(); // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame // objects (or "deserialized" CallStackFrames) when attached to a runspace in another // process. Without the intermediate variable Get-PSCallStack inexplicably returns // an array of strings containing the formatted output of the CallStackFrame list. var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); var results = await this.powerShellContext.ExecuteCommandAsync <PSObject>(psCommand).ConfigureAwait(false); var callStackFrames = results.ToArray(); this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; for (int i = 0; i < callStackFrames.Length; i++) { VariableContainerDetails autoVariables = new VariableContainerDetails( this.nextVariableId++, VariableContainerDetails.AutoVariablesName); this.variables.Add(autoVariables); VariableContainerDetails localVariables = await FetchVariableContainerAsync(i.ToString(), autoVariables).ConfigureAwait(false); // When debugging, this is the best way I can find to get what is likely the workspace root. // This is controlled by the "cwd:" setting in the launch config. string workspaceRootPath = this.powerShellContext.InitialWorkingDirectory; this.stackFrameDetails[i] = StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; if (scriptNameOverride != null && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { this.stackFrameDetails[i].ScriptPath = scriptNameOverride; } else if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote && this.remoteFileManager != null && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { this.stackFrameDetails[i].ScriptPath = this.remoteFileManager.GetMappedPath( stackFrameScriptPath, this.powerShellContext.CurrentRunspace); } } }
private async Task <VariableContainerDetails> FetchVariableContainerAsync( string scope, VariableContainerDetails autoVariables) { PSCommand psCommand = new PSCommand(); psCommand.AddCommand("Get-Variable"); psCommand.AddParameter("Scope", scope); var scopeVariableContainer = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); this.variables.Add(scopeVariableContainer); var results = await this.powerShellContext.ExecuteCommandAsync <PSObject>(psCommand, sendErrorToHost : false).ConfigureAwait(false); if (results != null) { foreach (PSObject psVariableObject in results) { var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; this.variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope)) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } } } return(scopeVariableContainer); }
/// <summary> /// Sets the specified variable by container variableReferenceId and variable name to the /// specified new value. If the variable cannot be set or converted to that value this /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or /// SessionStateUnauthorizedAccessException. /// </summary> /// <param name="variableContainerReferenceId">The container (Autos, Local, Script, Global) that holds the variable.</param> /// <param name="name">The name of the variable prefixed with $.</param> /// <param name="value">The new string value. This value must not be null. If you want to set the variable to $null /// pass in the string "$null".</param> /// <returns>The string representation of the value the variable was set to.</returns> /// <exception cref="InvalidPowerShellExpressionException"></exception> public async Task <string> SetVariableAsync(int variableContainerReferenceId, string name, string value) { Validate.IsNotNull(nameof(name), name); Validate.IsNotNull(nameof(value), value); _logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); // An empty or whitespace only value is not a valid expression for SetVariable. if (value.Trim().Length == 0) { throw new InvalidPowerShellExpressionException("Expected an expression."); } // Evaluate the expression to get back a PowerShell object from the expression string. // This may throw, in which case the exception is propagated to the caller PSCommand evaluateExpressionCommand = new PSCommand().AddScript(value); IReadOnlyList <object> expressionResults = await _executionService.ExecutePSCommandAsync <object>(evaluateExpressionCommand, CancellationToken.None).ConfigureAwait(false); if (expressionResults.Count == 0) { throw new InvalidPowerShellExpressionException("Expected an expression result."); } object expressionResult = expressionResults[0]; // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. // Ideally we would have a separate means from communicating error records apart from normal output. if (expressionResult is ErrorRecord errorRecord) { throw new InvalidPowerShellExpressionException(errorRecord.ToString()); } // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. // Get the variable referenced by variableContainerReferenceId and variable name. VariableContainerDetails variableContainer = null; await debugInfoHandle.WaitAsync().ConfigureAwait(false); try { variableContainer = (VariableContainerDetails)variables[variableContainerReferenceId]; } finally { debugInfoHandle.Release(); } VariableDetailsBase variable = variableContainer.Children[name]; // Determine scope in which the variable lives so we can pass it to `Get-Variable // -Scope`. The default is scope 0 which is safe because if a user is able to see a // 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"; // NOTE: This can't use a switch because the IDs aren't constant. if (variableContainerReferenceId == localScopeVariables.Id) { scope = VariableContainerDetails.LocalScopeName; } else if (variableContainerReferenceId == scriptScopeVariables.Id) { scope = VariableContainerDetails.ScriptScopeName; } else if (variableContainerReferenceId == globalScopeVariables.Id) { scope = VariableContainerDetails.GlobalScopeName; } // Now that we have the scope, get the associated PSVariable object for the variable to be set. PSCommand getVariableCommand = new PSCommand() .AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable") .AddParameter("Name", name.TrimStart('$')) .AddParameter("Scope", scope); IReadOnlyList <PSVariable> psVariables = await _executionService.ExecutePSCommandAsync <PSVariable>(getVariableCommand, CancellationToken.None).ConfigureAwait(false); if (psVariables.Count == 0) { throw new Exception("Failed to retrieve PSVariables"); } PSVariable psVariable = psVariables[0]; if (psVariable is null) { throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); } // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - // ArgumentTransformationAttribute. ArgumentTransformationAttribute argTypeConverterAttr = null; foreach (Attribute variableAttribute in psVariable.Attributes) { if (variableAttribute is ArgumentTransformationAttribute argTransformAttr && argTransformAttr.GetType().Name.Equals("ArgumentTypeConverterAttribute")) { argTypeConverterAttr = argTransformAttr; break; } } if (argTypeConverterAttr is not null) { // PSVariable *is* strongly typed, so we have to convert it. _logger.LogTrace($"Setting variable '{name}' using conversion to value: {expressionResult ?? "<null>"}"); // NOTE: We use 'Get-Variable' here instead of 'SessionStateProxy.GetVariable()' // because we already have a pipeline running (the debugger) and the latter cannot // run concurrently (threw 'NoSessionStateProxyWhenPipelineInProgress'). IReadOnlyList <EngineIntrinsics> results = await _executionService.ExecutePSCommandAsync <EngineIntrinsics>( new PSCommand() .AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable") .AddParameter("Name", "ExecutionContext") .AddParameter("ValueOnly"), CancellationToken.None).ConfigureAwait(false); EngineIntrinsics engineIntrinsics = results.Count > 0 ? results[0] : throw new Exception("Couldn't get EngineIntrinsics!"); // TODO: This is almost (but not quite) the same as 'LanguagePrimitives.Convert()', // which does not require the pipeline thread. We should investigate changing it. psVariable.Value = argTypeConverterAttr.Transform(engineIntrinsics, expressionResult); } else { // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. _logger.LogTrace($"Setting variable '{name}' directly to value: {expressionResult ?? "<null>"} - previous type was {psVariable.Value?.GetType().Name ?? "<unknown>"}"); psVariable.Value = expressionResult; } // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. // This makes the returned string consistent with the strings normally displayed for variables in the debugger. VariableDetails tempVariable = new(psVariable); _logger.LogTrace($"Set variable '{name}' to: {tempVariable.ValueString ?? "<null>"}"); return(tempVariable.ValueString); }
/// <summary> /// Sets the specified variable by container variableReferenceId and variable name to the /// specified new value. If the variable cannot be set or converted to that value this /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or /// SessionStateUnauthorizedAccessException. /// </summary> /// <param name="variableContainerReferenceId">The container (Autos, Local, Script, Global) that holds the variable.</param> /// <param name="name">The name of the variable prefixed with $.</param> /// <param name="value">The new string value. This value must not be null. If you want to set the variable to $null /// pass in the string "$null".</param> /// <returns>The string representation of the value the variable was set to.</returns> public async Task <string> SetVariableAsync(int variableContainerReferenceId, string name, string value) { Validate.IsNotNull(nameof(name), name); Validate.IsNotNull(nameof(value), value); this.logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); // An empty or whitespace only value is not a valid expression for SetVariable. if (value.Trim().Length == 0) { throw new InvalidPowerShellExpressionException("Expected an expression."); } // Evaluate the expression to get back a PowerShell object from the expression string. PSCommand psCommand = new PSCommand(); psCommand.AddScript(value); var errorMessages = new StringBuilder(); var results = await this.powerShellContext.ExecuteCommandAsync <object>( psCommand, errorMessages, false, false).ConfigureAwait(false); // Check if PowerShell's evaluation of the expression resulted in an error. object psobject = results.FirstOrDefault(); if ((psobject == null) && (errorMessages.Length > 0)) { throw new InvalidPowerShellExpressionException(errorMessages.ToString()); } // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. // Ideally we would have a separate means from communicating error records apart from normal output. if (psobject is ErrorRecord errorRecord) { throw new InvalidPowerShellExpressionException(errorRecord.ToString()); } // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. // Get the variable referenced by variableContainerReferenceId and variable name. VariableContainerDetails variableContainer = null; await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); try { variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; } finally { this.debugInfoHandle.Release(); } VariableDetailsBase variable = variableContainer.Children[name]; // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. string scope = null; if (variableContainerReferenceId == this.scriptScopeVariables.Id) { scope = "Script"; } else if (variableContainerReferenceId == this.globalScopeVariables.Id) { scope = "Global"; } else { // Determine which stackframe's local scope the variable is in. StackFrameDetails[] stackFrames = await this.GetStackFramesAsync().ConfigureAwait(false); for (int i = 0; i < stackFrames.Length; i++) { var stackFrame = stackFrames[i]; if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) { scope = i.ToString(); break; } } } if (scope == null) { // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. throw new Exception("Could not find the scope for this variable."); } // Now that we have the scope, get the associated PSVariable object for the variable to be set. psCommand.Commands.Clear(); psCommand = new PSCommand(); psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable"); psCommand.AddParameter("Name", name.TrimStart('$')); psCommand.AddParameter("Scope", scope); IEnumerable <PSVariable> result = await this.powerShellContext.ExecuteCommandAsync <PSVariable>(psCommand, sendErrorToHost : false).ConfigureAwait(false); PSVariable psVariable = result.FirstOrDefault(); if (psVariable == null) { throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); } // We have the PSVariable object for the variable the user wants to set and an object to assign to that variable. // The last step is to determine whether the PSVariable is "strongly typed" which may require a conversion. // If it is not strongly typed, we simply assign the object directly to the PSVariable potentially changing its type. // Turns out ArgumentTypeConverterAttribute is not public. So we call the attribute through it's base class - // ArgumentTransformationAttribute. var argTypeConverterAttr = psVariable.Attributes .OfType <ArgumentTransformationAttribute>() .FirstOrDefault(a => a.GetType().Name.Equals("ArgumentTypeConverterAttribute")); 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); } else { // PSVariable is *not* strongly typed. In this case, whack the old value with the new value. var msg = $"Setting variable '{name}' directly to value: {psobject ?? "<null>"} - previous type was {psVariable.Value?.GetType().Name ?? "<unknown>"}"; this.logger.LogTrace(msg); psVariable.Value = psobject; } // Use the VariableDetails.ValueString functionality to get the string representation for client debugger. // This makes the returned string consistent with the strings normally displayed for variables in the debugger. var tempVariable = new VariableDetails(psVariable); this.logger.LogTrace($"Set variable '{name}' to: {tempVariable.ValueString ?? "<null>"}"); return(tempVariable.ValueString); }