Windows task scheduler runs 'at system startup' tasks differently from other tasks - windows

I have a powershell script that needs to run 24 * 7.
To make sure it does this, I have created two (almost) identical tasks listed in task scheduler. One starts the task every day at midnight, the other is set to run with the trigger 'At System startup'. The script is set to exit at a minute to midnight.
So far so good, everything works fine. All my bases are covered. The scheduled task takes care of the script 99% of the time, and the 'on startup' task covers the occasional power-failure
However, I've noticed a subtle difference when I look at the process details.
If I open a powershell session and check the pid for the task that started at midnight using this -
PS C:\Users\Elvis> get-wmiobject win32_process | where{$_.ProcessId -eq nnnn}
(where nnnn is the PID) I see lots of details listed, including this....
CommandLine : "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -NoExit -command "&c:\myDir\myScript.ps1"
This makes sense, it's exactly what I put into the task definition.
If do a similar thing with the task that starts on boot-up, then instead of seeing the full command line I just get
CommandLine :
This may not seem important, but I want to check that no other versions of the script are running when I start a new copy. I do this by including this line in the script. (basically it checks for other powershell process running the same script name but with a different PID)
get-wmiobject win32_process | where{$_.processname -eq 'powershell.exe' -and $_.ProcessId -ne $pid -and $_.commandline -match 'myScript'}
I need to be able to either persuade the task scheduler to include the script name in the process details, or find another way to check if there's another copy of the script already running

Use what I call a "PID lockfile". Write the PID to a known file path, if the file already exists, check for the PID. If it's already running, throw an error or otherwise exit. When the script exits have it delete that file.
$lockfilePath = "\path\to\script.pid"
try {
if( Test-Path -PathType Leaf $lockFilePath ) {
$oldPid = ( Get-Content -Raw $lockfilePath ).Trim()
if( Get-Process -Id $oldPid -EA SilentlyContinue ) {
throw "Only one instance of this script can run at a time"
}
}
$PID > $lockfilePath
# Rest of your script goes within this try block
} finally {
# Add a catch block if you like but this finally code
# guarantees a deletion attempt will be made on the
# PID file whether the try block succeeds or errors
if( Test-Path -PathType Leaf $lockfilePath ) {
Remove-Item $lockfilePath -Force -EA Continue
}
}

Related

Windows cmd or powershell script to start an instance of a program, get its PID and stop it after a time period

I am trying to achieve the following using a command line script in windows:
Start an instance of a program (.exe) that launches a GUI (passing also some parameters to the command)
Wait a specific amount of time for the program to be executed (e.g. X second)
Terminate its execution
It can be the case that several instances of the program can run in parallel so what i am searching is a way to be able and terminate the specific instance of the program that was previously launched by the "start" command. A possible way i assume is to be able and get its PID but i am not sure if i can do that when using a simple command line script.
What i have tried is the following:
A) command line script for program's instance "A":
Start "" "C:\Program Files (x86)\XXXX\YYYY.exe" /USER=myUser /PASSWORDD=myPass /CMDLINEID=winTsk_IntSO_A
timeout 180
taskkill /F /T /IM YYYY.exe /FI "USERNAME eq domain\username"
timeout 30
exit
B) command line script for program's instance "B":
Start "" "C:\Program Files (x86)\XXXX\YYYY.exe" /USER=myUser /PASSWORDD=myPass /CMDLINEID=winTsk_IntSO_B
timeout 180
taskkill /F /T /IM YYYY.exe /FI "USERNAME eq domain\username"
timeout 30
exit
But obviously if i run the two scripts in parallel the taskkill command that is executed first, terminates both instances (i run them as windows scheduled tasks under a specific user account). The parameter that identifies uniquely each instance is the /CMDLINEID but i doubt i can filter the running tasks based on that parameter.
After #filimonic suggestion i am using the following powershell scripts to achieve the objective:
A) Powershell script for instance "A":
$process = [System.Diagnostics.Process]::Start(
'C:\Program Files (x86)\XXXX\YYYY.exe',
'/USER=myUser /PASSWORDD=myPass /CMDLINEID=winTsk_IntSO_A')
Write-Host $process.Id
Start-Sleep -Seconds 10
if (-not $process.HasExited) {
$process.Kill()
}
A) Powershell script for instance "B":
$process = [System.Diagnostics.Process]::Start(
'C:\Program Files (x86)\XXXX\YYYY.exe',
'/USER=myUser /PASSWORDD=myPass /CMDLINEID=winTsk_IntSO_B')
Write-Host $process.Id
Start-Sleep -Seconds 120
if (-not $process.HasExited) {
//Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ppid } | ForEach-Object { Kill-Tree $_.ProcessId }
//Stop-Process -Id $ppid
$process.Kill()
}
This is working however i have the following issue:
YYYY.exe is a GUI application that has a "loading" time ~ 60 seconds
The first script which has "10 seconds" wait period is executed normally and i can see in the task manager that the corresponding "task" is also terminated
The second script which has "120 seconds" wait period is executed normally but after its execution the "task" in task manager (same PID) remains running
Any ideas on how to more effectively terminate YYYY.exe running instance?
From one script:
$process = [System.Diagnostics.Process]::Start('cmd.exe')
$process.Id #PID here
# ... Wait something #
$process.Kill()
From second script: ($storedProcessId is somehow stored between scripts). You may requre admin permissions
$process = [System.Diagnostics.Process]::GetProcessById($storedProcessId)
$process.Kill()
Of course you may use alternaltively more powershell-native way:
$process = Start-Process -PassThrough -FileName 'cmd.exe'
Of course, there are variants with command line arguments for both ways. See docs for Process.Start and Start-Process
So your script will be like that
$process = [System.Diagnostics.Process]::Start(
'C:\Program Files (x86)\XXXX\YYYY.exe',
'/USER=myUser /PASSWORDD=myPass /CMDLINEID=winTsk_IntSO_A')
Start-Sleep -Seconds 180
if (-not $process.HasExited) {
$process.Kill()
}

Scheduled Task succesfully completes but doesn't get past import-csv

I'm trying to run below code in an automated scheduled task.
Whether I run this task manually or automated it is not working. When the option 'Run only when user is logged in' is set I at least see a PowerShell window opening, and I do see the jobs getting started. However, when the PS window closes the jobs are not visible (not completed, failed, nothing).
The logging shows the script runs till the import-csv command. I have put the CSV in the C: map, and I run the automated task as the logged in user and on highest privilege.
Why doesn't it get past import-csv? When I run this script in i.e Powershell ISE it works like a charm.
Running program
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Arguments:
–NoProfile -ExecutionPolicy Unrestricted -File "C:\Users\usr\Desktop\Scripts\script.ps1"
Start-in:
C:\Users\usr\Desktop\Scripts
Write-Host "Starting script"
$maxItems = 8
$iplist = import-csv "C:\Create.csv.txt"
Write-Host "Opened $($iplist[0])"
For ($i=0; $i -le $maxItems; $i++) {
Write-Host $iplist[$i].DisplayName
Start-Job -ScriptBlock {
Param($displayName)
try{
Start-Transcript
Write-Host "Found and started a job for $($displayName)"
Stop-Transcript
}
Catch{
Write-Host "Something went wrong "
Stop-Transcript
}
} -ArgumentList $iplist[$i].DisplayName
}
UPDATE:
The PS window closed before it got to do anything. The answer in this page send me in the right direction. The full fix I used to get this working:
Task Scheduling and Powershell's Start-Job
First, to prevent the powershell window from closing, run add the following line to the bottom of the script:
Read-Host 'Press Any Key to exit'
Second, if you run into issues with params, try explicitly naming the param with a flag:
$iplist = Import-csv -LiteralPath "C:\Create.csv.txt"
Third, make sure that you explicitly declare the delimiter being used if different than a comma.

How to get task scheduler to kill child process launched from a powershell script

I have a powershell script that launches an exe process. I have a task scheduled to run this powershell script on computer idle, and I have it set to stop it when it's not idle.
The problem is task schedule doesn't kill the launched exe process, I'm assuming it's just trying to kill the powershell process.
I can't seem to find a way to make the launched exe as a subprocess of a powershell.exe when it's launched from task scheduler
I've tried launching the process with invoke-expression, start-proces, I've also tried to pipe to out-null, wait-process, etc.. nothing seems to work when ps1 is launched from task scheduler.
Is there a way to accomplish this?
Thanks
I don't think there is an easy solution for your problem since when a process is terminated it doesn't receive any notifications and you don't get the chance to do any cleanup.
But you can spawn another watchdog process that watches your first process and does the cleanup for you:
The launch script waits after it has nothing else to do:
#store pid of current PoSh
$pid | out-file -filepath C:\Users\you\Desktop\test\currentposh.txt
$child = Start-Process notepad -Passthru
#store handle to child process
$child | Export-Clixml -Path (Join-Path $ENV:temp 'processhandle.xml')
$break = 1
do
{
Sleep 5 # run forever if not terminated
}
while ($break -eq 1)
The watchdog script kills the child if the launch script got terminated:
$break = 1
do
{
$parentPid = Get-Content C:\Users\you\Desktop\test\currentposh.txt
#get parent PoSh1
$Running = Get-Process -id $parentPid -ErrorAction SilentlyContinue
if($Running -ne $null) {
$Running.waitforexit()
#kill child process of parent posh on exit of PoSh1
$child = Import-Clixml -Path (Join-Path $ENV:temp 'processhandle.xml')
$child | Stop-Process
}
Sleep 5
}
while ($break -eq 1)
This is maybe a bit complicated and depending on your situation can be simplified. Anyways, I think you get the idea.

Assure only 1 instance of PowerShell Script is Running at any given Time

I am writing a batch script in PowerShell v1 that will get scheduled to run let's say once every minute. Inevitably, there will come a time when the job needs more than 1 minute to complete and now we have two instances of the script running, and then possibly 3, etc...
I want to avoid this by having the script itself check if there is an instance of itself already running and if so, the script exits.
I've done this in other languages on Linux but never done this on Windows with PowerShell.
For example in PHP I can do something like:
exec("ps auxwww|grep mybatchscript.php|grep -v grep", $output);
if($output){exit;}
Is there anything like this in PowerShell v1? I haven't come across anything like this yet.
Out of these common patterns, which one makes the most sense with a PowerShell script running frequently?
Lock File
OS Task Scheduler
Infinite loop with a sleep interval
Here's my solution. It uses the commandline and process ID so there's nothing to create and track. and it doesn't care how you launched either instance of your script.
The following should just run as-is:
Function Test-IfAlreadyRunning {
<#
.SYNOPSIS
Kills CURRENT instance if this script already running.
.DESCRIPTION
Kills CURRENT instance if this script already running.
Call this function VERY early in your script.
If it sees itself already running, it exits.
Uses WMI because any other methods because we need the commandline
.PARAMETER ScriptName
Name of this script
Use the following line *OUTSIDE* of this function to get it automatically
$ScriptName = $MyInvocation.MyCommand.Name
.EXAMPLE
$ScriptName = $MyInvocation.MyCommand.Name
Test-IfAlreadyRunning -ScriptName $ScriptName
.NOTES
$PID is a Built-in Variable for the current script''s Process ID number
.LINK
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[String]$ScriptName
)
#Get array of all powershell scripts currently running
$PsScriptsRunning = get-wmiobject win32_process | where{$_.processname -eq 'powershell.exe'} | select-object commandline,ProcessId
#Get name of current script
#$ScriptName = $MyInvocation.MyCommand.Name #NO! This gets name of *THIS FUNCTION*
#enumerate each element of array and compare
ForEach ($PsCmdLine in $PsScriptsRunning){
[Int32]$OtherPID = $PsCmdLine.ProcessId
[String]$OtherCmdLine = $PsCmdLine.commandline
#Are other instances of this script already running?
If (($OtherCmdLine -match $ScriptName) -And ($OtherPID -ne $PID) ){
Write-host "PID [$OtherPID] is already running this script [$ScriptName]"
Write-host "Exiting this instance. (PID=[$PID])..."
Start-Sleep -Second 7
Exit
}
}
} #Function Test-IfAlreadyRunning
#Main
#Get name of current script
$ScriptName = $MyInvocation.MyCommand.Name
Test-IfAlreadyRunning -ScriptName $ScriptName
write-host "(PID=[$PID]) This is the 1st and only instance allowed to run" #this only shows in one instance
read-host 'Press ENTER to continue...' # aka Pause
#Put the rest of your script here
If the script was launched using the powershell.exe -File switch, you can detect all powershell instances that have the script name present in the process commandline property:
Get-WmiObject Win32_Process -Filter "Name='powershell.exe' AND CommandLine LIKE '%script.ps1%'"
Loading up an instance of Powershell is not trivial, and doing it every minute is going to impose a lot of overhead on the system. I'd just scedule one instance, and write the script to run in a process-sleep-process loop. Normally I'd uses a stopwatch timer, but I don't think they added those until V2.
$interval = 1
while ($true)
{
$now = get-date
$next = (get-date).AddMinutes($interval)
do-stuff
if ((get-date) -lt $next)
{
start-sleep -Seconds (($next - (get-date)).Seconds)
}
}
This is the classic method typically used by Win32 applications. It is done by trying to create a named event object. In .NET there exists a wrapper class EventWaitHandle, which makes this easy to use from PowerShell too.
$AppId = 'Put-Your-Own-GUID-Here!'
$CreatedNew = $false
$script:SingleInstanceEvent = New-Object Threading.EventWaitHandle $true, ([Threading.EventResetMode]::ManualReset), "Global\$AppID", ([ref] $CreatedNew)
if( -not $CreatedNew ) {
throw "An instance of this script is already running."
}
Notes:
Make sure $AppId is truly unique, which is fullfilled when you use a random GUID for it.
The variable $SingleInstanceEvent should exist as long as the script is running. Putting it in the script scope as I did above, should normally be sufficient.
The event object is created in the "Global" kernel namespace, meaning it blocks execution even if the script is already running in another client session (e. g. when multiple users are logged onto the same machine). Replace "Global\$AppID" by "Local\$AppID" if you want to prevent multiple instances from running within the current client session only.
This doesn't have a race condition like the WMI (commandline) solution, because the OS kernel makes sure that only one instance of an event object with the same name can be created across all processes.
I'm not aware of a way to do what you want directly. You could consider using an external lock instead. When the script starts it changes a registry key, creates a file, or changes a file contents, or something similar, when the script is done it reverses the lock. Also at the top of the script before the lock is set there needs to be a check to see the status of the lock. If it is locked, the script exits.
$otherScriptInstances=get-wmiobject win32_process | where{$_.processname -eq 'powershell.exe' -and $_.ProcessId -ne $pid -and $_.commandline -match $($MyInvocation.MyCommand.Path)}
if ($otherScriptInstances -ne $null)
{
"Already running"
cmd /c pause
}else
{
"Not yet running"
cmd /c pause
}
You may want to replace
$MyInvocation.MyCommand.Path (FullPathName)
with
$MyInvocation.MyCommand.Name (Scriptname)
It's "always" best to let the "highest process" handle such situations. The process should check this before it runs the second instance. So my advise is to use Task Scheduler to do the job for you. This will also eliminate possible problems with permissions(saving a file without having permissions), and it will keep your script clean.
When configuring the task in Task Scheduler, you have an option under Settings:
If the task is already running, then the following rule applies:
Do not start a new instace

Register-ScheduledJob and Write-Host

I'm using Register-ScheduledJob to register job in powershell in background job I execute script. This script contains some commands like Get-Process and Write-Host command. And...
Altough every command is executed in results I don't see outputs from write-hosts (get-Process is is ok)
Maybe someone know why?
Write-Host writes to the host, which is the app that the script is running in (PowerShell.exe for instance), so it is output explicitly to the screen, and DOES NOTHING when you're running in a non-interactive environment. You should never use that to output data that you want to collect, only for lightweight debugging or for printing to the screen in interactive scripts.
You should generally use write-output for the data that you want to collect as output.
Although you can also use the debug/warning/error output (those are collected by the job, but not shown in the regular output).
Thank You very much. Write-Output helped.
Additionaly what i discover during last days and could be helpful for others: if you start scheduled background job and in this job start powershell script like this:
Register-ScheduledJob -Name1 temp -ScheduledJobOption $option -ScriptBlock {
D:\scprit.ps1
}
Your job will never end because after finish script powershell window is still open. So additionaly you have to add exit in your scriptblock:
Register-ScheduledJob -Name1 temp -ScheduledJobOption $option -ScriptBlock {
D:\scprit.ps1
Exit-PSSession
}

Resources