Anonymous clickhandler in foreach loop uses last variable state - forms

I guess this is a newbie question. Yet I did not find anything online...
I'm creating a small powershell script with a very simple gui.
This is the relevant part of the script:
foreach ($script in $scripts){
$btn = New-Object System.Windows.Forms.Button
#Text, Location, Size omitted
#Add clickhandler
$btn.Add_Click(
{
Write-Host "Clicked on Btn $script"
Start-Process -FilePath "powershell" -ArgumentList "-command", "`"$script`""
Write-Host "finished"
$Form.Close();
}
)
$Form.Controls.Add($btn)
}
Obviously $scripts contains paths pointing towards other powershell scripts.
Being a Java developer I was naiv enough to suspect every click handler to be created with its own reference to a script location ($script).
But of course powershell does not evaluate $script until the handler is invoked. Thus, every button will call the last element in my array $scripts since $script will reference the last element in $scripts after the loop completes.
How can I create a click handler inside a foreach-loop based on the loop-variable itself?
Solution
Mathias R. Jessen pointed me to a working solution. .GetNewClosure() called on the handler worked.
Working Code:
foreach ($script in $scripts){
$btn = New-Object System.Windows.Forms.Button
#Text, Location, Size omitted
#Add clickhandler
$btn.Add_Click(
{
Write-Host "Clicked on Btn $script"
Start-Process -FilePath "powershell" -ArgumentList "-command", "`"$script`""
Write-Host "finished"
$Form.Close();
}.GetNewClosure()
)
$Form.Controls.Add($btn)
}

Solution
Mathias R. Jessen pointed me to a working solution. .GetNewClosure() called on the handler worked.
Working Code:
foreach ($script in $scripts){
$btn = New-Object System.Windows.Forms.Button
#Text, Location, Size omitted
#Add clickhandler
$btn.Add_Click(
{
Write-Host "Clicked on Btn $script"
Start-Process -FilePath "powershell" -ArgumentList "-command", "`"$script`""
Write-Host "finished"
$Form.Close();
}.GetNewClosure()
)
$Form.Controls.Add($btn)
}

Related

Start-Process and powershell.exe with splatting

I've been trying for a couple of days now to multi-thread a WPF GUI which will run a PS3.0 script once the button has been clicked. I cannot use start-job as that I would have to track (multiple sessions at once), however, I would like to just run the script in a separate process of PS- as if I were to open multiple instances of the script from a shortcut. And be able to just have an open PS window which will track the progress within the script itself.
Expected results would be starting a script in powershell.exe session and passing 3 arguments - 2 strings and 1 boolean value. Which are provided by the user.
So in ISE:
C:\temp\test.ps1 -argumentlist $computername $username $citrixtest
Works fine.
I've spent a few hours scouring through the internet only to find a thread where a start-job was recommended or a way to use a background worker- this is not what I want from the script.
So I would guess the invocation from a button click would be something of the like (some of the things I have tried)
$ComputerName = "testtext1"
$UserName = "testtext2"
$CitrixTest = $True
$command = "c:\temp\test.ps1"
$arg = #{
Computername = "$computername";
Username = "$username";
CitrixTest = "$citrixtest"
}
#$WPFStartButton.Add_Click({
Start-Process powershell -ArgumentList "-noexit -command & {$command} -argumentlist $arg"
#})
Does not pass arguments to test.ps1- it is, however, getting to the "pause" - so the script successfully launches.
Where test.ps1 is
$ComputerName
$UserName
$CitrixTest
pause
Caller:
function Caller {
Param (
$ScriptPath = "c:\temp\test.ps1"
)
$Arguments = #()
$Arguments += "-computername $ComputerName"
$Arguments += "-UserName $UserName"
$Arguments += "-citrixtest $citrixtest"
$StartParams = #{
ArgumentList = "-File ""$ScriptPath""" + $Arguments
}
Start-Process powershell #StartParams
}
Caller
Does not start the script altogether- PS window just closes- possibly a path to .ps1 script not being found.
And a different approach which also nets in the script starts but not passing the arguments
$scriptFile = '"C:\temp\test.ps1"'
[string[]]$argumentList = "-file"
$argumentList += $scriptFile
$argumentlist += $computername
$argumentlist += $UserName
$argumentlist += $CitrixTest
$start_Process_info = New-Object System.Diagnostics.ProcessStartInfo
$start_Process_info.FileName = "$PSHOME\PowerShell.exe"
$start_Process_info.Arguments = $argumentList
$newProcess = New-Object System.Diagnostics.Process
$newProcess.StartInfo = $start_Process_info
$newProcess.Start() | Out-Null
Is there a way to make this work as I want it to? Or should I just dig deeper into runspaces and try with that?
#Bill_Stewart I just realized I did not put the param(args) in my script...
And that's why it would not pull those variables as I would like them to. I will have to check when I'm back in the office if it's just that what I was missing.
Checked on my laptop that's running PS 5.1 and this seems to be working as intended
$testarg = #(
'-File'
"C:\temp\test.ps1"
"$computername"
"$username"
"$citrixtest"
)
Start-Process powershell.exe -ArgumentList $testarg
Where test.ps1 is:
param(
$ComputerName,
$UserName,
$citrixtest
)
$ComputerName
$UserName
$CitrixTest
pause

Prevent popup from pausing powershell script

I know that by design the popup pauses the script until the user presses OK or closes it.
However, i'm trying to display something to a user in this popup, for example WARNING - STRING XYZ WAS DETECTED, while the script still continues.
Is it possible to prevent that popup from pausing the script?
$test = (Get-Process -Name Win*).ProcessName
$message_popup = (New-Object -COM Wscript.Shell).Popup(($test -join "`r`n"), 0, "Title", "48")
write-host "rest of script..."
It turns out what I needed was the JOB cmdlet, this works perfectly. Just make sure to pass your desired variable in the -ArgumentList and use it as $args in the job scriptblock!
$test = (Get-Process -Name Win*).ProcessName
Start-Job -ArgumentList $test -ScriptBlock {
(New-Object -COM Wscript.Shell).Popup(($args[0] -join "`r`n"), 0, "Title", "48")
} | Out-Null
write-host "rest of script..."

Waiting for copy process to finish

Is there any way to wait for the copy process to finish before running another command?
I tried Start-job and Wait-Job, but it doesn't work.
$func = {
function move-tozip
{
param([string]$filedest)
$Shell = New-Object -com Shell.Application
$b = $shell.namespace($zippath.ToString())
$b.CopyHere($filedest.tostring())
#Remove-Item -Path $filedest
}
}
start-job -InitializationScript $func -ScriptBlock {move-tozip $args[0]} -ArgumentList $file
The easiest way to wait for a job to complete is to give it a name and tell Wait-Job to wait on the task with that name, your script will wait for the job with the name WaitForMe to complete and then run the rest of your code once it has.
Using the -Name paramter with your code below:
$func =
{
function Move-ToZip
{
Param([string[]]$path, [string]$zipfile)
if (-not $zipfile.EndsWith('.zip')) {$zipfile += '.zip'}
if (-not (test-path $zipfile))
{
set-content $zipfile ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
}
$shell = (new-object -com shell.application).NameSpace($zipfile)
foreach($file in $path)
{
$shell.CopyHere($file)
start-sleep -milliseconds 100
}
}
}
Start-Job -Name "WaitForMe" -InitializationScript $func -ScriptBlock {Move-ToZip -path $args[0] -zipfile $args[1]} -ArgumentList "D:\data.log", "D:\datazip.zip"
Write-Host "Waiting for job to complete"
Wait-Job -Name "WaitForMe"
Write-Host "Job has completed :D"
To zip one file or folder
-ArgumentList "D:\testfile.log", "D:\datazip.zip"
To zip multiple files or folders
-ArgumentList #("D:\testfile.log","D:\testFolder1"), "D:\testzip.zip"
EDIT 17/12/2015
I've adapted code from This MSDN blog to the Move-ToZip function as the previous code didnt work for me at all, i've tested the above code successfully on files and folders. I have not tested the performance of this method, if you wish to compress/zip multiple large files/folders i would highly suggest looking into using a known working library or third party utility like 7zip.

Is there a way to pass serializable objects to a PowerShell script with start-process?

I know about PowerShell jobs, but I want to use start-process and pass a serializable object to the new powershell process. Is there any way to do this?
It seems that using start-process you have to provide a string argument list which won't cut it for me. I'm trying to get a PSCredential from one process to another (or a SecureString, I'll take either one). Maybe this circumvents security.
UPDATE - adding the solution I used after seeing help from others (using solution from #PetSerAl)
I wrote two test scripts: a parent script and a child script. The parent script calls the child script.
Parent Script:
$securePassword = ConvertTo-SecureString "testpassword" -AsPlainText -Force
$cred = New-Object PSCredential("testuser", $securePassword)
$credSerial = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([Management.Automation.PSSerializer]::Serialize($cred)))
$psFile = "C:\repos\Test\PowerShell Scripts\KirkTestChild.ps1"
$p1 = "-someparam ""this is a test!!!"""
$p2 = "-cred ""$credSerial"""
$proc = Start-Process PowerShell.exe -PassThru:$true -Argument "-File ""$($psFile)""", $p1, $p2
Write-Host "ID" $proc.Id
Write-Host "Has Exited" $proc.HasExited
Start-Sleep -Seconds 15
Write-Host "Has Exited" $proc.HasExited
Child Script:
Param(
$someParam,
$cred
)
Write-Host "someParam: $($someParam)"
Write-Host "cred (raw): $($cred)"
$realCred=[Management.Automation.PSSerializer]::Deserialize([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($cred)))
Write-Host "cred user: $($realCred.UserName)"
Write-Host "start"
Start-Sleep 5
Write-Host "ending"
Start-Sleep 5
Yes. As PetSerAl wrote in a comment, you can use the PSSerializer class to handle that:
$ser = [System.Management.Automation.PSSerializer]::Serialize($credential)
$cred = [System.Management.Automation.PSSerializer]::Deserialize($ser)
I don't know if your process will understand the CliXML format though; you don't specify what process you're starting.
Since this will produce XML which is multi-line, it may not work to pass this to a process as a parameter which is why PetSerAl is including the code to Base64 encode the resulting XML.
You might also be able to pass the XML over STDIN instead of Base64 encoding, which also ensures that you won't accidentally hit the command line size limit.
You can pass a scriptblock as an argument from a parent PS process to a child PS process. The result of the child process executing the script is serialized and returned to the parent. (Run powershell -? to get help on this feature). So if the direction you want to move the credential is from child to parent then this is an alternative, or if you want to move data in both directions, then you could combine the previous answers with this approach. (The OP doesn't say which direction).
EDIT
I'm assuming OP wants to start another Powershell process - because he said "new Powershell process". And he wants to pass a PSCredential from either the parent to the child or vice versa. I'd guess the former. Based on PetSerAl's solution he could serialize the cred as CliXML. It would have new lines. Besides potentially causing problems with passing the arguments to Powershell, the newlines will cause the Deserialize to fail if they are in the middle of a value. So to avoid those problems the whole script can be base64 encoded and passed via the -EncodedCommand parameter. It'll look something like this:
$xmlStr = [management.automation.psserializer]::Serialize($cred)
$scriptStr = "$liveCred = " +
"[management.automation.psserializer]::Deserialize('$xmlstr');" +
"# PS statements to do something with $liveCred"
$inB64 = [Convert]::ToBase64String( [Text.Encoding]::UTF8.GetBytes($scriptStr))
powershell -EncodedCommand $inB64
If OP needs something back from the script that ran in the child PS, and wanted to use the scriptblock feature mentioned earlier in this answer, I don't he could use this approach, because that feature is related to the -Command parameter. Rather newlines would need to be removed and escaping concerns might come into play.
Here's a variation that uses a ScriptBlock instead of script files. See the post by Greg Bray at Powershell Start-Process to start Powershell session and pass local variables.
$securePassword = ConvertTo-SecureString 'testpassword' -AsPlainText -Force
$cred = New-Object PSCredential( 'testuser', $securePassword )
$scriptBlockOuter = {
$sb = {
Param(
[Parameter( Mandatory, Position = 0 )]
[String] $someParam,
[Parameter( Mandatory, Position = 1 )]
[String] $credSerial
)
$cred = [System.Management.Automation.PSSerializer]::Deserialize( [System.Text.Encoding]::UTF8.GetString( [System.Convert]::FromBase64String( $credSerial )))
Write-Host "someParam: $someParam"
Write-Host "cred user: $($cred.UserName)"
Write-Host 'start'
Start-Sleep 5
Write-Host 'ending'
Start-Sleep 5
}
}
$p1 = 'this is a test!!!'
$credSerial = [System.Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes( [System.Management.Automation.PSSerializer]::Serialize( $cred )))
$proc = Start-Process PowerShell -PassThru -ArgumentList '-Command', $scriptBlockOuter, '& $sb', '-someParam', "'$p1'", '-credSerial', "'$credSerial'"
Write-Host 'ID' $proc.Id
Write-Host 'Has Exited' $proc.HasExited
Start-Sleep -Seconds 15
Write-Host 'Has Exited' $proc.HasExited
Using this ConvertTo-Expression cmdlet:
Parent script:
$Expression = $Cred | ConvertTo-Expression
$credSerial = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Expression))
Child script:
$Expression = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($credSerial))
$Cred = Invoke-Expression $Expression # or safer: &([ScriptBlock]::Create($Expression))

Run parameters in a Powershell script from a batch file

I'm trying to specify parameters in a powershell script, from a batch file.
The script itself looks likes this:
Param(
[Parameter(Mandatory=$true,HelpMessage="Application switch")]
[string[]]$Apps,
[ValidateRange(3,9999)]
[int]$SwitchDelay = 30
$AppPids = #()
$Windows = Get-Process | ? { $_.MainWindowTitle -ne "" }
$Wsh = New-Object -COM Wscript.Shell
foreach ($App in $Apps) {
foreach ($Window in $Windows) {
if ($Window.MainWindowTitle -like $App) {
Write-Verbose "Vindusfilter ""$App"" found hit on ""$($Window.MainWindowTitle)"" med PID ""$($Window.Id)"""
$AppPids += $Window.Id
}
}
}
do {
foreach ($ID in $AppPIDS) {
# Hides text...
$Wsh.AppActivate($ID) | Out-Null
Start-Sleep -Seconds $SwitchDelay
Write-Verbose "Changed window to PID ""$ID"""
}
} while ($true)
And what I'm trying to do is to define in a batch file is something like:
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "C:\AppRotate.ps1" -Apps "*Chrome*", "Spotify*" -Switchdelay 5
pause
(Supposed to show error message here, need more reputation first...)
Error: "... PositionalParameterNotFound.Approtate.ps1"
I'm basically new to scripting, so any ideas?
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "C:\AppRotate.ps1" -Apps "*Chrome*","Spotify*" -Switchdelay 5
The problem was the space between the first, and second variable of parameter -Apps.
Should work now.