13

我正在 PowerShell v1 中编写一个批处理脚本,它将被安排运行,假设每分钟运行一次。不可避免地,工作需要超过 1 分钟才能完成,现在我们有两个脚本实例正在运行,然后可能有 3 个,等等......

我想通过让脚本本身检查是否有自己的实例已经在运行来避免这种情况,如果是,则脚本退出。

我已经在 Linux 上用其他语言完成了此操作,但从未在 Windows 上使用 PowerShell 完成此操作。

例如,在 PHP 中,我可以执行以下操作:

exec("ps auxwww|grep mybatchscript.php|grep -v grep", $output);
if($output){exit;}

PowerShell v1 中有这样的东西吗?我还没有遇到过这样的事情。

在这些常见模式中,哪一种模式最适合频繁运行 PowerShell 脚本?

  1. 锁定文件
  2. 操作系统任务计划程序
  3. 带有睡眠间隔的无限循环
4

7 回答 7

11

这是我的解决方案。它使用命令行和进程 ID,因此无需创建和跟踪任何内容。它并不关心您如何启动脚本的任何一个实例。

以下应该按原样运行:

    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
于 2015-11-06T20:07:01.820 回答
9

如果脚本是使用 powershell.exe -File 开关启动的,您可以检测到进程命令行属性中存在脚本名称的所有 powershell 实例:

Get-WmiObject Win32_Process -Filter "Name='powershell.exe' AND CommandLine LIKE '%script.ps1%'"
于 2013-04-12T11:28:45.323 回答
5

加载 Powershell 实例并非易事,每分钟加载一次都会给系统带来大量开销。我只需安排一个实例,然后编写脚本以在进程-睡眠-进程循环中运行。通常我会使用秒表计时器,但我认为他们在 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)
    }
  }
于 2013-04-12T11:23:36.533 回答
1

“总是”最好让“最高进程”处理这种情况。该进程应在​​运行第二个实例之前检查这一点。所以我的建议是使用任务计划程序为您完成这项工作。这也将消除可能的权限问题(在没有权限的情况下保存文件),并且它将保持您的脚本干净。

在任务计划程序中配置任务时,您可以在“设置”下选择:

If the task is already running, then the following rule applies: 
Do not start a new instace
于 2013-04-12T12:12:57.233 回答
1

我不知道有一种方法可以直接做你想做的事。您可以考虑改用外部锁。当脚本启动时,它会更改注册表项、创建文件或更改文件内容或类似的东西,当脚本完成时,它会反转锁定。同样在设置锁之前的脚本顶部,需要检查以查看锁的状态。如果它被锁定,则脚本退出。

于 2013-04-12T11:20:04.793 回答
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
}

您可能想要更换

$MyInvocation.MyCommand.Path (FullPathName)

$MyInvocation.MyCommand.Name (Scriptname)
于 2017-01-02T08:53:39.873 回答
0

这是 Win32 应用程序通常使用的经典方法。它是通过尝试创建一个命名的事件对象来完成的。在 .NET 中存在一个 wrapper class EventWaitHandle,这也使得它在 PowerShell 中也很容易使用。

$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."
}  

笔记:

  • 确保$AppId它是真正唯一的,当您为其使用随机 GUID 时,它会被完全填充。
  • $SingleInstanceEvent只要脚本正在运行,该变量就应该存在。script像我上面那样把它放在范围内,通常就足够了。
  • 事件对象是在“全局”内核命名空间中创建的,这意味着即使脚本已经在另一个客户端会话中运行(例如,当多个用户登录到同一台机器上时),它也会阻止执行。如果您想防止多个实例仅在当前客户端会话中运行,请替换"Global\$AppID"为。"Local\$AppID"
  • 这不像 WMI(命令行)解决方案那样具有竞争条件,因为操作系统内核确保只能在所有进程中创建具有相同名称的事件对象的一个​​实例。
于 2022-03-04T16:38:26.897 回答