Marked One
Marked One

Reputation: 69

Check if PowerShell script is running using batch script

I'm running a powershell script using batch file like this :

@ECHO OFF
PowerShell.exe -Command "& 'U:\...\...\..._PS.ps1'"
PAUSE

This .ps1 script might take 15 minutes to run. During this time I want to avoid running this script once again. If a user runs the .bat file I want the console to print out something like "Powershell script is already running, wait until it's over"

From various examples I've seen this code :

echo off
tasklist /fi "imagename eq powershell.exe" |find ":" > nul
if errorlevel 1 taskkill /f /im "powershell.exe"
exit

There are 2 problems with it : 1.It only works with .exe files such as notepad,powershell etc... I have a path with .ps1 2.It kills the task, I need only to show a message

Upvotes: 1

Views: 3588

Answers (5)

sst
sst

Reputation: 1463

Since you are using PowerShell, you could also create a system-wide mutex in PowerShell to prevent multiple instances of the script to be executed at the same time.

@echo off
setlocal

:: This should be a predefined static unique value.
:: This is an example, you should generate your own GUID
set "MutexName=MyScript.PS1:{72a7654d-aa01-4e7e-a985-89add8524590}"
set "PSMsg=Powershell script is already running, wait until it''s over"

set "PSCode=[bool]$isOwner = $false; $mutex=[System.Threading.Mutex]::new($true , '%MutexName%', [ref]$isOwner)"
set "PSCode=%PSCode%; if($isOwner) {& '.\MyScript.ps1'} else {Write-Output '%PSMsg%'}; $mutex.close()"

PowerShell.exe -Command "iex ${env:PSCode}"

If you have control over the contents of the PowerShell script, then you should incorporate this method directly in to script itself instead of wrapping the script inside another code

Upvotes: 0

mklement0
mklement0

Reputation: 437052

AdminOfThings's helpful answer and Compo's helpful answer both provide robust solutions.

Here's an alternative solution that works if you're willing to make one assumption:

  • If your batch is named, say, foo.cmd, finding any cmd.exe process executing a batch file named foo.cmd is indeed executing that same batch file.

The solution takes advantage of two facts:

  • A cmd.exe console window appends the name / path (depending on invocation specifics) to the window title; for instance, if you're invoking foo.cmd from the current directory, the window title changes to Command Prompt - foo.cmd.

  • tasklist.exe /v also outputs the main-window titles of matching processes.

@echo off
setlocal

rem # Count the cmd.exe processes that have the name of this batch file
rem # in their window title.
for /f %%c in ('tasklist /v /fi "imagename eq cmd.exe" ^| find /C "%~nx0"') do (
  REM # If more than 1 process matches, another instance of this batch file
  REM # must already be running, so issue a warning and exit.
  if %%c GTR 1 echo Powershell script already running, wait for it to finish. >&2 & exit /b 1 
)

echo Running PowerShell script...

rem # Run the PowerShell script.
rem # As @Compo advises, it's better to use -File to invoke scripts
rem # and to use -ExecutionPolicy Bypass if needed (and you trust the script).
powershell.exe -file U:\path\to\your\PowerShellScript.ps1

pause

With a more effort you can make the solution more robust:

  • Assign a unique custom window title to the current console window.

  • Check for cmd.exe processes with that title first: if found, exit; if not, set the unique window title and launch the PowerShell script.

  • Caveat: You must reset the window title on completion, otherwise it will linger beyond the execution of the batch file:

    • The challenge there is that if someone terminates your batch file with Ctrl+C, you won't get a chance to reset the title.

    • However, this won't be an issue if your batch file was launched in a console window that automatically closes when the batch file terminates - such as if it was launched from the desktop or File Explorer.

@echo off
setlocal

rem # Get and save the current window title.
rem # Note: This - complex and somewhat slow - solution is necessary, 
rem #       because the `title` only supports *setting* a window title, 
rem #       not *getting it*.
for /f "usebackq delims=" %%t in (`powershell -noprofile -c "[Console]::Title.Replace(' - '+[Environment]::CommandLine,'') -replace '(.+) - .+', '$1'"`) do set origTitle=%%t

rem # Choose a unique window title.
set "titleWhileRunning=Some.ps1 is running..."

rem # If a cmd.exe process with the same window title already exists
rem # we issue a warning and exit
tasklist /v /fi "imagename eq cmd.exe" /fi "windowtitle eq %titleWhileRunning%" | find "%titleWhileRunning%" >NUL
if %ERRORLEVEL% EQU 0  echo Powershell script already running, wait for it to finish. >&2 & exit /b 1

rem # Set the new title that indicates that the script is running.
title %titleWhileRunning%

echo Running PowerShell script...

rem # Run the PowerShell script.
rem # As @Compo advises, it's better to use -File to invoke scripts
rem # and to use -ExecutionPolicy Bypass if needed (and you trust the script).
powershell.exe -file U:\path\to\your\PowerShellScript.ps1

rem # Restore the original window title.
title %origTitle%

pause

Upvotes: 0

Compo
Compo

Reputation: 38579

You could probably use to check for a powershell.exe process with an associated CommandLine ending with the scripts name:

@(For /F "Delims==" %%A In (
    'WMIC Process Where "Name='powershell.exe' And CommandLine Like '%%\\unknown[_]PS.ps1\"'" Get Name /Value'
)Do @If "%%A"=="Name" (
    Echo "Powershell script is already running, wait until it's over"&Timeout 10 /NoBreak>NUL&Exit /B)) 2>NUL
@Powershell -ExecutionPolicy RemoteSigned -File "U:\...\...\unknown_PS.ps1"

Because you did not provide the fully qualified actual file path and name, you'll need to adjust this script yourself. On line 5 replace U:\...\...\unknown_PS.ps1 with your actual file path and importantly replace unknown on line 2 with the actual prefix to _PS.ps1. Please note that the Like operator uses an underscore as a wildcard character, so any underscores require escaping by surrounding them in square brackets, (I've already done that for the known underscore in _PS.ps1). I think that it's sufficient to check only for a command line ending with \unknown_PS.ps1" although you could extend it to the full path if you expect to have more than one powershell script with the same file name.

Upvotes: 2

SNR
SNR

Reputation: 762

It seems that one solution to this problem is to use a lock file mechanism, by dbenham, that adapted to your code might look like this,

@ECHO OFF
CALL :main >>"%~f0"
EXIT /B

:main
PowerShell.exe -Command "& 'U:\...\...\..._PS.ps1'"

EXIT /B

Then if another process tries to execute this same batch-file it will fail. However a Powershell can execute .ps1. Therefore, the optimal solution, in this case, will be probably using the same mechanism inside this .ps1.

Upvotes: -1

AdminOfThings
AdminOfThings

Reputation: 25001

You could do something like the following if you are sticking with the -Command parameter:

powershell.exe -Command "if (((Get-CimInstance Win32_Process -Filter \"name = 'powershell.exe'\").CommandLine -match '..._PS\.ps1').Count -gt 1) {'Powershell is already running.'} else {& 'U:\...\...\..._PS.ps1'}"

The idea is to check the Win32_Process items currently running that have a command line that includes your ..._PS.ps1 file. Since the command that checks will add itself to the stack before condition is checked, you must check for more than 1 occurrence. Any literal . characters will need to be backslash escaped when using the -match operator since -match uses regex. Note the backslash escape of inner double quotes because you are running this from the CMD shell. Running this from PowerShell will require different escape characters.

Upvotes: 3

Related Questions