16

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?

  1. Lock File
  2. OS Task Scheduler
  3. Infinite loop with a sleep interval
Slinky
  • 5,662
  • 14
  • 76
  • 130
  • 1
    How are you scheduling it? If you're using 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". – Frode F. Apr 12 '13 at 11:14
  • Well, that's easy. I can use Task Scheduler or create a lock file, which seems totally unnecessary if the OS can handle it – Slinky Apr 12 '13 at 11:30

7 Answers7

14

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
Mr. Annoyed
  • 541
  • 3
  • 18
  • 1
    Thanks, this example was exactly what I needed but in reversed order (I wanted to close the running one). I replaced `exit` with `Stop-Process -id $OtherPID -Force`. And I removed the `Press ENTER to continue...` message so it just continues with the last one without asking. – Mark Duivesteijn Apr 19 '18 at 12:20
10

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%'"
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • 1
    Doesn't this have a race condition? Both scripts could start, parse their arguments, and then run this command before the other finishes exiting. So both scripts find another one is running and then exit. It's probably a very small risk, but good to be aware of it if so. – jpmc26 Dec 16 '15 at 22:59
  • use a launch-/wrapperscript to do the `Get-Wmiobject`-test to avoid a race-condition – Frode F. Mar 18 '16 at 16:11
5

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)
    }
  }
mjolinor
  • 66,130
  • 7
  • 114
  • 135
3

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.
zett42
  • 25,437
  • 3
  • 35
  • 72
1

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.

Stanley De Boer
  • 4,921
  • 1
  • 23
  • 31
1
$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)
Pang
  • 9,564
  • 146
  • 81
  • 122
nexo1960
  • 11
  • 1
0

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
Frode F.
  • 52,376
  • 9
  • 98
  • 114
  • That is not a valid response, because if the previous task hangs and cannot complete, you would never have the script executed ever, unless you manually check and kill it. I have to run scheduled tasks on 1000 VMs and if the process hangs on a few of them, I want to be sure that it gets killed after certain amount of time, or when the new instance is triggered ;) – silverbackbg Jun 10 '22 at 14:49
  • Then add a condition to kill running task if running longer than x minutes. The question was to avoid duplicates when running a little longer. Scripts prone to freezing completely are a different situation. – Frode F. Jun 12 '22 at 13:27