背景
MSI Center の更新によってあるいはソフトの競合によってか、LEDKeeper2 が PC の起動後最初のログインでクラッシュする事象が多発しました。それによって信頼性モニターに✖印が付けまくられるのをどうにかしたいと思ったのが始まりです。まずは Mystic_Light_Service によって登録されるタスクに遅延を設定し様子を見ましたが、特に改善されませんでした。そこで LEDKeeper2 の直接起動が原因なんだと考え、PowerShell を経由する条件で作成したのが以下のスクリプトになります。
説明
Mystic_Light_Service によって登録されるタスクは、Administrators に所属するユーザーがログオンしたタイミングでそのユーザーの権限に依って開始されるように構成されており、そのためには "タスクの実行時に使うユーザーアカウント" を Administrators グループとし、トリガーを "任意のユーザーのログオン時" に設定する必要がありました。必然的に "ユーザーがログオンしているかどうかにかかわらず実行する" が選択できなくなり、非インタラクティブにプログラムを実行する手段が一つなくなったことになります。また今回はスクリプトファイルを作りたくなかったので VBScript の使用も控えました。その条件下で白羽の矢が立ったのが conhost.exe の文書化されていないオプションである --headless です。将来的に削除されうる引数であり、個人的な趣味以外に使うのは推奨されませんが、今回は個人的な趣味の話なので使うことにしました。
conhost.exe を使うことにしましたが、それによって生じる新たな問題もあります。PowerShell スクリプトブロック内の終了コードは PowerShell に戻り値として渡されますが、それを conhost.exe は継承しません。conhost.exe は PowerShell の終了コードに関係なく 0 をタスクスケジューラーに返します。この問題を解決するために外部から ExitCode を指定してプロセスを終了させる Win32 API の WTSTerminateProcess function を使う必要がありました(ちなみに conhost.exe を終了させれば、conhost.exe から起動された PowerShell も終了します)。この関数で PowerShell から親プロセス(conhost.exe)を ExitCode 指定して終了すると、その値がタスクスケジューラーに戻り値として渡され、ResultCode に反映されます。
補足として、スクリプトの最後で LEDKeeper2.exe を強制終了しているのは、ここで終了させないと私の環境ではサインアウト時かシャットダウン時に LEDKeeper2 がクラッシュするためです。ログイン後に RGB ライティング機器を接続したときは手動で Mystic Light を起動してください。
手順
1."MSI Task Host - LEDKeeper2_Host" を無効化、Mystic_Light_Service を手動に
# タスクの無効化
Get-ScheduledTask -TaskName "*LEDKeeper2_Host*" | Disable-ScheduledTask
# サービスの手動化
Get-Service -Name 'Mystic_Light_Service' | Set-Service -StartupType 'Manual'
2.タスクスケジューラから直接実行できるよう Base64文字列 にエンコード
※try・catch
ステートメントにおいてcatch{}
ブロック内で終了例外が起こった時、同じcatch{}
内の以降のコマンドは実行されません(PSVersion:5.1.22621.2506時点)。
※$Call::WTSTerminateProcess()
以降に “条件的に実行されるコマンド・ステートメントがない” または “直後にEXIT
がある” 必要があります。
※WTSTerminateProcess
関数について、
⟪HANDLE hServer(ターミナルサーバーのハンドル)
=WTS_CURRENT_SERVER_HANDLE(C++)
=System.IntPtr.Zero(C#)
=[System.IntPtr]::Zero(PowerShell)
⟫
を設定すれば自分自身のセッションを指定することができます。
# エンコード
$GetBytes = [System.Text.Encoding]::Unicode.GetBytes({
Function Terminate([Int]$ProcessId,[Int]$ExitCode){$Call::WTSTerminateProcess([System.IntPtr]::Zero,$ProcessId,$ExitCode)}
$WTSTerminateProcess = @"
[DllImport("Wtsapi32.dll", SetLastError = false, CharSet = CharSet.Auto)]
public static extern bool WTSTerminateProcess(IntPtr hServer, Int32 ProcessId, Int32 ExitCode);
"@; $Call = Add-Type -Name "WtsApi" -Namespace "Win32" -MemberDefinition $WTSTerminateProcess -PassThru
$ParentID = (Get-CimInstance -Class Win32_Process -Filter "ProcessId = $PID").ParentProcessId
Switch -Exact ((Get-ScheduledTask -TaskName "*LEDKeeper2_Host*").State) {
$null {}
Disabled {}
default {
Get-ScheduledTask -TaskName "*LEDKeeper2_Host*" | Disable-ScheduledTask
Get-Service -Name 'Mystic_Light_Service' | Set-Service -StartupType 'Manual'
Terminate -ProcessId $ParentID -ExitCode 32
EXIT
}
}
if (![System.IO.File]::Exists('C:\Program Files (x86)\MSI\MSI Center\Mystic Light\LEDKeeper2.exe')) {
Terminate -ProcessId $ParentID -ExitCode 1920
EXIT
} elseif ((tasklist /fi "imagename eq LEDKeeper2.exe" /v /fo CSV /NH).Contains("Running")) {
Terminate -ProcessId $ParentID -ExitCode 2182
EXIT
}
$SetId = (Start-Process 'C:\Program Files (x86)\MSI\MSI Center\Mystic Light\LEDKeeper2.exe' -WindowStyle Hidden -PassThru).Id
Start-Sleep -Seconds 100
try {
Get-Process -Name "LEDKeeper2" -ErrorAction Stop
Terminate -ProcessId $SetId -ExitCode 0
[System.Environment]::Exit(!$?)
} catch {
Terminate -ProcessId $ParentID -ExitCode 1067
}
})
$EncodedCommands = [Convert]::ToBase64String($GetBytes)
# デコード
# [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($EncodedCommands))
# この時点で実行できるか確認
# PowerShell -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -EncodedCommand $EncodedCommands; $LastExitCode
3.PowerShell からタスクを登録
※conhost.exe
の引数--headless
は使えなくなる可能性があります(使えるかどうかの確認は姉妹プログラム⦅OpenConsole.exe⦆の Arguments List から確認できるかもしれません)。
# 変数の宣言・代入
$GROUP = "Administrators"
$DirPath = "C:\WINDOWS\System32\WindowsPowerShell\v1.0\"
# タスク登録
$Action = New-ScheduledTaskAction `
-Execute "C:\Windows\System32\conhost.exe" `
-Argument "--headless PowerShell -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -EncodedCommand `"$EncodedCommands`" " `
-WorkingDirectory "$DirPath"
$Trigger = New-ScheduledTaskTrigger -AtLogOn
$Trigger.Delay = [System.Xml.XmlConvert]::ToString((New-TimeSpan -Seconds 3))
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-Compatibility Win8 `
-DisallowHardTerminate `
-DontStopIfGoingOnBatteries `
-DontStopOnIdleEnd `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
-MultipleInstances IgnoreNew `
-Hidden `
-Priority 0 `
-WakeToRun
$Principal = New-ScheduledTaskPrincipal `
-GroupId "$GROUP" `
-RunLevel Highest
$Component = New-ScheduledTask `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-Principal $Principal
Register-ScheduledTask -TaskName "User_LEDKeeper2" -InputObject $Component -AsJob -Force
4.タスクを手動で実行、プロセスに "LEDKeeper2 (32 ビット)" が出現し、少しして消えることを確認
# 一旦、プロセス:LEDKeeper2 を kill
(Get-Process -Name "LEDKeeper2" | Stop-Process -Force -Confirm:$false) 2>&1 > $null
# タスク実行
Get-ScheduledTask -TaskName "User_LEDKeeper2" | Start-ScheduledTask
# PowerShell から確認
$C = {Get-Process -Name "LEDKeeper2" -ErrorAction Stop}
$D = {try{.$C;Write-Host "Process is EXIST $((++$i))" -F:Gre}catch{Write-Host "Process is not EXIST $((++$i))" -F:R}}
1..53|%{.$D}{sleep 2;.$D}
5.タスクの実行結果を確認
※$NumDays
は ミリ秒 を設定します。1日は86400000
です。
$TaskPath = "User_LEDKeeper2"
$NumDays = 172800000
$Count = 6
$GetEvent = Get-WinEvent -MaxEvents $Count -LogName Microsoft-Windows-TaskScheduler/Operational -FilterXPath "*[
System [
Provider [@Name='Microsoft-Windows-TaskScheduler']
and ( EventID=100 or EventID=201 )
and TimeCreated [timediff (@SystemTime) <= $NumDays]
]
and EventData [
Data [@Name='TaskName']
and (Data='\$TaskPath')
]
]"
[string[]]$PropertyQueries = @('Event/EventData/Data[@Name="TaskName"]','Event/EventData/Data[@Name="ResultCode"]')
$PropertySelector = [System.Diagnostics.Eventing.Reader.EventLogPropertySelector]::new($PropertyQueries)
$TaskInvocations = foreach ( $EachEvent in $GetEvent ) {
$TaskName,$ResultCode = $EachEvent.GetPropertyValues($PropertySelector)
Switch -Exact ($EachEvent.Id) {
"100" {$ID = [String]$EachEvent.Id + ' (開始)'}
"201" {$ID = [String]$EachEvent.Id + ' (停止)'}
}
if ([String]::IsNullOrWhiteSpace($ResultCode)) {
$ResultCode = 'N/A'
$ExitCode = 'N/A'
} elseif ([bool]$ResultCode) {
$ResultCode = '0x'+[convert]::ToString($ResultCode,16)
$ExitCode = $ResultCode - 0x80070000
} else {
$ExitCode = $ResultCode
}
[PSCustomObject]@{
タスク名 = $TaskName
発生日時 = $EachEvent.TimeCreated
イベントID = $ID
戻り値 = $ExitCode
終了コード = $ResultCode
}
}
# "戻り値" は今回エンコードしたスクリプトの終了値で、
# "終了コード" は "戻り値" からタスクマネージャーが生成した値になります。
$TaskInvocations | Format-Table
conhost.exe 終了値 | taskschd.msc への戻り値 | エラーコード | 説明 |
---|---|---|---|
1920 | 2147944320 | 0x80070780 | The file cannot be accessed by the system.( LEDKeeper2.exe が削除されている場合に表示) |
0 | 0 | 0 | |
0 | 0 | 0x0 | |
0 | 2147942400 | 0x80070000 | The operation completed successfully. |
32 | 2147942432 | 0x80070020 | The process cannot access the file because it is being used by another process. |
1067 | 2147943467 | 0x8007042b | The process terminated unexpectedly. |
2182 | 2147944582 | 0x80070886 | The requested service has already been started. |
$ExitCode = 2182
$ErrorCode = '0x'+[convert]::ToString(2147942400 + $ExitCode, 16)
$ErrorCode
PowerShell における10進数と16進数の対応表
長さが 8 の倍数である 16 進文字列の最初の桁が 8 以上の場合、数字は負の数として扱われます
10進数 | 16進数 |
---|---|
-2147483648 | 0x80000000 |
-1 | 0xffffffff |
0 | 0x00000000 |
2147483647 | 0x7fffffff |
2147483648 | 0x80000000 |
4294967295 | 0xffffffff |
'0x'+[convert]::ToString(0x80000000 * 0xffffffff, 16)
6.タスクの削除
※Task
が指定されないでUnregister-ScheduledTask -Confirm:$false
が実行されると全タスクが削除されるため、ここではschtasks /delete
を使っています。
schtasks /delete -f /tn "User_LEDKeeper2"
参考
WTSTerminateProcess
・WTSTerminateProcess 関数
・WIn32API ターミナルサーバー上のプロセスを終了させる
・リモート接続先で接続元の情報を C# で取得する
・RDS-Manager.psm1
ScheduledTask
・PowerShellでタスクスケジュール登録
・ScheduledTasks Module | Microsoft Learn
・PowerShellでタスクスケジューラを追加する詳細設定編
・特定グループに所属する任意のユーザーがログインした時、タスクを実行する
・Powershellでタスクスケジューラへタスク登録する際に、遅延時間を指定する方法
conhost.exe --headless
・Palmer Eldrichの3つのブラウザ
・Windows 11のコンソール処理について解説する
Get-WinEvent -FilterXPath
・指定期間のイベントログを取得 [PowerShell]
・Acting on exit code in Windows Task Scheduler
・Task Scheduler - get history information into script variables
Error Codes
・Windowsで表示されるエラーコードの見方
・2.2 Win32 Error Codes
・ErrorCodes_Final