【问题标题】:Assure only 1 instance of PowerShell Script is Running at any given Time确保在任何给定时间仅运行 1 个 PowerShell 脚本实例
【发布时间】:2013-04-12 11:01:56
【问题描述】:

我正在 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. 带有睡眠间隔的无限循环

【问题讨论】:

  • 你是如何安排的?如果您使用的是任务计划程序,则在设置下有一个选项:“如果任务已在运行,则适用以下规则:不要启动新实例”。
  • 嗯,这很简单。我可以使用任务计划程序或创建一个锁定文件,如果操作系统可以处理它,这似乎完全没有必要

标签: windows powershell


【解决方案1】:

这是我的解决方案。它使用命令行和进程 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

【讨论】:

  • 谢谢,这个例子正是我需要的,但顺序相反(我想关闭正在运行的)。我用Stop-Process -id $OtherPID -Force 替换了exit。我删除了Press ENTER to continue... 消息,所以它会继续最后一条而无需询问。
【解决方案2】:

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

Get-WmiObject Win32_Process -Filter "Name='powershell.exe' AND CommandLine LIKE '%script.ps1%'"

【讨论】:

  • 这不是有竞争条件吗?两个脚本都可以启动,解析它们的参数,然后在另一个完成退出之前运行这个命令。所以两个脚本都发现另一个正在运行,然后退出。这可能是一个很小的风险,但如果是这样,请注意这一点。
  • 使用启动/包装脚本执行Get-Wmiobject-test 以避免竞争条件
【解决方案3】:

加载 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)
    }
  }

【讨论】:

  • 编辑:请注意我在睡眠定时器计算中将操作数反转了。固定。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

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

    If the task is already running, then the following rule applies: 
    Do not start a new instace
    

    【讨论】:

      【解决方案6】:
      $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)
      

      【讨论】:

        【解决方案7】:

        这是 Win32 应用程序通常使用的经典方法。它是通过尝试创建一个命名的event object 来完成的。在 .NET 中存在一个包装类 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 范围内,通常就足够了。
        • 事件对象是在“全局”kernel namespace 中创建的,这意味着即使脚本已经在另一个客户端会话中运行(例如,当多个用户登录到同一台机器上时),它也会阻止执行。如果您想防止多个实例仅在当前客户端会话中运行,请将 "Global\$AppID" 替换为 "Local\$AppID"
        • 这没有像 WMI(命令行)解决方案那样的 race condition,因为操作系统内核确保只能在所有进程中创建同名事件对象的一个​​实例。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-07-08
          • 2010-10-31
          • 1970-01-01
          • 2011-09-22
          • 1970-01-01
          • 2014-11-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多