PowerShell 自更新脚本
PowerShell Self-Updating Script
我们有一个 PowerShell 脚本可以持续监控文件夹中的新 JSON 文件并将它们上传到 Azure。我们将此脚本保存在共享文件夹中,以便多人可以同时 运行 此脚本以实现冗余。每个人的计算机都有一个在登录时 运行 它的计划任务,因此脚本总是 运行ning。
我想更新脚本,但我不得不要求每个人停止他们的 运行ning 脚本并重新启动它。这尤其麻烦,因为我们最终希望 运行 这个脚本处于 "hidden" 模式,这样就不会有人不小心关闭 window。
所以我想知道是否可以创建一个自动更新的脚本。我想出了下面的代码,当这个脚本是 运行 并且保存了一个新版本的脚本时,我希望 运行ning PowerShell window 在它点击 Exit
命令然后重新打开一个新的 window 到 运行 新版本的脚本。然而,那并没有发生。
它毫无声息地继续前进。它不会关闭当前的 window,它甚至会在屏幕上保留旧版本脚本的输出。就好像 PowerShell 并不是真的 Exit
,它只是弄清楚发生了什么,并继续使用新版本的脚本。我想知道为什么会这样?喜欢,就是看不懂
#Place at top of script
$lastWriteTimeOfThisScriptWhenItFirstStarted = [datetime](Get-ItemProperty -Path $PSCommandPath -Name LastWriteTime).LastWriteTime
#Continuous loop to keep this script running
While($true) {
Start-Sleep 3 #seconds
#Run this script, change the text below, and save this script
#and the PowerShell window stays open and starts running the new version without a hitch
"Hi"
$lastWriteTimeOfThisScriptNow = [datetime](Get-ItemProperty -Path $PSCommandPath -Name LastWriteTime).LastWriteTime
if($lastWriteTimeOfThisScriptWhenItFirstStarted -ne $lastWriteTimeOfThisScriptNow) {
. $PSCommandPath
Exit
}
}
有趣的旁注
我决定看看如果我的计算机失去与 运行ning 脚本所在的共享文件夹的连接会发生什么。它继续 运行,但按预期每 3 秒显示一条错误消息。但是,当网络连接恢复时,它通常会恢复到旧版本的脚本。
因此,如果我在脚本中将 "Hi" 更改为 "Hello" 并保存,"Hello" 将按预期开始出现。如果我拔下网络电缆一段时间,我很快就会收到预期的错误消息。但是当我重新插入电缆时,脚本通常会再次开始输出 "Hi",即使新保存的版本中有 "Hello"。我想这是脚本在遇到 Exit
命令时从未真正退出这一事实的负面影响。
. $PSCommand
是一个 阻塞 (同步)调用,这意味着下一行的 Exit
直到 $PSCommand
才会执行自己退出了。
鉴于 $PSCommand
这是您的脚本, 永远不会退出 (即使它 看似 退出),从未达到 Exit
语句(假设新版本的脚本保持相同的基本 while
循环逻辑)。
虽然这种方法原则上可行,但有一些注意事项:
您正在使用 .
, the "dot-sourcing" operator,这意味着脚本的新内容已加载到 current 范围(通常您始终保持在相同的过程,就像调用 *.ps1
文件时一样,无论是使用 .
还是(隐含的)常规调用运算符 &
) .
新脚本中的变量/函数/别名然后替换当前范围内的旧变量/函数/别名,您从新版本脚本中删除的旧定义会 徘徊 并可能导致不必要的副作用。
根据您自己的观察,如果新脚本包含导致其退出的语法错误,您的自我更新机制将会中断,因为 Exit
语句则是,没有运行.
也就是说,您可以将其用作检测调用新版本失败的机制:
- 使用
try { . $ProfilePath } catch { Write-Error $_ }
而不是 . $ProfilePath
- 而不是
Exit
命令,发出警告(或做任何适当的事情来提醒某人失败)然后 继续循环(continue
),这意味着旧脚本一直有效,直到找到 有效 新脚本。
即使采用上述方法,这种方法的基本限制是您可能会超过最大调用递归深度。嵌套的 .
调用堆积起来,当达到嵌套限制时,您将不会
能够执行另一个,并且您陷入了无用的重试循环。
也就是说,从 Windows PowerShell v5.1 开始,这个 限制似乎是大约 4900 个嵌套调用 ,所以如果您不希望脚本在给定用户时频繁更新会话处于活动状态(重启/注销会重新开始),这可能不是问题。
替代方法:
更稳健的方法是创建一个单独的看门狗脚本,其唯一目的是监视新版本,终止旧的 运行 脚本并启动新的一,具有启动新脚本失败时的警报机制。
另一种选择是让主脚本有 "stages",其中 运行s 命令基于文件夹中最高版本脚本的名称。不过,我认为 mklement0 的看门狗是个绝妙的主意。
但我指的是做你所做的,但使用变量作为你的命令,并且这些变量会使用最高编号的脚本名称进行更新。这样你只需将 10.ps1 放入文件夹,它就会忽略 9.ps1。该脚本中的函数将命名为 mainfunction10 等...
类似
$command = ((get-childitem c:\path\to\scriptfolder\).basename)[-1]
& "C:\path\to\scruptfolder\$command"
文件必须按字母顺序从最旧到最新命名。否则,您将不得不按日期对对象进行排序。
$command = ((get-childitem c:\path\to\scriptfolder\ | sort-object -Property lastwritetime).basename)[-1]
& "C:\path\to\scruptfolder\$command"
或者。源而不是将其用作命令。然后让后面的代码调用 function$command
之类的函数,函数将是脚本的名称
我还是更喜欢看门狗的创意。
看门狗看起来有点像
While ($true) {
$new = ((get-childitem c:\path\to\scriptfolder\ | sort-object -Property lastwritetime).fullname)[-1]
If ($old -ne $new){
Kill $old
Sleep 10
& $new
}
$old -eq $new
Sleep 600
}
请注意,我不确定脚本是怎样的 运行,您可能需要根据用于启动它的命令来寻找 powershell 的实例。
$kill = ((WMIC path win32_process get Caption,Processid,Commandline).where({$_.commandline -contains $command})).processid
Kill $kill
将取代 kill $old
此命令是有根据的猜测,未经测试。
其他技巧是 运行将看门狗的主脚本作为一项工作。获取作业 ID。然后检查文件更改。如果有新文件进来,watch dog 会杀死作业 Id 并重复整个过程
你也可以让脚本结束。并且每 10 分钟有一个 windows 作业,只需重新 运行 脚本。这样一来,您就可以每十分钟 运行 获得任何脚本。不过,这对每个初创公司来说都更加激烈。
您可以使用 break
来终止循环而不是退出。并且脚本会自然退出
您可以使用测试连接来检查服务器。但如果是每 3 秒一次。如果来自很多计算机的 ping 就很多了
我们有一个 PowerShell 脚本可以持续监控文件夹中的新 JSON 文件并将它们上传到 Azure。我们将此脚本保存在共享文件夹中,以便多人可以同时 运行 此脚本以实现冗余。每个人的计算机都有一个在登录时 运行 它的计划任务,因此脚本总是 运行ning。
我想更新脚本,但我不得不要求每个人停止他们的 运行ning 脚本并重新启动它。这尤其麻烦,因为我们最终希望 运行 这个脚本处于 "hidden" 模式,这样就不会有人不小心关闭 window。
所以我想知道是否可以创建一个自动更新的脚本。我想出了下面的代码,当这个脚本是 运行 并且保存了一个新版本的脚本时,我希望 运行ning PowerShell window 在它点击 Exit
命令然后重新打开一个新的 window 到 运行 新版本的脚本。然而,那并没有发生。
它毫无声息地继续前进。它不会关闭当前的 window,它甚至会在屏幕上保留旧版本脚本的输出。就好像 PowerShell 并不是真的 Exit
,它只是弄清楚发生了什么,并继续使用新版本的脚本。我想知道为什么会这样?喜欢,就是看不懂
#Place at top of script
$lastWriteTimeOfThisScriptWhenItFirstStarted = [datetime](Get-ItemProperty -Path $PSCommandPath -Name LastWriteTime).LastWriteTime
#Continuous loop to keep this script running
While($true) {
Start-Sleep 3 #seconds
#Run this script, change the text below, and save this script
#and the PowerShell window stays open and starts running the new version without a hitch
"Hi"
$lastWriteTimeOfThisScriptNow = [datetime](Get-ItemProperty -Path $PSCommandPath -Name LastWriteTime).LastWriteTime
if($lastWriteTimeOfThisScriptWhenItFirstStarted -ne $lastWriteTimeOfThisScriptNow) {
. $PSCommandPath
Exit
}
}
有趣的旁注
我决定看看如果我的计算机失去与 运行ning 脚本所在的共享文件夹的连接会发生什么。它继续 运行,但按预期每 3 秒显示一条错误消息。但是,当网络连接恢复时,它通常会恢复到旧版本的脚本。
因此,如果我在脚本中将 "Hi" 更改为 "Hello" 并保存,"Hello" 将按预期开始出现。如果我拔下网络电缆一段时间,我很快就会收到预期的错误消息。但是当我重新插入电缆时,脚本通常会再次开始输出 "Hi",即使新保存的版本中有 "Hello"。我想这是脚本在遇到 Exit
命令时从未真正退出这一事实的负面影响。
. $PSCommand
是一个 阻塞 (同步)调用,这意味着下一行的 Exit
直到 $PSCommand
才会执行自己退出了。
鉴于 $PSCommand
这是您的脚本, 永远不会退出 (即使它 看似 退出),从未达到 Exit
语句(假设新版本的脚本保持相同的基本 while
循环逻辑)。
虽然这种方法原则上可行,但有一些注意事项:
您正在使用
.
, the "dot-sourcing" operator,这意味着脚本的新内容已加载到 current 范围(通常您始终保持在相同的过程,就像调用*.ps1
文件时一样,无论是使用.
还是(隐含的)常规调用运算符&
) .
新脚本中的变量/函数/别名然后替换当前范围内的旧变量/函数/别名,您从新版本脚本中删除的旧定义会 徘徊 并可能导致不必要的副作用。根据您自己的观察,如果新脚本包含导致其退出的语法错误,您的自我更新机制将会中断,因为
Exit
语句则是,没有运行.
也就是说,您可以将其用作检测调用新版本失败的机制:- 使用
try { . $ProfilePath } catch { Write-Error $_ }
而不是. $ProfilePath
- 而不是
Exit
命令,发出警告(或做任何适当的事情来提醒某人失败)然后 继续循环(continue
),这意味着旧脚本一直有效,直到找到 有效 新脚本。
- 使用
即使采用上述方法,这种方法的基本限制是您可能会超过最大调用递归深度。嵌套的
.
调用堆积起来,当达到嵌套限制时,您将不会 能够执行另一个,并且您陷入了无用的重试循环。
也就是说,从 Windows PowerShell v5.1 开始,这个 限制似乎是大约 4900 个嵌套调用 ,所以如果您不希望脚本在给定用户时频繁更新会话处于活动状态(重启/注销会重新开始),这可能不是问题。
替代方法:
更稳健的方法是创建一个单独的看门狗脚本,其唯一目的是监视新版本,终止旧的 运行 脚本并启动新的一,具有启动新脚本失败时的警报机制。
另一种选择是让主脚本有 "stages",其中 运行s 命令基于文件夹中最高版本脚本的名称。不过,我认为 mklement0 的看门狗是个绝妙的主意。
但我指的是做你所做的,但使用变量作为你的命令,并且这些变量会使用最高编号的脚本名称进行更新。这样你只需将 10.ps1 放入文件夹,它就会忽略 9.ps1。该脚本中的函数将命名为 mainfunction10 等...
类似
$command = ((get-childitem c:\path\to\scriptfolder\).basename)[-1]
& "C:\path\to\scruptfolder\$command"
文件必须按字母顺序从最旧到最新命名。否则,您将不得不按日期对对象进行排序。
$command = ((get-childitem c:\path\to\scriptfolder\ | sort-object -Property lastwritetime).basename)[-1]
& "C:\path\to\scruptfolder\$command"
或者。源而不是将其用作命令。然后让后面的代码调用 function$command
之类的函数,函数将是脚本的名称
我还是更喜欢看门狗的创意。
看门狗看起来有点像
While ($true) {
$new = ((get-childitem c:\path\to\scriptfolder\ | sort-object -Property lastwritetime).fullname)[-1]
If ($old -ne $new){
Kill $old
Sleep 10
& $new
}
$old -eq $new
Sleep 600
}
请注意,我不确定脚本是怎样的 运行,您可能需要根据用于启动它的命令来寻找 powershell 的实例。
$kill = ((WMIC path win32_process get Caption,Processid,Commandline).where({$_.commandline -contains $command})).processid
Kill $kill
将取代 kill $old
此命令是有根据的猜测,未经测试。
其他技巧是 运行将看门狗的主脚本作为一项工作。获取作业 ID。然后检查文件更改。如果有新文件进来,watch dog 会杀死作业 Id 并重复整个过程
你也可以让脚本结束。并且每 10 分钟有一个 windows 作业,只需重新 运行 脚本。这样一来,您就可以每十分钟 运行 获得任何脚本。不过,这对每个初创公司来说都更加激烈。
您可以使用 break
来终止循环而不是退出。并且脚本会自然退出
您可以使用测试连接来检查服务器。但如果是每 3 秒一次。如果来自很多计算机的 ping 就很多了