0

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.

  • 3
    Why not just use a file in a well-known location (either user-specific or machine-wide) as the mutex? This is a fairly common technique on Linux. (Some care must be taken that the file gets properly cleaned up even if scripts are interrupted, but this can be done by, for example, writing a PID to it and considering it invalid if the process with that PID no longer exists, or is not PowerShell.) – Jeroen Mostert Jun 29 '22 at 19:25
  • 1
    Yeah, I agree with using a lock file as a flag to indicate that the script is already running. It's very simple logic that ends up being very robust. – Bacon Bits Jun 29 '22 at 19:29
  • 1
    Is this for Windows only? In this case creating a file is not necessary, instead a [global event object](https://stackoverflow.com/a/71354452/7571258) can be used as a mutex. It gets cleaned up automatically by the OS, when the process exits, even when forcefully terminated. – zett42 Jun 29 '22 at 20:22
  • Thanks for answering everyone!, @zett42 I ended up going with the file logic because it sounded a little less complex, but thank you for the suggestion. – Joseph Saliba Jun 30 '22 at 19:57
  • While the file solution may look more familiar, I don't see how it is less complex. More lines of code, more things that can go wrong. – zett42 Jul 01 '22 at 07:05

1 Answers1

1

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.

Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39