Powershell コラム:タスクスケジューラによるPowerShellの実行

Microsoft

2022.03.23

  

始めに

「タスクスケジューラによるPowerShellの実行」について紹介します。
本記事では、タスクスケジューラからバックグラウンドでPowerShellスクリプトを実行する方法を紹介します。

タスクスケジューラからPowerShellスクリプトを使う際のポイント

VBScriptを経由して実行する

特に何も考えずにタスクスケジューラでPowerShellを実行しようとすると、一瞬ウィンドウが表示されてしまいます。
これを防止するためには、VBScriptをwscript.exeを使用して呼び出します。
以下のようにWscript.ShellのRunメソッドを使うことで、ウィンドウを非表示にすることができます。

第一引数 PowerShell.exe およびパラメータ。後述。
第二引数 ウィンドウスタイル。0を与えることで非表示。
第三引数 True で同期実行、Falseで非同期実行。

PowerShell.exeをExecutionPolicyオプション付きで呼ぶ

wscript.exe からPowerShell.exe を呼び出す際には、 ExecutionPolicyを指定します。これを指定しない場合、ExecutionPolicyが指定されていないアカウントやSYSTEMアカウントなどで実行する際に失敗することがあります。

PowerShellのExitCodeをタスクスケジューラに返す

普通に実行すると、PS1ファイル内でエラーがあってもPowerShell.exeのExitCodeは0になります。スクリプト内で明示的にExitCodeを指定しましょう。
PS1ファイルの中で「Exit $ExitCode」のように数値を返し、それをVBSファイルの中でWScript.Quit(ExitCode)のように処理します。
なおVBScript側で使用しているWScript.Shell のRunメソッドは、標準出力やエラー出力を受け取る機能がありません。メッセージなど詳細な情報が欲しい場合はPowerShellからファイルに書き出したり、Write-EventLog といったコマンドレットでイベントログに書き出すなどの工夫が必要です。

ダイアログは抑制する

WScriptはエラーが出るとダイアログを出そうとします。
しかしSYSTEMアカウントなどでバックグラウンド実行すると、これを見ることができません。そこで処理が止まってしまい、タスクが実行中のままになってしまいます。

wscript.exe //B

とすることで、一部の呼び出し時のエラーダイアログは対処できます。VBScriptのOn Error Goto Next と適時使い分けてください。

タスクを強制終了してもPowerShellプロセスは残る

PowerShellがフリーズしてしまった場合、タスクが実行中のままになってしまいます。その際タスクを強制終了することはできますが、PowerShellのプロセスは残ってしまいます。
フリーズに陥るような実装は避けましょう。やむを得ない場合は、プロセスを強制終了する仕組みを合わせて実装します。

エンコードに注意する

今回のサンプルは複数のスクリプト言語にまたがるのですが、使えるエンコードに違いがあるので少し注意が必要です。
たとえばエディタによってはVBSファイルに日本語が含まれるとUTF-8で開こうとする環境がありますが、そのまま保存してしまうとエラーとなってしまいます。
このあたり公式ドキュメントが見当たらないのですが、

VBScriptはUnicode または SJIS
PowerShellはSJIS、Unicode、BOM付のUTF8

あたりが使用できるようです。

与えるパラメータについて

このサンプルスクリプトStartPs1.vbsは、パラメータを受け取れるようにしていますが、記号交じりのパラメータなど場合によっては誤動作するかもしれません。そういったパラメータが必要なら、入力ファイルなどにした方が安全です。
またPowerShell.exeは "-File" パラメータをスクリプト、それ以降の文字列をスクリプトのパラメータとして扱います。PowerShell.exeのパラメータを変えたい場合はVBSファイルの方を修正してください。

サンプルコード

Sample.ps1をタスクスケジューラからSYSTEM権限で実行します。
パラメータが必須のスクリプトを実行することもよくあることなので、呼び出すVBSにはパラメータをやり取りできるようにしました。

サンプルのZIPにはVBScript、PowerShell、タスクスケジューラのタスク設定(XML)を同梱しています。サンプルコードをZipにまとめたものはこちら
これをC:¥Temp\ に展開します。

Sample.ps1(実行するスクリプト)

C:\Temp\Sample.ps1

Param($Path)
"実行されました" | Out-File $Path -Encoding default -Force
exit 0
StartPs1.vbs

C:\Temp\StartPs1.vbs

' 使いかた:
' StartPs1.vbs <ps1ファイルのフルパス> [<ps1ファイルに続くパラメータ>]
' 例:
' WScript StartPs1.vbs D:\test.ps1 -param1 -param2 "abc"
Option Explicit
' エラーが発生するとダイアログが出て処理が終わらない問題対策
On Error Resume Next
'オブジェクト変数の宣言と代入
Dim objWshNamed, objWshUnnamed
Set objWshNamed = WScript.Arguments.Named
Set objWshUnnamed = WScript.Arguments.Unnamed
'名前なし引数の列挙
Dim PS1File
Dim Arguments
Dim intIndex
' 引数を文字列にする
Arguments = ""
If objWshUnnamed.Count > 0 Then
For intIndex = 0 To objWshUnnamed.Count - 1
if intIndex = 0 Then
' 第一引数は PS1ファイルパス
PS1File = objWshUnnamed.Item(intIndex)
else
Arguments = Arguments & " " & objWshUnnamed.Item(intIndex)
end If
Next
End If
dim objShell
dim exitcode
dim oExec
Set objShell = CreateObject("Wscript.Shell")
' ここまでエラーが起きていなければ処理実行
If Err.Number=0 Then
exitcode = objShell.run ("PowerShell.exe -ExecutionPolicy RemoteSigned -File """ & PS1File & """ " & Arguments,0,True)
End If
Set objWshNamed = Nothing
Set objWshUnnamed = Nothing
Set objShell = Nothing
' 終了コードを返して終了
Wscript.Quit(exitcode)
SampleTask.xml(タスクスケジューラ)

GUIやコマンドでタスクをインポートします。
GUIで入力する場合:
タスクスケジューラを起動し、以下のように設定します。

全般

名前 _SampleTask
タスクの実行に使うユーザーアカウント SYSTEM

操作

操作 プログラムの開始
プログラム/スクリプト wscript.exe
引数の追加 //B "C:\Temp\StartPs1.vbs" "C:\Temp\Sample.ps1" "C:\Temp\Result.txt"

なおXMLをタスクスケジューラのGUIでインポートすると「タスクXMLに、書式設定が正しくない値または範囲外の値が含まれています。」というエラーが出る環境があります。
その場合は「ユーザーまたはグループの変更」でユーザアカウントを再度指定するか、後述のCUIでインポートする方法を実行します。

CUIでインポートする場合

C:¥Temp\ に保存した場合、コマンドプロンプトでは

schtasks /create /tn "_SampleTask" /xml "C:\Temp\SampleTask.xml"

のコマンドでインポートすることができます。

PowerShellのコマンドレットを使う場合は

Register-ScheduledTask -XML (Get-Content "C:\Temp\SampleTask.xml" | Out-String) -TaskName "_SampleTask"

のように実行します。XMLファイルのエンコードはUTF-16です。-XMLのパラメータがパスでないことに注意してください。
「既に存在するファイルを作成することはできません」と出た場合は -Force オプションを付けると、既存のタスクを上書きできます。

StartPs1.xml

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2022-02-05T03:07:56.9706188</Date>
<Author>intellilink</Author>
<URI>\_SampleTask</URI>
</RegistrationInfo>
<Triggers />
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>wscript.exe</Command>
<Arguments>"C:\Temp\StartPs1.vbs" "C:\Temp\Sample.ps1" "C:\Temp\Result.txt"</Arguments>
</Exec>
</Actions>
</Task>

実行

タスクスケジューラを開き「_SampleTask」タスクを開始します。
実行後、C:¥Temp¥Result.txt ができていればスクリプトは動作しています。

終わりに

タスクスケジューラからPowerShellスクリプトをバックグラウンド実行させる方法について紹介しました。
手法自体はバッチファイルのころからあるものなので、パラメータやエラーコードの扱いなどちょっとだけPowerShell用にアレンジを加えてみました。
参考になれば幸いです。

※文章中の商品名、会社名、団体名は、各社の商標または登録商標です。