Search code examples
powershellscriptingpowershell-7.0

How to prevent multiple instances of the same PowerShell 7 script?


Context

On a build server, a PowerShell 7 script script.ps1 will be started and will be running in the background in the remote computer.

What I want

A safenet to ensure that at most 1 instance of the script.ps1 script is running at once on the build server or remote computer, at all times.

What I tried:

I tried meddling with PowerShell 7 background jobs (by executing the script.ps1 as a job inside a wrapper script wrapper.ps1), however that didn't solve the problem as jobs do not carry over (and can't be accessed) in other PowerShell sessions.

What I tried looks like this:

# inside wrapper.ps1

$running_jobs = $(Get-Job -State Running) | Where-Object {$_.Name -eq "ImportantJob"}

if ($running_jobs.count -eq 0) {
    Start-Job .\script.ps1 -Name "ImportantJob" -ArgumentList @($some_variables)
} else {
    Write-Warning "Could not start new job; Existing job detected must be terminated beforehand."
}

To reiterate, the problem with that is that $running_jobs only returns the jobs running in the current session, so this code only limits one job per session, allowing for multiple instances to be ran if multiple sessions were mistakenly opened.

What I also tried:

I tried to look into Get-CimInstance:

 $processes = Get-CimInstance -ClassName Win32_Process | Where-Object {$_.Name -eq "pwsh.exe"}

While this does return the current running PowerShell instances, these elements carry no information on the script that is being executed, as shown after I run:

foreach ($p in $processes) {
     $p | Format-List *
}

I'm therefore lost and I feel like I'm missing something. I appreciate any help or suggestions.


Solution

  • I like to define a config path in the $env:ProgramData location using a CompanyName\ProjectName scheme so I can put "per system" configuration. You could use a similar scheme with a defined location to store a lock file created when the script run and deleted at the end of it (as suggested already within the comments).

    Then, it is up to you to add additional checks if needed (What happen if the script exit prematurely while the lock is still present ?)

    Example

    
    # Define default path (Not user specific)
    $ConfigLocation = "$Env:ProgramData\CompanyName\ProjectName"
    # Create path if it does not exist
    New-Item -ItemType Directory -Path $ConfigLocation -EA 0 | Out-Null
    
    $LockFilePath =  "$ConfigLocation\Instance.Lock"
    
    $Locked = $null -ne (Get-Item -Path $LockFilePath -EA 0)
    if ($Locked) {Exit}
    
    
    # Lock
    New-Item -Path $LockFilePath
    
    # Do stuff 
    
    # Remove lock
    Remove-Item -Path $LockFilePath
    

    Alternatively, on Windows, you could also use a scheduled task without a schedule and with the setting "If the task is already running, then the following rule applies: Do not start a new instance". From there, instead of calling the original script, you call a proxy script that just launch the scheduled task.