Powershell build step, fire and forget? - powershell

I am running the following powershell command in a build step using TFS 2018.
Start-Job -ScriptBlock {
Invoke-Command -FilePath \\MyServer\run.ps1 -ComputerName MyServer -ArgumentList arg1, arg2
}
Since I don't want the script to affect the build step it should simply fire and forget the script. Hence I am using Start-Job. But it seems that once the step is done the process is killed. Is there a way to maintain the process lifetime even though the build step is done or the build process is finished?
Additional information... the powershell script should run on the remote server. The script itself triggers an .exe with parameters.

To simply fire and forget, invoke the script with Invoke-Command -AsJob:
Invoke-Command -AsJob -FilePath \\MyServer\run.ps1 -ComputerName MyServer -Args arg1, arg2
Start-Sleep 1 # !! Seemingly, this is necessary, as #doorman has discovered.
This should kick off the script remotely, asynchronously, with a job getting created in the local session to monitor its execution.
Caveat: The use of Start-Sleep - possibly with a longer wait time -
is seemingly necessary in order for the remote process to be created before the calling script exits, but such a solution may not be fully robust, as there is no guaranteed timing.
Since you're not planning to monitor the remote execution, the local session terminating - and along with it the monitoring job - should't matter.

When do you want the script to stop running? You could use a do-while loop and come up with a <condition> that meets your needs.
Start-Job -ScriptBlock {
do{
Invoke-Command -FilePath \\MyServer\run.ps1 -ComputerName MyServer -ArgumentList arg1, arg2
Start-Sleep 2
}while(<condition>)
}
Alternatively, you could use the condition $true so it executes forever. You will have to stop the job later in the script when you no longer need it.
$job = Start-Job -ScriptBlock {
do{
Invoke-Command -FilePath \\MyServer\run.ps1 -ComputerName MyServer -ArgumentList arg1, arg2
Start-Sleep 2
}while($true)
}
Stop-Job $job
Remove-Job $job
I've added a Start-Sleep 2 so it doesn't lock up your CPU as no idea what the script is doing - remove if not required.

Why not something like this:
Invoke-Command -Filepath \\MyServer\Run.ps1 -Computername MyServer -Argumentlist Arg1,Arg2 -AsJob
$JobCount = (get-job).Count
Do
{
Start-Sleep -Seconds 1
$totalJobCompleted = (get-job | Where-Object {$_.state -eq "Completed"} | Where-Object {$_.Command -like "NAMEOFCOMMAND*"}).count
}
Until($totalJobCompleted -ge $JobCount)

#doorman -
PowerShell is natively a single threaded application. In almost all cases, this is a huge benefit. Even forcing multiple threads, you can see the child threads are always dependent on the main thread. If this wasn't the case, it would be very easy to create memory leaks. This is almost always a good thing as when you close the main thread, .Net will clean up all the other threads you may have forgotten about for you. You just happened to run across a case where this behaviour is not beneficial to your situation.
There are a few ways to tackle the issue, but the easiest is probably to use the good ol' command prompt to launch an independent new instance not based at all on your original script. To do this, you can use invoke-expression in conjunction with 'cmd /c'. See Below:
invoke-expression 'cmd /c start powershell -NoProfile -windowstyle hidden -Command {
$i = 0
while ($true) {
if($i -gt 30) {
break
}
else {
$i | Out-File C:\Temp\IndependentSessionTest.txt -Append
Start-Sleep -Seconds 1
$i++
}
}
}
'
This will start a new session, run the script you want, not show a window and not use your powershell profile when the script gets run. You will be able to see that even if you kill the original PowerShell session, this one will keep running. You can verify this by looking at the IndependentSessionTest.txt file after you close the main powershell window and see that the file keeps getting updated numbers.
Hopefully this points you in the right direction.
Here's some source links:
PowerShell launch script in new instance
How to run a PowerShell script without displaying a window?

Related

Trying to run a chkdsk on c:\ automatically

Trying to run a chkdsk on all drives of the computer. Having issues with having C:\ start at all.
Trying to use SendKeys to answer "Would you like to schedule this volume to be checked the next time the system restarts? (Y/N)" but having no luck. Where am I going wrong?
$host.ui.RawUI.WindowTitle = "BOHCdrivefix"
$FixCDrive = Start-Job -Name cDriveFix -ScriptBlock {chkdsk c:/f}
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('BOHCdrivefix')
sleep 3
$wshell.SendKeys('y')
$wshell.SendKeys('~')
wait-job $FixCDrive
Receive-Job $FixCDrive | Out-File -FilePath D:\temp\cDriveFix.txt
shutdown -r -f -t 0
I would like to answer Y to the question then shutdown the PC and have it start the chkdsk
From Start-Job document:
The Start-Job cmdlet starts a Windows PowerShell background job on
the local computer.
A Windows PowerShell background job runs a command without
interacting with the current session.
Hence, you can't send any key from the current session to a command running inside a background job. Sending a key must be inside the background job. Fortunately, chkdsk.exe accepts use pipeline operator (which sends the results of the preceding command to the next command) as follows:
$FixCDrive = Start-Job -Name cDriveFix -ScriptBlock {Write-Output 'y'|chkdsk.exe c:/f}
or (using echo alias for Write-Output cmdlet):
$FixCDrive = Start-Job -Name cDriveFix -ScriptBlock {echo 'y'|chkdsk.exe c:/f}
Please note:
The type of the file system is NTFS.
Cannot lock current drive.
Chkdsk cannot run because the volume is in use by another
process. Would you like to schedule this volume to be
checked the next time the system restarts? (Y/N)
To answer "yes" to above question asked by chkdsk c:/f (fix file system errors on the boot partition), you must press Y, followed by Enter.
Honestly said, I'm not sure whether Write-Output cmdlet sends Enter into the pipeline. If not, force output of the new line as follows:
Write-Output "y$([System.Environment]::NewLine)"|chkdsk.exe c:/f}

Run Scriptblock in It's Own Instance and Return ExitCode to Parent Script in PowerShell

I am attempting to have a PowerShell script as a node inside of an XML file that either returns an exit code of 1 or 0. I would then like to run this script in an instance separate from the parent PS script but return its exit code back to the parent instance so that I can write an If statement based on the ExitCode.
Right now I made the XML PowerShell script simple (this seems to be working fine without any issues):
exit 1
Here's my code in the parent PS script:
#write XML script to string then convert string to scriptblock
[String]$installCheck_ScriptString = $package.installcheck_script
$installCheck_Script = [Scriptblock]::Create($installCheck_ScriptString)
#start new instance of powershell and run script from XML
$process = (Start-Process powershell.exe -ArgumentList "-command {$installCheck_Script} -PassThru -Wait")
$installCheck_ScriptResult = $process.ExitCode
If ($installCheck_ScriptResult -gt 0)
{
....
}
In playing around with the code, I seem to either get a message that Wait or Passthru are unexpected tokens or I don't get any ExitCode value. $LastExitCode always returns a 0.
-Wait and -PassThru are not valid parameters for powershell.exe. Did you mean to apply them to Start-Process like this?
$process = (Start-Process powershell.exe -ArgumentList "-command {$installCheck_Script}" -PassThru -Wait)
Note that you're going to have some issues with this approach. If $installCheck_Script contains any characters that need to be escaped you're going to be doing a lot of checks and replaces.
You could avoid that by using -EncodedCommand with powershell.exe and passing in the base64 encoded version of the script:
$encodedScript = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($installCheck_Script))
$process = (Start-Process powershell.exe -ArgumentList "-EncodedCommand",$encodedScript -PassThru -Wait)
But only do this if you're insistent on calling via shell.
A better (?) way:
As an alternative to what you're doing (shelling out), you might consider creating a job, and then instead of using an exit code, use an actual return value:
$installCheck_Script = " 1 " # for example
$sb = [ScriptBlock]::Create($installCheck_Script)
$job = Start-Job -ScriptBlock $sb
$job | Wait-Job
$code = $job | Receive-Job
If you wanted better performance you could do it in process with runspaces. This is made easier with the PoshRSJob Module which lets you use runspaces in a similar way to using jobs.

How can I start a background job in Powershell that outlives it's parent?

Consider the following code:
start-job -scriptblock { sleep 10; cmd /c set > c:\env.txt; }
exit
The background job is killed when the parent exits, so the call to cmd.exe never occurs. I would like to write some similar code, such that the parent exits immediately, and the child continues to run in the background.
I would like to keep everything in one script if possible.
You will have to start a process:
start-process powershell -ArgumentList "sleep 10; cmd /c set > c:\env.txt" -WindowStyle hidden
If you use Start-Process, you create a new child process that has your powershell session as its parent process. If you kill the powershell parent process that starts this, the new process will be orphaned and keep running. It will, however, not survive if you kill the parent's process tree
Start-Process -FilePath notepad.exe
Powershell cannot start a new independent process outside of its process tree for you. However, this can be done in Windows using CreateProcess and this functionality is exposed through WMI. Luckily, you can call that from powershell:
Invoke-WmiMethod -Class Win32_Process -Name Create -ArgumentList notepad.exe
This way, the new process will also keep running if the process tree is killed, because the new process does not have your powershell session as a parent, but the WMI host process.
If found that using Invoke-Command was able to bypass the restriction that the background job dies with it's parent. The nice thing is that the syntax is almost the same as with start-job, so the scriptblock can be kept as is.
start-job -scriptblock { sleep 10; cmd /c set > c:\env.txt; }
would just turn into
Invoke-Command -ComputerName . -AsJob -scriptblock { sleep 10; cmd /c set > c:\env.txt; }
and suddenly survive the death of it's parent (probably because invoke-command is fit to run programs on another computer so the parent can never matter to it)
You can also do it like that
$a = start-job -scriptblock { sleep 10; cmd /c set > c:\env.txt; }
Register-ObjectEvent -InputObject $a -EventName StateChanged -SourceIdentifier "finished"
$b = Wait-Event -SourceIdentifier "finished"
exit
Your script will wait for the end of the scriptblock to finish.

Background job does not seem to run

I have function in a powershell 2.0 script that I am launching as a bkground job
Start-Job -ScriptBlock {CopyDataToServer($uploadSessionGuid)} -Name $uploadSessionGuid
Then at the end of the script I have
Wait-Job -State Running -Timeout $LogCopyTimeout
Event though the job is showing as Running and then completed, nothing is copied to the server.
How can I debug this?
Roman is right about CopyDataToServer and $uploadSessionGuid probably not being defined in the runspace the job executes in (upvoted his answer). BTW I believe it is better to wait on a specific job object than for any job in the running state e.g.:
$job = Start-Job {param($path, $guid) . $path\lib.ps1; CopyDataToServer $guid} `
-arg $pwd,$uploadSessionGuid
Wait-Job $job
Receive-Job $job
Note that you can use the -ArgumentList parameter to pass in parameters to your scriptblock. While you can access these arguments in your scriptblock via $args, I prefer using a param block and naming the args. This example also shows how you can pass in the path to a PowerShell script containing the function CopyDataToServer which gets dot sourced into the job's runspace.
The script block {CopyDataToServer($uploadSessionGuid)} is invoked in a new runspace where the command CopyDataToServer or the variable $uploadSessionGuid might be not available. To check this instead of your job run this at first:
Start-Job -ScriptBlock {
Get-Command CopyDataToServer
Get-Variable uploadSessionGuid
}
Wait-Job -State Running
Get-Job | Receive-Job
If the job returns the command and the variable then the problem is elsewhere and debugging is not over. But if it fails or gets wrong results then this is the problem to be fixed (to make the command available and/or use a different way to supply the parameter).

Start-job vs. Invoke-command -asjob

I'm trying to do basic background jobs in PowerShell 2.0, and I'm seeing different things with start-job and invoke-command -asjob.
If I do this:
start-job -scriptblock {get-process}
I get a job object, but the child job (which is created automatically by start-job) always has a JobStateInfo of "NotStarted".
this, however, works as expected:
invoke-command -scriptblock {get-process} -computer localhost -asjob
I've run the enable-psremoting....anything else I need to do to get background jobs working?
The first example using start-job does not use HTTP for the call and instead uses an IPC channel with WinRM to run; it does not require administrative privileges this way. The second example with invoke-command does require admin rights (by default) and will connect via HTTP and WinRM.
To be honest, I would have expected the second one to fail for most people. If you run: Receive-Job against the ID of the start-job invocation, do you get any error messages?
-Oisin
To receive an updated JobStateInfo you'll need to use Get-Job and the job created by Start-Job. Though, if you're using this information to see when the job finishes, Wait-Job or Receive-Job -wait might be better suited to your needs.
Wait-Job simply waits until the job, or list of jobs, indicated is finished before moving on. Receive-Job -wait does the same thing, but it also gathers the results/output of the job.