Resolving Variables within Scriptblocks for Jobs - powershell

I have a job setup to run/call a script that outputs a file. The code works fine when run on its own. I have verified the job is not being blocked and completes successfully. However, no file is generated when the script is called from the job. I'm even doing something very similar elsewhere without an issue, in fact it is from another similar set up that I pulled this code to begin with. Anyway, here is what I have:
When run as job, no file output:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
} -Name McShieldCheckerJob
When run not as a job, file outputs as expected.
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile $McShieldCheckOutputFileName -GroupByMachine "N" -AsJob "Y"
Stumped as to why this won't work exactly the same as a job vs. not as a job. Again, I have verified via Get-Job and Receive-job that the job is not being blocked and is completing successfully.
EDIT:
It seems the variable $McShieldCheckOutputFileName cannot be resolved within the scriptblock for the job. If I use a string literal [for $McShieldCheckOutputFileName] I do not have the issue. (I was able to determine after a bit more troubleshooting that the lack of an output file when running as a job was due to a null value for my output file name.) So, I can probably work around this easily enough, but still this seems curious that a variable cannot be resolved within the scriptblock? Must be a scope thing...

Figured it out... here is how it is done:
$McShieldCheckerJob = Start-Job -ScriptBlock {
E:\ICSScoreCardUtilityPack\ServiceAndProcessCheckerV1.0.ps1 -ServicesAndOrProcesses 'McShield' -Comps 'COMP15' `
-OutputMode 1 -OutputFile "$($args[0])" -GroupByMachine "N"
} -Name McShieldCheckerJob -ArgumentList $McShieldCheckOutputFileName
As we can see, we need to specify an -Argumentlist to the script block, then reference said arguments within the scriptblock via the $args variable.
Reference (see example 10)

Related

[System.IO.Path]::GetTempPath() outputs local temp directory when called through Invoke-Command on a remote machine

I'm running PowerShell commands on a remote machine by the use of Invoke-Command -ComputerName. I'm trying to obtain the path of the temporary directory of the remote machine.
Depending on where I call [System.IO.Path]::GetTempPath() it either outputs the expected remote directory C:\Users\…\AppData\Local\Temp or my local temporary directory C:\temp.
This command is not working as expected:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
}
# Outputs local directory 'C:\temp'
# Expected remote directory 'C:\Users\…\AppData\Local\Temp'
The problem can be reproduced with other commands than Write-Output, e. g. Join-Path.
Contrary, the following code samples all give the expected output of C:\Users\…\AppData\Local\Temp.
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
[System.IO.Path]::GetTempPath()
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
$tmp = [System.IO.Path]::GetTempPath(); Write-Output $tmp
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Start-Sleep 1
Write-Output ([System.IO.Path]::GetTempPath())
}
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath())
Start-Sleep 1
}
Obviously Start-Sleep isn't a solution, but it seems to indicate some kind of timing problem.
Suspecting that the problem isn't limited to GetTempPath() I tried another user-related .NET API, which also unexpectedly outputs my local folder instead of the remote one:
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments))
}
How can I use [System.IO.Path]::GetTempPath() and other .NET API in a PowerShell remote session in a predictable way?
Santiago Squarzon has found the relevant bug report:
GitHub issue #14511
The issue equally affects Enter-PSSession.
While a decision was made to fix the problem, that fix hasn't yet been made as of PowerShell 7.3.1 - and given that the legacy PowerShell edition, Windows PowerShell (versions up to v5.1, the latest and final version) will see security-critical fixes only, the fix will likely never be implemented there.
While the linked bug report talks about the behavior originally having been by (questionable) design, the fact that it only surfaces in very narrow circumstances (see below) implies that at the very least that original design intent's implementation was faulty.
The problem seems to be specific to a script block with the following characteristics:
containing a single statement
that is a cmdlet call (possibly with additional pipeline segments)
whose arguments involve .NET method calls, which are then unexpectedly performed on the caller's side.
Workaround:
Make sure that your remotely executing script block contains more than one statement.
A simple way to add a no-op dummy statement is to use $null++:
# This makes [System.IO.Path]::GetTempPath() *locally* report
# 'C:\temp\'
# *Remotely*, the *original* value should be in effect, even when targeting the
# same machine (given that the env. var. modification is process-local).
$env:TMP = 'C:\temp'
Invoke-Command -ComputerName MyRemoteMachine -ScriptBlock {
Write-Output ([System.IO.Path]::GetTempPath()); $null++ # <- dummy statement.
}
Other workarounds are possible too, such as enclosing the cmdlet call in (...) or inserting a dummy variable assignment
(Write-Output ($unused = [System.IO.Path]::GetTempPath()))
Your Start-Sleep workaround happened to work because by definition it too added another statement; but what that statement is doesn't matter, and there's no timing component to the bug.

Stop a process running longer than an hour

I posted a question a couple ago, I needed a powershell script that would start a service if it was stopped, stop the process if running longer than an hour then start it again, and if running less than an hour do nothing. I was given a great script that really helped, but I'm trying to convert it to a "process". I have the following code (below) but am getting the following error
Error
"cmdlet Start-Process at command pipeline position 3
Supply values for the following parameters:
FilePath: "
Powershell
# for debugging
$PSDefaultParameterValues['*Process:Verbose'] = $true
$str = Get-Process -Name "Chrome"
if ($str.Status -eq 'stopped') {
$str | Start-Process
} elseif ($str.StartTime -lt (Get-Date).AddHours(-1)) {
$str | Stop-Process -PassThru | Start-Process
} else {
'Chrome is running and StartTime is within the past hour!'
}
# other logic goes here
Your $str is storing a list of all processes with the name "Chrome", so I imagine you want a single process. You'll need to specify an ID in Get-Process or use $str[0] to single out a specific process in the list.
When you store a single process in $str, if you try to print your $str.Status, you'll see that it would output nothing, because Status isn't a property of a process. A process is either running or it doesn't exist. That said, you may want to have your logic instead check if it can find the process and then start the process if it can't, in which case it needs the path to the executable to start the process. More info with examples can be found here: https://technet.microsoft.com/en-us/library/41a7e43c-9bb3-4dc2-8b0c-f6c32962e72c?f=255&MSPPError=-2147217396
If you're using Powershell ISE, try storing the process in a variable in the terminal, type the variable with a dot afterwards, and Intellisense (if it's on) should give a list of all its available properties.

Assign a variable inside of a scriptblock while running a job

Related to Terminate part of powershell script and continue.
Partially related to Powershell Job Always Shows Complete.
My script runs locally and access the registry hive of a remote PC. I need the value of registry keys to be written into a $RegHive variable. And I want to monitor it as a job in case some PC freezes, I can terminate the command and move on to another PC.
My original code would be:
$global:RegHive = $null
$job = Start-Job -ScriptBlock {
$RegHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("SomeKeyName", "SomePCName")
}
But no matter what I do, the variable $RegHive is empty.
If I do $RegHive = (Get-Job | Receive-Job) some value gets assigned to $RegHive that on one side looks exactly as if I would run it normally without a job/scriptblock, ie:
$RegHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("SomeKeyName", "SomePCName")
and even has the same $RegHive.SubKeyCount
But the "normal" one has $RegHive.GetSubKeyName() method and the one from job doesn't.
How do I escape assigning a variable with Receive-Job and do the assignment directly inside the scriptblock, which is run as a job?
In simple words:
$job = Start-Job -ScriptBlock {$a = 1 + 2}
How to get $a be equal to 3 without $a = (Get-job | Receive-job)?
This might be helpful for you. The job is sort of like a variable
What you can do is name the job and then call it by name with -Keep to maintain it's value stored - aka it will store all final output inside itself until you call it. (it can be kept but the default is to remove it once called)
$global:RegHive = $null
Start-Job -Name "RegHive" -ScriptBlock {
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("SomeKeyName", "SomePCName")
}
Receive-Job -Name "RegHive" -Keep
obviously calling the Receive-Job immediately after defeats the purpose of jobs, they add a lot of overhead, and are only efficient when needing to do multiple things at once. - if you call for 100s or thousands at once, you could do get-job | wait-job then when finished start using their outputs ---- wait-job also accepts job names or can wait on your entire list of jobs.
another option to set the variable is
$RegHive = "Receive-Job -Name "RegHive"
and finally, you can do this to use the value
get-<insert command> -value "$(Receive-Job -Name 'RegHive' -Keep)" -argument2 "YADA YADA"
remember keep will not delete the value and can be "Received" again later.

PowerShell error Method invocation failed

I have a strange problem where I have existing code that works on Windows 7x86 PowerShell 2.0 but will not work on Windows 10x86 using PowerShell 5.0.
The error states:
Method invocation failed because
[System.Management.Automation.PSRemotingJob] does not contain a method named 'op_Addition'.
At
C:\Build\AmazingCharts\Working\Building\Installer\WiseInstaller\Obfusated
Projects\ObfuscateFiles.ps1:211
char:13 + $Jarray +=
Start-Job -name ObfuscateFiles $ScriptBlock - ...
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation:
(op_Addition:String) [], Runti
meException +
FullyQualifiedErrorId : MethodNotFound
This is stating that the overloaded operator += doesn't seem to be defined but I find it very difficult to believe that the += overloaded addition operator would be deprecated in PowerShell 5.0 for arrays.
Further, the code seems to still execute and produce a correct output.
In my case I use a jobs array to execute Red Gate Smart Assembly to obfuscate build files. Below is a code snippet
$Jarray = #() #initialize jobs array
foreach ($file in $farray_) {
$params = $file,$cwd,$bldLog #parameters that are passed to script block
$nCount = (get-job -state "Running").Count #determines num of running jobs
if (-not $nCount) {$ncount=0} #initialize variable if null
if ($nCount -le $MAX_INSTANCES) {
#The line below is the one that generates the error
$Jarray += Start-Job -name ObfuscateFiles $ScriptBlock -ArgumentList $params
Start-Sleep -s 1 #gives the system time for process to start
} else {
$nCount = (get-job -state "Running").Count
if (-not $nCount) {$nCount=0}
while ($nCount -ge $MAX_INSTANCES) {
Start-Sleep -s 1 #gives time to recognize the job has started
$nCount = (get-job -state "Running").Count
if (-not $nCount) {$nCount=0}
}
$Jarray += Start-Job -name ObfuscateFiles $ScriptBlock -ArgumentList $params
}
}
When this execute it correctly generates all of the obfuscated files and in a timely manner so I know it is building in parallel up to the MAX_INSTANCES value. I even have code (not included above) that verifies that the number of expected obfuscated files is correct. The 'Method invocation failed' error reports back to CruiseControl.NET and my build fails.
Another frustration is that executing the same .ps1 file in PowerGUI completes successfully without any errors.
I have made sure that my array gets initialized. I have tried suggestions to specify that the array be of type [PSObject] but I get the same results.
Does anyone have any ideas what I can check for this?
Thank you.
EDIT:
I tested this in the PowerShell IDE
-- When tested running from a command prompt, I get the errors
-- When tested running the debugger, I do not get errors
I checked the $Jarray type and it is a System.Object[], which is what I would expect for an array.
I checked the $Jarray value during debug and the jobs are being created but the error still persists as if it doesn't understand the += overloaded Addition operator. My only guess is that this is a bogus error masking something else.
I added a try/catch block around the call to $Jarray. I had tried ErrorAction Ignore but that did not work because the call does not know what to do with the error. The try/catch block successfully masks the error; fortunately I have another method to determine if my call is successful. Needless to say this does not 'solve' the issue, it only masks the issue because right now it is not critical.
I will continue to update this area with things I have tested.
Just an idea, but might want to make sure your new machine has a relaxed policy for remoting AND enabling scripts:
Set-ExecutionPolicy Unrestricted &
Enable-PSRemoting

Powershell to shut down a VM

I have a small Powershell script that is used to shut down my virtual machines in event of an extended power outage. It takes a specific VM object and forces a shutdown.
Function DirtyShutdown
{ param([VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl]$VM )
$VM | Stop-VM -Confirm:$false
}
I would like to speed up this process using the start-job command to run all these tasks in parallel. I have tried using several variants including the following which I believe to be correct.
Start-Job -InputObject $VM -ScriptBlock{ $input | Shutdown-VMGuest -Confirm:$false }
Based on the Receive-Job output it appears the problem is the snap in in use (added before the above function is called) is not loaded in the context of Start-Job.
What is the correct syntax to make this happen?
While I appreciate the desire to use PowerShell v2's job subsystem for this task, note that vCenter has a built-in job system which you can take advantage of here. Most PowerCLI cmdlets which perform a change to your environment have a RunAsync parameter. To know which ones, run this piece of PowerShell code:
get-help * -parameter runasync
The RunAsync parameter will take your command(s) and queue them up in vCenter. The cmdlet will return a task object and then immediately return control back to your script.
To turn this into an answer in your case, simply append "-runasync" to the end of your Stop-VM command, like so:
$VM | Stop-VM -Confirm:$false -RunAsync
Each time you start a job, PowerShell creates a new runspace. This means a new environment that you may need to initialize, and that includes loading snap-ins and connecting to your VI Server. Start-Job has a parameter that you can use here called InitializationScript. Try something like this:
Start-Job -InitializationScript { Add-PSSnapin VMware.VimAutomation.Core } {
Connect-ViServer myserver
Get-VM foo | Stop-VM
}