Mutual Exclusion Techniques for Batch Scripts
In some situations, it's important to ensure that a Batch script runs only once at a time, in order to preserve the integrity of resources that cannot be shared or accessed concurrently. While implementing a mutex is straightforward in PowerShell or other .NET languages, the default Windows Shell does not provide an equally simple mechanism to prevent parallel execution of Batch scripts.
In this post, I explore several workaround methods for identifying and managing a single running instance of a Batch script.
1. Using a Lock File
One of the simplest techniques for achieving mutual exclusion in Batch scripts is the use of a lock file. When the script starts, it creates a specific file to signal that an instance is already running. Upon completion, the script deletes the lock file.
If another instance of the script is launched while the lock file exists, it detects the file's presence and exits immediately, preventing parallel execution.
Here's what a script using this technique might look like:
mutex-script.bat
set lockfile=%TEMP%\mutex.lock
fsutil file createnew "%lockfile%" 0 > nul || goto :eof
@rem -- resource manipulation --
del "%lockfile%"
fsutil
will fail (and thus return an error) if the file already exists, which is leveraged here to detect another running instance. However, if you prefer not to rely on an external utility like fsutil
, you can achieve the same logic using built-in commands:
if exist "%lockfile%" goto :eof
type nul > "%lockfile%"
The %TEMP%
directory is used in this example because it's a location where users typically have both read and write permissions. However, the choice is arbitrary — any directory where the script has permission to create and check for files is a suitable option.
While more complex logic can be implemented to improve reliability, this basic pattern illustrates the fundamental idea behind the lock file scheme.
Pros and Cons
The main advantage of this method is its simplicity — it's easy to implement using only built-in commands, with no need for external tools.
However, it is not entirely reliable. If the script terminates unexpectedly (e.g., due to a crash or forced closure) before the lock file is deleted, the file will remain. In such cases, future runs of the script will detect the stale lock file and exit, even though no other instance is actually running. This requires manual intervention to delete the lock file before the script can run again.
To reduce the likelihood of a stale lock file due to errors in the main logic, you can isolate the resource manipulation logic into a separate Batch script. This helps contain potential faults and ensures that the lock file management stays clean. For example:
mutex-script.bat
set lockfile=%TEMP%\mutex.lock
if exist "%lockfile%" goto :eof
type nul > "%lockfile%"
call handle-resource.bat %*
del "%lockfile%"
This approach is less intrusive than launching a new cmd
process, but if needed, you can also execute the isolated script (handle-resource.bat
) in a new Command Prompt window as a more robust fallback.
2. Using the Window Title
Changing the title of a Command Prompt window is straightforward, and assigning a unique title allows you to distinguish one cmd
instance from another. This can be done in a Batch script using the title
command:
title Unique-Title
However, the displayed window title may vary depending on certain conditions:
Elevation: If the Command Prompt is run with administrator privileges, the title will be prefixed with
Administrator:
followed by two spaces before your specified title.Focus: If the window is actively selected (e.g., clicked or focused), it may also be prefixed with
Select
.Combination: When both conditions are true, the full prefix becomes
Select Administrator:
.
Therefore, to reliably detect a running instance, your script should check for all possible title variants. Here's how to implement that:
mutex-script.bat
set uniqueTitle=Unique-Title&
(
for %%i in (
""
"Select "
"Administrator: "
"Select Administrator: "
) do tasklist /fi "imagename eq cmd.exe" /fi "windowtitle eq %%~i%uniqueTitle%" /v /fo:list
) | findstr /bil "Window Title:" > nul && goto :eof
title %uniqueTitle%
@rem -- resource manipulation --
This script checks if any running cmd
process has a window title matching any of the specified variants. If a match is found the script exits early.
Using PowerShell
The tasklist
command is limited in that it does not support disjunction (OR) filters directly. This means you cannot check for multiple possible window titles in a single command using logical OR. As a result, a script using tasklist
must scan the full list of processes multiple times — once for each title variant.
To avoid this inefficiency, you can use PowerShell's Get-Process
cmdlet with the Where-Object
filter, which allows you to evaluate all possible titles in a single pass through the process list:
mutex-script.bat
set uniqueTitle=Unique-Title&
powershell "exit @(Get-Process cmd | Where-Object MainWindowTitle -In (@('','Select ','Administrator: ','Select Administrator: ') -replace '$',%uniqueTitle%') | Select-Object -First 1).Count" || goto :eof
title %uniqueTitle%
@rem -- resource manipulation --
While PowerShell one-liners like this are compact and powerful, they tend to sacrifice readability. To make the script easier to maintain, you can offload the logic into a dedicated PowerShell script located alongside mutex-script.bat
, named Test-UniqueInstance.ps1
:
Test-UniqueInstance.ps1
exit @(
Get-Process cmd |
Where-Object MainWindowTitle -In (@(
''
'Select '
'Administrator: '
'Select Administrator: '
) -replace '$',$args[0]) |
Select-Object -First 1
).Count
Then update your Batch script to invoke the PowerShell file, passing the unique title as an argument:
mutex-script.bat
set uniqueTitle=Unique-Title&
powershell -ExecutionPolicy Bypass -File "%~dp0Test-UniqueTitle.ps1" %uniqueTitle% || goto :eof
title %uniqueTitle%
@rem -- resource manipulation --
Pros and Cons
Using the window title method is also relatively simple. However, there are some important caveats:
Persistent Window Title Change: When the script modifies the Command Prompt window title using the
title
command, the change is permanent for that session. If the script is run in the same CMD thread, the window title does not revert to its original value after the script finishes. A reliable workaround is to run the Batch script in a new Command Prompt process.cmd /c mutex-script.bat [args]
Windows Terminal Consideration: Another subtle issue arises when using Windows Terminal. Running the
title
command affects theWindowsTerminal.exe
window rather than the specificcmd.exe
session. This may not be a major issue, but if precise window title management is important, it's better to launch the script in a new Command Prompt window usingstart
, which gives the new process its own title context:start /wait mutex-script.bat [args]
If you don't need the parent script to wait for completion, you can omit the /wait
switch.
Be aware that if the parent CMD process modifies the window title even once — intentionally or by mistake — before spawning a new cmd process, it can cause the new process to inherit and not revert to the parent's title unexpectedly.
In such cases, using the start method is generally the most reliable option, as it gives the new process a fully independent title context.
3. Inspecting the Command Line
This technique is similar to the window title method in that it relies on string comparison to identify running script instances. However, unlike the window title approach, it inspects the command line used to launch the process, which requires spawning a new cmd
process to capture that information.
cmd /c mutex-script.bat [args]
The idea is to use a distinguishing element — such as the path to the script, a unique script name, or a specific argument (e.g., a GUID) — to identify the running instance. We then check for existing processes whose command lines match that signature.
Unlike previous methods—where the differentiating element (like a window title or lock file) is set after the script starts—this approach leverages information fixed at launch. To avoid false positives, we must ensure the current script instance is excluded from the match. This is done by comparing command lines while excluding the process with the same process ID (PID) as the current one.
Using WMI
To query running processes and their command lines, we use the Win32_Process
WMI class. This is similar to using tasklist
, but offers access to more detailed information such as the CommandLine
property.
The WMI query structure is:
SELECT CommandLine FROM Win32_Process
WHERE Name="cmd.exe" AND CommandLine LIKE "%search_specifier%" AND ProcessId<>pid
We'll demonstrate this using PowerShell and WMIC (with .NET-based options discussed in another post). To keep things simple, the script name (accessible as %~n0
in Batch) will be used as the search specifier.
In WMIC, we can use the Win32_Process class alias Process
:
mutex-script.bat
powershell exit (Get-CimInstance Win32_Process -Filter """ProcessId=$PID""" -Property ParentProcessId).ParentProcessId
wmic Process where "Name='cmd.exe' AND CommandLine LIKE '%%%~n0%%' AND ProcessId<>%errorlevel%" get CommandLine /format:list | findstr /bil "CommandLine=" > nul && goto :eof
@rem -- resource manipulation --
With PowerShell alone:
mutex-script.bat
powershell exit @(Get-CimInstance Win32_Process -Filter """ProcessId=$PID""" -Property ParentProcessId).ParentProcessId
powershell exit @(Get-CimInstance Win32_Process -Filter """Name='cmd.exe' AND CommandLine LIKE '%%%~n0%%' AND ProcessId<>%errorlevel%""" -Property CommandLine ^| Select-Object -First 1).Count || goto :eof
@rem -- resource manipulation --
To avoid lengthy and hard-to-read embedded PowerShell one-liners in Batch files, you can move the logic into a dedicated PowerShell script, as demonstrated in the second section. For example:
Test-UniqueInstance.ps1
$PSDefaultParameterValues['Get-CimInstance:ClassName'] = 'Win32_Process'
exit @(
Get-CimInstance -Filter "Name='cmd.exe' AND CommandLine LIKE '%$($args[0])%' AND ProcessId<>$(
Get-CimInstance -Filter "ProcessId=$PID" -Property ParentProcessId |
Select-Object -ExpandProperty ParentProcessId
)" -Property CommandLine |
Select-Object -First 1
).Count
Then update your Batch script to invoke the PowerShell file, passing the name of the Batch file (without its extension) as an argument:
mutex-script.bat
powershell -ExecutionPolicy Bypass -File "%~dp0Test-UniqueTitle.ps1" "%~n0" || goto :eof
@rem -- resource manipulation --
Using PowerShell Core
You can achieve the same result with PowerShell Core by replacing powershell
with pwsh -Command
. Notably, PowerShell Core's Get-Process
allows easy retrieval of the parent PID, enabling you to merge the PID retrieval and instance detection into one line:
mutex-script.bat
pwsh -Command exit @(Get-Process cmd ^| Where-Object CommandLine -Like "*%~n0*" ^| Where-Object Id -NE (Get-Process -Id $PID).Parent.Id ^| Select-Object -First 1).Count || goto :eof
@rem -- resource manipulation --
Below is the PowerShell script equivalent:
Test-UniqueInstance.ps1
#Requires -PSEdition Core
exit @(
Get-Process cmd |
Where-Object CommandLine -Like "*$($args[0])*" |
Where-Object Id -NE (Get-Process -Id $PID).Parent.Id |
Select-Object -First 1
).Count
Pros and Cons
The main advantage of this method is that no cleanup is needed when the script exits — even abruptly. It doesn't involve creating or modifying system resources, it's robust against unexpected terminations.
However, it comes with some caveats:
Complexity: This approach is more intricate than others and requires careful handling of process queries.
Privilege Boundaries: A regular user cannot query command lines of processes launched with elevated privileges. As a result, two script instances — one running as admin and the other as a normal user — could run simultaneously. If both have access to the same shared resource, this could compromise data integrity.
In cases where the resource itself requires administrative privileges, the script running without elevation will likely fail due to UAC or access errors. However, if that's not the case, concurrent access could result in data corruption.
4. Conclusion
Ensuring mutual exclusion in Batch scripts is a nuanced challenge, especially given the limitations of the native Windows Shell. While Batch lacks a built-in mechanism for single-instance enforcement, we've explored three practical workarounds — each with its own trade-offs in complexity, reliability, and security context.
Lock files are simple and effective, but require cleanup and can be error-prone in case of abnormal termination.
Window titles offer a visual and lightweight solution, but are susceptible to variation depending on elevation and host environment.
Command line inspection is the most robust and cleanup-free method, but introduces complexity and potential visibility limitations across privilege boundaries.
In the Custom Drive Letter Assigner project, I use the command-line inspection technique in the assign.bat
script. A GUID serves as the unique search specifier that identifies the script instance. This GUID is passed to the PowerShell script test_uniqueProcess.ps1
, which returns a non-zero exit code if the current script instance is not unique. This mechanism ensures that no more than one drive is modified at a time — an essential safeguard for maintaining consistency and preventing conflicts.
Comments
Post a Comment