How to use a variable ForegroundColor with write-host onto Start-job - multithreading

I'm stuck into a little problem that I don't understand, here is a simplified version of my code :
[hashtable] $result_color = #{
OPENED = [ConsoleColor]::green;
FERME = [ConsoleColor]::red
}
$job = Start-Job -Name "test" -Scriptblock {
Write-Host "Test text" -ForegroundColor $result_color.OPENED
} -ArgumentList $result_color
Wait-Job $job | Out-Null
Receive-Job $job
I get the following error :
Unable to bind "ForegroundColor" parameter to target. Exception when defining "ForegroundColor": "Unable to convert Null value to "System.ConsoleColor" type due to invalid enumeration values."
I also tried this :
[hashtable] $result_color= #{
OPENED = [System.ConsoleColor] "green";
CLOSED = [System.ConsoleColor] "red"
}
$job = Start-Job -Name "test" -Scriptblock {
Write-Host "Test text" -ForegroundColor $result_color.OPENED
} -ArgumentList $result_color
Wait-Job $job | Out-Null
Receive-Job $job
But same error... Thanks in advance for your help !

Either do what Mathias commented, OR add a param block into your scriptblock and use the -ArgumentList parameter to feed it the correct value like
$job = Start-Job -Name "test" -Scriptblock {
param($color)
Write-Host "Test text" -ForegroundColor $color
} -ArgumentList $result_color.OPENED

Related

Azure with PowerShell: empty string for Identity

I have a PowerShell script that logs into Azure and assign a group call membership to a user based on the input email address the user gave:
Function SetUser
{
$script:user = Read-Host -Prompt "Enter the user's email address"
Write-Host "You set the email address as '$user'." -ForegroundColor yellow
}
Function Assigndelegate($user)
{
$job = Start-Job -ScriptBlock {
param($user)
New-CsUserCallingDelegate -Identity $identity -Delegate $user -MakeCalls $true -ReceiveCalls $true -ManageSettings $false
} -ArgumentList $user
while ($job.State -eq 'Running') {
Write-Host "Connecting to Azure..."
Wait-Job -Job $job
}
$result = Receive-Job $job
(Get-CsUserCallingSettings -Identity $user).Delegators | ft -Property Id, MakeCalls, ReceiveCalls, ManageSettings
}
Connect-MicrosoftTeams
$script:identity = "user#contoso.com"
SetUser
Assigndelegate $user
This gives me the following error:
Cannot bind argument to parameter 'Identity' because it is an empty string.
+ CategoryInfo : InvalidData: (:) [New-CsUserCallingDelegate], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,New-CsUserCallingDelegate
+ PSComputerName : localhost
What I understand is that it cannot read the variable $identity, but I don't see the reason why: I tried also with the "$global:" prefix to see it works, even though that would not be ideal as this changes later, got the same error. If I directly add the value of this variable, it works.
If I define Identity in the same name in the 'param' and 'ArgumentList' the error changes to:
Cannot bind argument to parameter 'Identity' because it is an empty string.
+ CategoryInfo : InvalidData: (:) [New-CsUserCallingDelegate], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,New-CsUserCallingDelegate
+ PSComputerName : localhost
Get-CsUserCallingSettings : Cannot process argument transformation on parameter 'Identity'. Cannot convert value to type System.String.
At line:36 char:42
+ (Get-CsUserCallingSettings -Identity $user).Delegators | ft -Prop ...
+ ~~~~~
+ CategoryInfo : InvalidData: (:) [Get-CsUserCallingSettings], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Get-CsUserCallingSettings
UPDATE:
After Updating the argument list, I still get the error:
#Set the user
Function SetUser
{
$script:user = Read-Host -Prompt "Enter the user's email address"
Write-Host "You set the email address as '$user'." -ForegroundColor yellow
}
Function Assigndelegate($user, $identity)
{
$job = Start-Job -ScriptBlock {
param($user, $identity)
New-CsUserCallingDelegate -Identity $identity -Delegate $user -MakeCalls $true -ReceiveCalls $true -ManageSettings $false
} -ArgumentList #($user, $identity)
while ($job.State -eq 'Running') {
Write-Host "Connecting to Azure..."
Wait-Job -Job $job
}
$result = Receive-Job $job
#(Get-CsUserCallingSettings -Identity $user).Delegators | ft -Property Id, MakeCalls, ReceiveCalls, ManageSettings
}
Connect-MicrosoftTeams
$script:identity = "user#contoso.com"
SetUser
Assigndelegate $user, $identity
UPDATE II
Based on the idea recommended I simplified the script:
Function SetUser
{
$script:user = Read-Host -Prompt "Enter the user's email address"
Write-Host "You set the email address as '$user'." -ForegroundColor yellow
}
Function Assigndelegate($user, $identity)
{
Write-Output $identity
}
$script:identity = "user#contoso.com"
SetUser
Assigndelegate $user, $identity
It gives no results apart from:
"You set the email address as '$user'." -ForegroundColor yellow
The Start-Job cmdlet starts a process in a separate session, so none of the scopes (local, script or global) are visible to the script block it executes. The only way around this is to add identity as a parameter in your script block and pass it explicitly, i.e:
Function SetUser
{
$script:user = Read-Host -Prompt "Enter the user's email address"
Write-Host "You set the email address as '$user'." -ForegroundColor yellow
}
Function Assigndelegate($user, $identity)
{
$job = Start-Job -ScriptBlock {
param($user, $identity)
New-CsUserCallingDelegate -Identity $identity -Delegate $user -MakeCalls $true -ReceiveCalls $true -ManageSettings $false
} -ArgumentList #($user, $identity)
while ($job.State -eq 'Running') {
Write-Host "Connecting to Azure..."
Wait-Job -Job $job
}
$result = Receive-Job $job
(Get-CsUserCallingSettings -Identity $user).Delegators | ft -Property Id, MakeCalls, ReceiveCalls, ManageSettings
}
Connect-MicrosoftTeams
$script:identity = "user#contoso.com"
SetUser
Assigndelegate -user $user -identity $identity

Need to run app on N remotes hosts and handle them all

Im using this code to run app on N remote hosts and need to wait until all of them are finished and get their results. But the execution just passes throuhg, all jobs are being marked Completed and code exits.
How to make it wait till apps finished their execution?
$procArray = [System.Collections.ArrayList]#()
foreach ($key in $simulatorServers.keys) {
$unitHost = $simulatorServers[$key][0]
$EXE="C:\app.exe"
Wr-DebugReg "Running $EXE on $unitHost "
$ScriptString="Start-Process -FilePath $EXE "
$ScriptBlock=[System.Management.Automation.ScriptBlock]::Create($ScriptString)
$Session=New-PSSession -ComputerName $unitHost -EnableNetworkAccess -Name "session$counter" -Credential $crNonProd
$rez2 = Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -AsJob
$rez00=$procArray.Add($rez2)
Wr-DebugReg "Running process id=$($rez2.id) name=$($proc.Name)on $unitHost"
Wr-DebugReg ""
}
$procArray | Wait-Job
$procArray | Receive-Job
these jobs gone to status Completed even if launched processes still running
let invoke-command handle the the amount of jobs/sessions to open in parallel - you will receive 1 job with childs:
$scriptBlock = {
start-process -FilePath 'C:\app.exe'
}
$sessions = #(
foreach ($key in $simulatorServers.keys) {
$unitHost = $simulatorServers[$key][0]
New-PSSession -ComputerName $unitHost -EnableNetworkAccess -Name "session$counter" -Credential $crNonProd
}
)
$job = Invoke-Command -Session $Sessions -ScriptBlock $ScriptBlock -AsJob
$result = receive-job -Wait -Job $job
btw. I do not see, based on this sample, what you want to receive. you want to execute "start-process -FilePath 'C:\app.exe' " on the target machines but this won't give you anything back.
to get the information back modify the scriptblock like this:
$scriptBlock = {
$result = start-process -FilePath 'C:\app.exe' -wait -PassThru
return $result
}
This code is working. -Wait is the key to make it wait until all jobs are finished.
$procArray = [System.Collections.ArrayList]#()
foreach ($key in $hosts.keys) {
$unitHost = $hosts[$key][0]
$EXE="c:\app.exe"
$Session=New-PSSession -ComputerName $unitHost -EnableNetworkAccess -Name "session$counter" -Credential $crNonProd
$rez2 = Invoke-Command -Session $Session -ScriptBlock {Start-Process -FilePath $args[0] -Wait} -ArgumentList $EXE -AsJob
$rez00=$procArray.Add($rez2)
Wr-DebugReg "Starting process id=$($rez2.id) name=$($proc.Name)on $unitHost"
Wr-DebugReg ""
}
while ($procArray.State -contains 'Running')
{
Start-Sleep -Seconds 1
}

Issues having a retry count work in powershell

I have a Powershell script that checks on a set of VM status before starting them. If the VM'S are in deallocating mode there should be a sleep and retry on 30 seconds. The code does not do a a retry. The code does a vm start in batches on 2 for vm's with wilcards as mentioned below in an order.
Need help if possible
$ResName= "resvmtest"
$action="start"
if($action -eq "start"){
$vnames=#('*dom*','*DBs*','*')
foreach($vname in $vnames) {
Write-Host "Starting VM with "$vname
$vmList = Get-AzVM -ResourceGroupName $ResName -Name $vname -Status | Select-Object Name, PowerState, ResourceGroupName
do{
$batch = #{
Skip = 0
First = 2
}
do{
foreach($vm in ($vmList | Select-Object #batch)){
$Stoploop = $false
[int]$Retrycount = "0"
do {
try {
if($vm.PowerState -eq "VM Deallocated"){
Write-Host "Job completed"
$Stoploop = $true
}
}
catch {
if ($vm.PowerState -eq "VM Deallocatting") {
Write-Host "VM Still not Deallocated"
Start-Sleep -Seconds 10
$Retrycount = $Retrycount + 1
}
}
}
While ($Stoploop -eq $false)
$params = #($vm.Name, $vm.ResourceGroupName,$vm.PowerState)
$job = Start-Job -ScriptBlock {
param($ComputerName,$serviceName,$statuses)
Start-AzVM -Name $ComputerName -ResourceGroupName $serviceName
} -ArgumentList $params
}
Wait-Job -Job $job
Get-Job | Receive-Job
$batch.Skip += 2
}
until($batch.skip -ge $vmList.count)
}
while($job.state -ne "Completed")
}
}
If you just want to wait for a VM's PowerState turns to VM Deallocated from VM Deallocatting, try the code below:
$vms = Get-AzVm -Status| Select-Object Name, PowerState,ResourceGroupName
#pick certain vm for demo here.
$vm = $vms[1]
$retryCount = 0
while($vm.PowerState -eq 'VM deallocating'){
Write-Host "waiting for VM deallocated..."
Start-Sleep -Seconds 5
$retryCount +=1
Write-Host "check count:$retryCount"
#get latest vm status
$vm.PowerState = (Get-AzVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName -Status).Statuses[1].DisplayStatus
Write-Host "vm current state:"$vm.PowerState
}
Write-Host "vm new state:" + $vm.PowerState
Result:

PowerShell Mutex & Jobs

I'm trying to run a PowerShell file delete operation that deletes files based on specific parameters. So, the code goes like this:
$sb = {
Get-ChildItem -Path $SubFolder | Remove-ChildItem
if ($Error[0] -eq $null) {
$results = New-Object PSObject -Property #{
$Foldername = $Subfolder.Name
$TotalFiles = $SubFolder.(Count)
}
} else {
$errorresult = "Error deleting files from $($Subfolder.Name)"
}
#Error Mutex
$ErrorLogmutex = New-Object System.Threading.Mutex($false, "ErrorLogFileHandler")
$ErrorLogmutex.WaitOne()
Out-File -Append -InputObject $errorresult -FilePath $Errorlog
$ErrorLogmutex.ReleaseMutex()
#Success Mutex
$Successmutex = New-Object System.Threading.Mutex($false, "SuccessFileHandler")
$SuccessLogmutex.WaitOne()
Out-File -Append -InputObject $results -FilePath $successlog
$Successmutex.ReleaseMutex()
}
#Calling scriptblock in multi-thread count of 5
{
foreach ($Subfolder in $Folder) {
while ((Get-Job -State Running).Count -ge 5) {
Start-Sleep -Seconds 5
}
Start-Job -ScriptBlock $sb -ArgumentList $arguments | Out-Null
}
The script runs, able to see the results if I explicitly call out Receive-Job -Name JobID, but the output does not produce any log files as I would've thought it would.

Wait until all threads complete before running next task

I would wrap everything inside foreach($computer in $computers) in a Start-Job to make them run simultaneously. The only problem is, I need to wait for all the jobs to complete before I do the ConvertTo-Json at the bottom.
$sb = "OU=some,OU=ou,DC=some,DC=domain"
$computers = Get-ADComputer -Filter {(Enabled -eq $true)} -SearchBase "$sb" -Properties *
$hasmanufacturer = New-Object System.Collections.Generic.List[System.Object]
foreach($computer in $computers)
{
$drives = try{#(Get-WMIObject -Class Win32_CDROMDrive -Property * -ComputerName $computer.Name -ErrorAction Stop)} catch {$null}
foreach($drive in $drives)
{
if($drive.Manufacturer)
{
$hasmanufacturer.Add($computer)
continue
}
} # inner foreach
}
ConvertTo-Json $hasmanufacturer
Use a Get-Job | Wait-Job before executing the ConvertTo-Json
How about using the array of computer names as a parameter to Invoke-Command. It will run, by default, 32 concurrent remote sessions. The number can be changed with the -Throttle parameter.
$computers = Get-ADComputer -Filter {(Enabled -eq $true)} -SearchBase "OU=Servers,DC=xxx,DC=com" -Properties Name |
Where-Object { $_.Name -match 'LAX_*' } |
ForEach-Object { $_.Name }
$computers
$j = Invoke-Command `
-ComputerName $computers `
-ScriptBlock { Get-WMIObject -Class Win32_CDROMDrive -Property * -ErrorAction Stop } `
-AsJob
while ( (Get-Job -Id $j.Id).Status -eq 'Running') {}
Get-Job -Id $j.Id | Wait-Job
$results = Receive-Job -Id $j.Id
$results

Resources