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();
            }
        }
Esempio n. 2
0
        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);
        }
Esempio n. 3
0
        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);
                }
            }
        }
Esempio n. 4
0
        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);
        }
Esempio n. 6
0
        /// <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);
        }