Powershell - How to script a session refresh in powershell? - powershell

I have one ps1 script that drives the operations I want to perform.
I am using modules with class definitions in the modules that use Command pattern.
All is well and good first time I open a powershell session console and run the script.
If I change a class in any way and re-run in the same console, the console does not seem to be picking up the changed script. I have to close the powershell console and run the script fresh in order for my changes work. Otherwise I just get the script behaving the same way it does before I made the change. Clearly there is some caching going on.
I am wondering if MS has finally resolved this issue. I have read many older posts with complaints about this.
I have tried the following and none of them appears to work:
Remove-Variable * -ErrorAction SilentlyContinue;
Remove-Module *;
$error.Clear();
Clear-Host
I have even tried all of them together. Still not helping.
Is there something else can can be done to ensure the latest code in any supporting modules gets loaded? Having to close the whole console and reload is a serious productivity issue.
Example of what I am doing:6
using module .\Logger.psm1
using module .\AzurePlatformParmsDefault.psm1
using module .\AzurePlatform.psm1
[Logger] $Logger = [Logger]::Create()
[AzurePlatformParms] $AzurePlatformParms = [AzurePlatformParmsDefault]::Create( $Logger )
[AzurePlatform] $AzurePlatform = [AzurePlatform]::Create( $Logger, $AzurePlatformParms )
[bool] $Result = $AzurePlatform.Execute()

The conventional wisdom is that there isn't a way to do this natively, and creating a new runspace or process is the solution.
You can reset variables to default values and import environment variables from the user/machine scope (on windows); before clearing any jobs, events, event subscribers etc. This isn't a true session refresh though, and classes/custom types will persist.
To speed up your workflow, you may want to use a function in your $profile that can automate creating a new session, and loading in what's needed. This approach can save enough time that it is trivial to recycle an interactive session. I will include the one I use in my profile as an example. It's fairly comprehensive, but I suggest tailoring one that is suitable for your specific needs.
Example
function Start-NewSession {
[CmdletBinding(DefaultParameterSetName = 'NoChange')]
[Alias('sans')]
param(
[Alias('N')]
[switch]
$NoClose,
[Parameter(ParameterSetName = 'Elevate')]
[Parameter(ParameterSetName = 'NoChange')]
[Alias('nop')]
[switch]
$NoProfile,
[Parameter(ParameterSetName = 'Elevate')]
[Parameter(ParameterSetName = 'NoChange')]
[Alias('A')]
[switch]
$AddCommands,
[Parameter(ParameterSetName = 'Elevate')]
[Alias('E')]
[switch]
$Elevate,
[Parameter(ParameterSetName = 'DeElevate')]
[Alias('D')]
[switch]
$DeElevate
)
$PSAppPath = (Get-Process -Id $PID).Path
$SPParams = #{
Filepath = $PSAppPath
WorkingDirectory = $PWD
ArgumentList = ''
}
if ($Elevate.IsPresent) {
$SPParams['Verb'] = 'RunAs'
}
elseif ($DeElevate.IsPresent) {
$SPParams['FilePath'] = Join-Path $env:windir 'explorer.exe'
$SPParams['ArgumentList'] = $PSAppPath
}
if ($NoProfile.IsPresent) {
$SPParams['ArgumentList'] += ' -NoProfile'
}
if ($AddCommands.IsPresent) {
$ExtraCmds = Read-Host -Prompt 'Post-startup commands'
if (-not [string]::IsNullOrWhiteSpace($ExtraCmds)) {
$SPParams['ArgumentList'] +=
' -NoExit -Command "' + $ExtraCmds.Replace('"', '\"') + '"'
}
}
if ([string]::IsNullOrWhiteSpace($SPParams['ArgumentList'])) {
$SPParams.Remove('ArgumentList')
}
Start-Process #SPParams
if (-not $NoClose.IsPresent) { exit }
}
This permits typing sans to generate a new session and close the old one.

Related

Bring a program to foreground or start it

For a handful of programs I use frequently, I am trying to write me some functions or aliases which would check if this program is already running and bring its window to foreground, else start this program.
Usage example with np, a handle for notepad.exe:
PS> np
checks if notepad.exe is running (Get-Process -Name "notepad.exe") if not, it would start it. When Notepad is already running but my maximized console is in the foreground, I'd like to execute the same command again, but this time I want it to bring the already running notepad process to foreground, rather than start a new one.
In order to implement this, I created this class called Program which I would instantiate for every program I want to handle like this. Then I have a HashTable $knownprograms of instances of this class, and in the end I try to define functions for every program, so that I could just type two or three letters to the console to start a program or bring its running process back to foreground.
class Program {
[string]$Name
[string]$Path
[string]$Executable
[string[]]$Arguments
Program(
[string]$n,
[string]$p,
[string]$e,
[string[]]$a
){
$this.Name = $n
$this.Path = $p
$this.Executable = $e
$this.Arguments = $a
}
[string]FullPath(){
return ("{0}\{1}" -f $this.Path, $this.Executable)
}
[void]ShowOrStart(){
try {
# Adapted from https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/bringing-window-in-the-foreground
$Process = Get-Process -Name $this.Name -ErrorAction Stop
Write-Host "Found at least one process called $this.Name"
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
$Mode = 4 # Will restore the window, not maximize it
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
} catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
Write-Host "Did not find any process called $this.Name"
Invoke-Command -ScriptBlock { & $this.FullPath() $this.Arguments }
}
}
}
$knownprograms = #{}
$knownprograms.Add("np", [Program]::new(
"np",
"$Env:SystemRoot\System32",
"notepad.exe",
#())
)
$knownprograms.Add("pt", [Program]::new(
"pt",
"$Env:SystemRoot\System32",
"mspaint.exe",
#())
)
Function np {
[cmdletbinding()]
Param()
$knownprograms.np.ShowOrStart()
}
Function pt {
[cmdletbinding()]
Param()
$knownprograms.pt.ShowOrStart()
}
The idea would be that I would source this script in my profile.ps1 and then just use the pre-factored functions. However, it seems that this code always opens a new instance of the program, rather than using its running process. Maybe I need some sort of delayed evaluation, so that the ShowOrStart() method checks at the time of invocation of np or pt whether the associated process exists. Any ideas how to accomplish this?
The process name for notepad.exe is notepad.
Update
$knownprograms.Add("np", [Program]::new(
"notepad",
"$Env:SystemRoot\System32",
"notepad.exe",
#())
)
And this works as expected.
This would be probably interesting to register $sig once for all and not on every call (which will probably raise an error).

Testing for mandatory parameters with Pester

I'm trying to figure out how to have Pester test for parameters that are missing:
Find-Waldo.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
Describe 'Mandatory paramters' {
it 'ComputerName' {
{
$Params = #{
#ComputerName = 'MyPc'
ScriptName = 'Test'
}
. "$here\$sut" #Params
} | Should throw
}
}
Find-Waldo.ps1
Param (
[Parameter(Mandatory)]
[String]$ComputerName,
[String]$ScriptName
)
Function Find-Waldo {
[CmdletBinding()]
Param (
[String]$FilePath
)
'Do something'
}
Every time I try to assert the result or simply run the test, it will prompt me for the ComputerName parameter instead of failing the test.
Am I missing something super obvious here? Is there a way to test for the presence of mandatory parameters?
Per the comments from Mathias, you can't really test for whether a Mandatory parameter is missing because PowerShell prompts for it rather than throwing an error. Per the comment he linked to from the Pester team you could use Get-Command to test for the Mandatory parameter setting in the script (assuming it is the only parameter attribute set for that variable)
((Get-Command "$here\$sut").Parameters['ComputerName'].Attributes.Mandatory | Should Be $true
An alternative option would be to not use Mandatory parameters in this instance, and instead have a script block that does a Throw as the default value of the parameter:
Param (
[String]$ComputerName = $(Throw '-ComputerName is required'),
[String]$ScriptName
)
If the script is always used as part of an automated process (instead of via user execution) this might be preferred as it allows you to control/capture its behavior and avoids it getting stuck during execution. You can then test the script as you had originally proposed:
Describe 'Mandatory paramters' {
it 'ComputerName' {
{
$Params = #{
#ComputerName = 'MyPc'
ScriptName = 'Test'
}
. "$here\$sut" #Params
} | Should throw '-ComputerName is required'
}
}
Although the accepted answer indicates that this isn't possible, it actually is possible. Here is the solution that I developed to solve for this problem.
It 'Should fail when no priority is specified, for a valid process name' {
{
$ScriptBlock = {
Import-Module -Name $args[0]
Set-ProcessPriority -Name System
}
Start-Job -ScriptBlock $ScriptBlock -ArgumentList $HOME/git/ProcessPriority/src/ProcessPriority | Wait-Job | Receive-Job
} | Should -Throw
}
What you'll notice from the above example is:
🚀 The code being tested has been wrapped in a PowerShell ScriptBlock
🚀 We invoke a PowerShell background job, containing the test code
🚀 We wait for the background job to complete, and then receive the results
🚀 If you run the Get-Job command, you'll notice that there is a job in the Blocked status
The exception that's thrown by the background job is similar to the following:
The Wait-Job cmdlet cannot finish working, because one or more jobs are blocked waiting for user interaction. Process interactive job output by using the Receive-Job cmdlet, and then try again.
You'll notice that I hard-coded the filesystem path to the module. I am not sure how to pass this as an argument into the "outer" ScriptBlock that Pester is invoking for us. Perhaps someone has a suggestion on how to accomplish that final piece of the puzzle.
What's uniquely interesting about PowerShell background jobs is that you can actually resume a job in the Blocked status, and it will prompt you for input, even though it threw the earlier exception.

Powershell workflow function issue

I´m trying to reuse code on my SMA runbooks but everything I try to put inside a function doesn´t seem to work as expected.
For example, If I do this it works and returns the username of the credential:
workflow RB_Test
{
$credent = Get-AutomationPSCredential -Name "CRED_TESTE"
$var = $credent.Username
"result = ${var}"
}
Output:
But if I turn into this it doesn't work anymore (returns null):
workflow RB_Test
{
function FN_Test
{
$credent = Get-AutomationPSCredential -Name "CRED_TESTE"
$var = $credent.Username
"result = ${var}"
}
FN_Test
}
Output:
I've tried different things but without success. The debug/verbose screen don't return anything different. That also doesn't work:
Inlinescript {
. FN_Test
}
My goal would be to put several functions into a separate module and then import it on my runbooks for reusability but this really seems not to work.
This is a runbook (powershell workflow) created in the Service Management Automation (SMA).
I've read that there are some restrictions with Powershell workflow compared to pure Powershell but I am not sure if I am hitting one of them:
https://blogs.technet.microsoft.com/heyscriptingguy/2013/01/02/powershell-workflows-restrictions/
Thanks
Here's what I've had to do to get functions to work:
workflow FunctionTest {
function log {
param(
[string]$Message
)
Write-Output $Message
Write-Output "Filename: $Filename"
Write-Output "using:Filename: $using:Filename"
Write-Output "workflow:Filename: $workflow:Filename"
Write-Output "----"
## Under what conditions is 'global' used? Can't be used in a workflow...Hey Scripting Guy!
}
workflow DoSomething {
param(
[string]$Filename
)
log "Starting DoSomething"
}
$Filename = "LogFile_2017.csv"
log "Starting workflow"
## Variables need to be passed into workflow from parent-workflow
DoSomething -Filename $Filename
log "End workflow"
}
FunctionTest
I found you need to define your functions before using them. The tricky part was discovering that you have to pass your variables into the child-workflow.
The scope of the variables takes some getting used to.

PowerShell Param ValidateSet values with Spaces and Tab completion

First, I do apologize for posting another question concerning PowerShell and tab completion. The StackOverflow system identified several excellent questions with answers concerning this very topic, but they all seemed too cumbersome to implement into this simple New-ADComputer script.
The params are going into a Splat to keep the script readable. The following code correctly tab completes in the ISE, but must be wrapped in double quotes.
Is there any native method in PowerShell to allow for tab completion of Parameter Sets that include spaces?
Param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('Env1','Env 2','Env 3')]
[string]$Environment,
[Parameter(Mandatory=$true)]
[ValidateSet('Application','Database','File and Print','Web Server')]
[string]$Type
)
$NewADitems = #{
Name = $server
Path = "OU=$Type,OU=$Environment,OU=Smaller DN string"
Location ='MySite'
Description = "Test Description"
ManagedBy = "Huge Distingushed Name string"
WhatIf = $true
}
Write-Host #NewADitems
Command used and error received:
PS C:\Scripts> .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Print
C:\Scripts\ADComputer-ParamTest.ps1 : Cannot validate argument on parameter
'Environment'. The argument "Env" does not belong to the set "Env1,Env 2,Env3"
specified by the ValidateSet attribute. Supply an argument that is in the
set and then try the command again.At line:1 char:58
+ .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Pr ...
+ ~~~
Edit: More information. If you leave off the single/double quotes in my example script for the parameter Environment, tab completion will not work for the final parameter Type. Enclosing the 2nd set in quotes will correct this but it's a way to keep watch for this behavior.
No, at least up to Powershell 5.0 April 2015 preview. Tab completion works as you describe. It will still need the quotes around the set to actually work without throwing the error. For what it's worth, it does add the closing quote of the matching type when you start the tab completion with a quote. For example, pressing "f then Tab will complete to "File and Print"(not sure when that was added as a feature).
I tried finding ways to auto-include the quotes as part of the ValidateSet including additional double quotes around the parameter sets and other attempts at escaping quotes. All attempts resulted in tab completion not working in various ways.
Some of the attempts, in case anyone might try that avenue:
[ValidateSet('Env1','"Env 2"','"Env 3"')]
[ValidateSet('Env1',"'Env 2'","'Env 3'")]
[ValidateSet('Env1','`"Env 2`"',"`'Env 3`'")]
[ValidateSet('Env1','\"Env 2\"',"\'Env 3\'")]
This has been entered as a bug since 2013. According to the workarounds listed in Auto-completed parameter values, with spaces, do not have quotes around them, you can update the TabExpansion2 function that Powershell uses for autocompletion. To do so, just run the following code:
function TabExpansion2
{
[CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
Param(
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)]
[string] $inputScript,
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)]
[int] $cursorColumn,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)]
[System.Management.Automation.Language.Ast] $ast,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)]
[System.Management.Automation.Language.Token[]] $tokens,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)]
[System.Management.Automation.Language.IScriptPosition] $positionOfCursor,
[Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
[Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
[Hashtable] $options = $null
)
End
{
if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$inputScript,
$cursorColumn,
$options)
}
else
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$ast,
$tokens,
$positionOfCursor,
$options)
}
$count = $completion.CompletionMatches.Count
for ($i = 0; $i -lt $count; $i++)
{
$result = $completion.CompletionMatches[$i]
if ($result.CompletionText -match '\s')
{
$completion.CompletionMatches[$i] = New-Object System.Management.Automation.CompletionResult(
"'$($result.CompletionText)'",
$result.ListItemText,
$result.ResultType,
$result.ToolTip
)
}
}
return $completion
}
}
It's worth noting that string insertion works properly for native cmdlets like Get-EventLog -LogName which will properly encase 'Internet Explorer'. Although if you look at the source for Get-EventLog, you'll see that $LogName doesn't actually use ValidateSet so it's intellisense must be provided through another mechanism.
Other Instances:
ValidateSet and tab completion does not work on strings with spaces

PowerShell DSC - how to pass configuration parameters to ScriptResources?

I'm having a lot of trouble trying to get a PowerShell Desired State Configuration script working to configure an in-house application. The root of the problem is that I can't seem to pass my configuration data down into a ScriptResource (at least not with the way I'm trying to do it).
My script is supposed to create a config folder for our in-house application, and then write some settings into a file:
configuration MyApp {
param (
[string[]] $ComputerName = $env:ComputerName
)
node $ComputerName {
File ConfigurationFolder {
Type = "Directory"
DestinationPath = $Node.ConfigFolder
Ensure = "Present"
}
Script ConfigurationFile {
SetScript = {
write-verbose "running ConfigurationFile.SetScript";
write-verbose "folder = $($Node.ConfigFolder)";
write-verbose "filename = $($Node.ConfigFile)";
[System.IO.File]::WriteAllText($Node.ConfigFile, "enabled=" + $Node.Enabled);
}
TestScript = {
write-verbose "running ConfigurationFile.TestScript";
write-verbose "folder = $($Node.ConfigFolder)";
write-verbose "filename = $($Node.ConfigFile)";
return (Test-Path $Node.ConfigFile);
}
GetScript = { #{Configured = (Test-Path $Node.ConfigFile)} }
DependsOn = "[File]ConfigurationFolder"
}
}
}
For reference, my configuration data looks like this:
$config = #{
AllNodes = #(
#{
NodeName = "*"
ConfigFolder = "C:\myapp\config"
ConfigFile = "C:\myapp\config\config.txt"
}
#{
NodeName = "ServerA"
Enabled = "true"
}
#{
NodeName = "ServerB"
Enabled = "false"
}
)
}
And I'm applying DSC with the following:
$mof = MyApp -ConfigurationData $config;
Start-DscConfiguration MyApp –Wait –Verbose;
When I apply this configuration it happily creates the folder, but fails to do anything with the config file. Looking at the output below, it's obvious that it's because the $Node variable is null inside the scope of ConfigurationFile / TestScript, but I've got no idea how to reference it from within that block.
LCM: [ Start Resource ] [[Script]ConfigurationFile]
LCM: [ Start Test ] [[Script]ConfigurationFile]
[[Script]ConfigurationFile] running ConfigurationFile.TestScript
[[Script]ConfigurationFile] node is null = True
[[Script]ConfigurationFile] folder =
[[Script]ConfigurationFile] filename =
LCM: [ End Test ] [[Script]ConfigurationFile] in 0.4850 seconds.
I've burnt off an entire day searching online for this specific problem, but all the examples of variables, parameters and configuration data all use File and Registry resources or other non-script resources, which I've already got working in the "ConfigurationFolder" block in my script. The thing I'm stuck on is how to reference the configuration data from within a Script resource like my "ConfigurationFile".
I've drawn a complete blank so any help would be greatly appreciated. If all else fails I may have to create a separate "configuration" script per server and hard-code the values, which I really don't want to do if at all possible.
Cheers,
Mike
Change this: $Node.ConfigFolder to $using:Node.ConfigFolder.
If you have a variable called $Foo and you want it to be passed to a script DSC resource, then use $using:Foo
Based on David's answer, I've written a utility function which converts my script block to a string and then performs a very naive search and replace to expand out references to the configuration data as follows.
function Format-DscScriptBlock()
{
param(
[parameter(Mandatory=$true)]
[System.Collections.Hashtable] $node,
[parameter(Mandatory=$true)]
[System.Management.Automation.ScriptBlock] $scriptBlock
)
$result = $scriptBlock.ToString();
foreach( $key in $node.Keys )
{
$result = $result.Replace("`$Node.$key", $node[$key]);
}
return $result;
}
My SetScript then becomes:
SetScript = Format-DscScriptBlock -Node $Node -ScriptBlock {
write-verbose "running ConfigurationFile.SetScript";
write-verbose "folder = $Node.ConfigFolder";
write-verbose "filename = $Node.ConfigFile)";
[System.IO.File]::WriteAllText("$Node.ConfigFile", "enabled=" + $Node.Enabled);
}
You have to be mindful of quotes and escapes in your configuration data because Format-DscScriptBlock only performs literal substitution, but this was good enough for my purposes.
A quite elegant way to solve this problem is to work with the regular {0} placeholders. By applying the -f operator the placeholders can be replaced with their actual values.
The only downside with this method is that you cannot use the curly braces { } for anything other than placeholders (i.e. say a hashtable or a for-loop), because the -f operator requires the braces to contain an integer.
Your code then looks like this:
SetScript = ({
Set-ItemProperty "IIS:\AppPools\{0}" "managedRuntimeVersion" "v4.0"
Set-ItemProperty "IIS:\AppPools\{0}" "managedPipelineMode" 1 # 0 = Integrated, 1 = Classic
} -f #($ApplicationPoolName))
Also, a good way to find out if you're doing it right is by simply viewing the generated .mof file with a text editor; if you look at the generated TestScript / GetScript / SetScript members, you'll see that the code fragment really is a string. The $placeholder values should already have been replaced there.
ConfigurationData only exists at the time the MOF files are compiled, not at runtime when the DSC engine applies your scripts. The SetScript, GetScript, and TestScript attributes of the Script resource are actually strings, not script blocks.
It's possible to generate those script strings (with all of the required data from your ConfigurationData already expanded), but you have to be careful to use escapes, subexpressions and quotation marks correctly.
I posted a brief example of this over on the original TechNet thread at http://social.technet.microsoft.com/Forums/en-US/2eb97d67-f1fb-4857-8840-de9c4cb9cae0/dsc-configuration-data-for-script-resources?forum=winserverpowershell