PCのスタートアップ時に LEDKeeper2 がクラッシュするのをどうにかする

### 背景
　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 を起動してください。

### 手順

#### １．"MSI Task Host - LEDKeeper2_Host" を無効化、Mystic_Light_Service を手動に 
```PowerShell:PowerShell：管理者
# タスクの無効化
Get-ScheduledTask -TaskName "*LEDKeeper2_Host*" | Disable-ScheduledTask

# サービスの手動化
Get-Service -Name 'Mystic_Light_Service' | Set-Service -StartupType 'Manual'
```

#### ２．タスクスケジューラから直接実行できるよう Base64文字列 にエンコード

:::note warn
**※**```try・catch```ステートメントにおいて```catch{}```ブロック内で終了例外が起こった時、同じ```catch{}```内の以降のコマンドは実行されません（PSVersion：5.1.22621.2506時点）。
**※**```$Call::WTSTerminateProcess()```以降に “<INS>条件的に実行されるコマンド・ステートメントがない</INS>”&ensp;または&ensp;“<INS>直後に```EXIT```がある</INS>” 必要があります。
**※**```WTSTerminateProcess```関数について、
**⟪**```HANDLE hServer（ターミナルサーバーのハンドル）```＝```WTS_CURRENT_SERVER_HANDLE（C++）```＝```System.IntPtr.Zero（C#）```＝```[System.IntPtr]::Zero（PowerShell）```**⟫**
を設定すれば自分自身のセッションを指定することができます。
:::

```PowerShell: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
```

#### ３．PowerShell からタスクを登録

:::note warn
**※**```conhost.exe```の引数```--headless```は使えなくなる可能性があります（使えるかどうかの確認は姉妹プログラム｟[OpenConsole.exe](https://github.com/microsoft/terminal/blob/main/src/host/ConsoleArguments.cpp)｠の Arguments List から確認できるかもしれません）。
:::

```PowerShell:PowerShell：管理者
# 変数の宣言・代入
$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
```

#### ４．タスクを手動で実行、プロセスに "LEDKeeper2 (32 ビット)" が出現し、少しして消えることを確認
```PowerShell:PowerShell：管理者
# 一旦、プロセス: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}
```

#### ５．タスクの実行結果を確認

:::note warn
**※**```$NumDays```は <INS>ミリ秒</INS> を設定します。１日は```86400000```です。
:::

```PowerShell:PowerShell
$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. |
|   |   |   |   |


```PowerShell:エラーコード算出式
$ExitCode = 2182
$ErrorCode = '0x'+[convert]::ToString(2147942400 + $ExitCode, 16)
$ErrorCode
```

<details><summary> PowerShell における10進数と16進数の対応表</summary>

[長さが 8 の倍数である 16 進文字列の最初の桁が 8 以上の場合、数字は負の数として扱われます](https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_numeric_literals?view=powershell-7.4#working-with-binary-or-hexadecimal-numbers)

| 10進数 | 16進数 |
|------------:|:----------------:|
| -2147483648 | ```0x80000000``` |
|          -1 | ```0xffffffff``` |
|           0 | ```0x00000000``` |
|  2147483647 | ```0x7fffffff``` |
|  2147483648 | ```0x80000000``` |
|  4294967295 | ```0xffffffff``` |
~~~PowerShell:PowerShell
'0x'+[convert]::ToString(0x80000000 * 0xffffffff, 16)
~~~
</details>

#### ６．タスクの削除

:::note warn
**※**```Task```が指定されないで```Unregister-ScheduledTask -Confirm:$false```が実行されると全タスクが削除されるため、ここでは```schtasks /delete```を使っています。
:::

```PowerShell:cmd or PowerShell：管理者
schtasks /delete -f /tn "User_LEDKeeper2"
```


### 参考
```WTSTerminateProcess```
・[WTSTerminateProcess 関数](https://learn.microsoft.com/ja-jp/windows/win32/api/wtsapi32/nf-wtsapi32-wtsterminateprocess)
・[WIn32API ターミナルサーバー上のプロセスを終了させる](https://s-kita.hatenablog.com/entry/20120720/1342806719)
・[リモート接続先で接続元の情報を C# で取得する](https://tech.tanaka733.net/entry/how-to-retrieve-terminal-serssion)
・[RDS-Manager.psm1](https://github.com/kendalvandyke/sqlpowerdoc/blob/master/Modules/RDS-Manager/RDS-Manager.psm1#L268)

```ScheduledTask```
・[PowerShellでタスクスケジュール登録](https://www.vwnet.jp/windows/hs12r2/schdule/task.htm)
・[ScheduledTasks Module | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks)
・[PowerShellでタスクスケジューラを追加する詳細設定編](https://syanaise-soudan.com/scheduledtasksettingsset-powershell/)
・[特定グループに所属する任意のユーザーがログインした時、タスクを実行する](https://www.takosuke.net/タスクを『任意のユーザのログオン時』に実行さ/)
・[Powershellでタスクスケジューラへタスク登録する際に、遅延時間を指定する方法](https://teratail.com/questions/370898)

```conhost.exe --headless```
・[Palmer Eldrichの3つのブラウザ](https://news.mynavi.jp/article/pebble_in_the_window-150/)
・[Windows 11のコンソール処理について解説する](https://ascii.jp/elem/000/004/141/4141418/)

```Get-WinEvent -FilterXPath```
・[指定期間のイベントログを取得 [PowerShell]](https://blog.goo.ne.jp/limited_terra/e/fbc81a1b7cc0bc22464103ce41003cc4)
・[Acting on exit code in Windows Task Scheduler](https://superuser.com/questions/1278486/acting-on-exit-code-in-windows-task-scheduler)
・[Task Scheduler - get history information into script variables](https://stackoverflow.com/questions/46452001/task-scheduler-get-history-information-into-script-variables)

```Error Codes```
・[Windowsで表示されるエラーコードの見方](https://ascii.jp/elem/000/001/432/1432965/)
・[2.2 Win32 Error Codes](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d)
・[ErrorCodes_Final](https://smsagent.files.wordpress.com/2015/07/errorcodes_final.xlsx)

### 以上