たとえ途中で再起動と管理者権限が必要でも、自動化をあきらめたらそこで試合終了ですよ…？

# 前書き
Windowsで自動化していると面倒なケースに遭遇します。

それは再起動を挟みかつ管理者権限が必要なケースです。
たとえば、以下のようなケースになります。

・Aというアプリをインストールしたあとに再起動をしてBというアプリをインストールする
・WindowsUpdateでインストールしたあとに再起動をしてコントロールパネルの設定を実施する

今回は、Windowsにおいて再起動を挟んで管理者権限で実行するスクリプトの書き方について検討してみます。


# ログインの自動化
再起動後に自動的にログインを行う方法はマイクロソフトのサポート情報として紹介されています。

**How to turn on automatic logon in Windows**
https://support.microsoft.com/en-us/help/324737/how-to-turn-on-automatic-logon-in-windows

これをPowerShellで記載すると下記のようになります。

**ログインの自動化を有効**

```powershell
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
$DefaultUsername = "IEUser"
$DefaultPassword = "Passw0rd!"
Set-ItemProperty -LiteralPath $RegPath 'AutoAdminLogon' -Value "1" -type String 
Set-ItemProperty -LiteralPath $RegPath 'DefaultUsername' -Value "$DefaultUsername" -type String 
Set-ItemProperty -LiteralPath $RegPath 'DefaultPassword' -Value "$DefaultPassword" -type String
```

**ログインの自動化を無効**

```powershell
$RegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
Set-ItemProperty -LiteralPath $RegPath "AutoAdminLogon" -Value "0" -type String
```

# ログイン後のスクリプトの実行方法
## 案1:Run,RunOnceレジストリを利用する
ログイン後のスクリプトの実行を行うために、Run/RunOnceというレジストリキーが存在しています。

**Run and RunOnce Registry Keys** 
https://docs.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys

下記のレジストリキーに文字列のエントリを追加することで、そのレジストリの値で指定されたパスのスクリプトが実行されます。

 - HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
 - HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
 - HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce
 - HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce


HKEY_LOCAL_MACHINEとHKEY_CURRENT_USERの違いは全てのユーザで行うか、特定のユーザで行うかの違いです。

Runに登録されたスクリプトはログインのたびに実行されるようになります。
RunOnceに登録されたスクリプトは１度実行したあとに削除されます。

ただし、標準ユーザでログインした場合や、セーフモードでログインした場合は実行されません。
セーフモードでも実行したい場合はエントリ名の先頭に「*」を付与します。

RunOnceのエントリはスクリプト実行前に削除されます。これをスクリプト実行後に変更したい場合はエントリ名の先頭に「!」を指定します。

これらのキーのいずれかから実行されるプログラムは、キーの下に登録されている他のプログラムの実行を妨げるため、実行中にキーに書き込むべきではありません。
また、RunOnce下でエントリを継続的に再作成すべきではないです。

これらのスクリプトから実行される実行の権限ですが、HKEY_CURRENT_USERのRunOnceを除きすべて管理者権限がありません。
つまり、HKEY_LOCAL_MACHINEのレジストリの変更や、インストールなどの権限の厳しい作業を行うことはできません。

これらをまとめると以下のようになります。

|キー|実行時のエントリ|実行時の管理者権限|
|:---|:---|:----|
|HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run|残る|なし|
|HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run|残る|なし|
|HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce|消える|あり|
|HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce|消える|なし|

### 実行権限の確認方法と使用例
実行権限を確認するには以下のようなスクリプトを、Run,RunOnceで実行して検証します。

```powershell:chkfunc.ps1
Param(
    [String]$path
)
#https://serverfault.com/questions/95431/in-a-powershell-script-how-can-i-check-if-im-running-with-administrator-privil
$cur = [Security.Principal.WindowsIdentity]::GetCurrent()
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal($cur)
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$tm = Get-Date -Format "yyyy/MM/dd HH:mm:ss"

$txt = $tm + " " + $cur.Name + " " + $isAdmin

Add-Content -LiteralPath $path -Value $txt

```

このスクリプトではパラメータで与えられたファイルに実行時の時刻と、実行ユーザ、管理者権限の有無を登録します。
このスクリプトをRunOnce,Runを使用して実行するには以下のようにレジストリに登録します。

```powershell
$v = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File c:\share\chkfunc.ps1 c:\share\hkcu_run.txt'
New-ItemProperty -LiteralPath 'HKCU:Software\Microsoft\Windows\CurrentVersion\Run' -Name 'test1' -PropertyType 'String' -Value $v -force

$v = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File c:\share\chkfunc.ps1 c:\share\hkcu_runonce.txt'
New-ItemProperty -LiteralPath 'HKCU:Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'test2' -PropertyType 'String' -Value $v -force


$v = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File c:\share\chkfunc.ps1 c:\share\hklm_run.txt'
New-ItemProperty -LiteralPath 'HKLM:Software\Microsoft\Windows\CurrentVersion\Run' -Name 'test3' -PropertyType 'String' -Value $v -force

$v = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File c:\share\chkfunc.ps1 c:\share\hklm_runonce.txt'
New-ItemProperty -LiteralPath 'HKLM:Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'test4' -PropertyType 'String' -Value $v -force

```

Windows7の環境で2回再起動を繰り返した結果は以下のようになります。

```text:hkcu_run.txt
2019/09/04 13:44:56 IEWIN7\IEUser False
2019/09/04 13:48:32 IEWIN7\IEUser False
```

```text:hkcu_runonce.txt
2019/09/04 13:44:55 IEWIN7\IEUser False
```

```text:hklm_run.txt
2019/09/04 13:44:55 IEWIN7\IEUser False
2019/09/04 13:48:32 IEWIN7\IEUser False
```

```text:hklm_runonce.txt
2019/09/04 13:44:53 IEWIN7\IEUser True
```

runonceの場合、１度のみ実行されていることと、hklm以下のRunOnceのみが管理者権限で実行できることが確認できます。

なお、この結果はWindows10でも同じでした。

### 使いどころ
再起動後、一度だけ実行すればよいという状況では有効かもしれません。
もし、複数の再起動がある場合は後述のタスクスケジューラを利用したほうがいいでしょう。

## 案2:タスクスケジューラを利用する
Windowsが標準で搭載しているタスクスケジューラを利用することで、**「ログイン後」**に**「最上位の特権」**でスクリプトを**自動で**実行することが可能です。

タスクスケジューラは管理者権限のあるプロセスからであれば以下のコマンドで利用できます。

```
schtasks
```

ログイン後に実行するタスクを登録するスクリプトは以下になります

```
schtasks /create /tn タスク名 /tr スクリプトのパス /sc onlogon /rl highest /F
```

/sc オプションでどのタイミングで実行するかを指定します。今回はログイン後なのでonlogonとなります。
/rl hightが最上位の特権をあらわしており、/Fで同名のタスクがあった場合に上書きを行います。


また、、タスクを削除するには以下のようになります。

```
schtasks /Delete /tn "MyTask" /F
```

### サンプル
今回は再起動後にTortoiseSVNをインストールしてみる例を考えてみます。
一般的なインストーラの自動化方法については[ココ](https://qiita.com/mima_ita/items/414451b78062151e1595)に記載しています。

**再起動前に実行するスクリプト settask.ps1**

```powershell:settask.ps1
# 管理者権限で実行

# 再起動後に実行するタスクを登録
# 最近のバージョンはRegister-ScheduledTaskが使えそう.
$run = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File C:\share\after_restart.ps1'
schtasks /create /tn "MyTask" /tr $run /sc onlogon /rl highest /F
if ($lastexitcode -ne 0) {
    exit
}

# 検証用のイベントログを登録できるようにSource「test」を追加しておく
New-EventLog -LogName Application -Source test -ea SilentlyContinue

# ログインの自動化
# http://get-cmd.com/?p=4679
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
$DefaultUsername = "IEUser"
$DefaultPassword = "Passw0rd!"
Set-ItemProperty -LiteralPath $RegPath 'AutoAdminLogon' -Value "1" -type String 
Set-ItemProperty -LiteralPath $RegPath 'DefaultUsername' -Value "$DefaultUsername" -type String 
Set-ItemProperty -LiteralPath $RegPath 'DefaultPassword' -Value "$DefaultPassword" -type String


Restart-Computer -Force
```

**再起動後に実行するスクリプト after_restart.ps1**

```powershell:after_restart.ps1
# なんか処理をする
Start-Process msiexec.exe -Wait -NoNewWindow -ArgumentList '/i c:\share\TortoiseSVN-1.12.2.28653-x64-svn-1.12.2.msi /passive /qn /norestart ADDLOCAL=F_OVL,DefaultFeature,MoreIcons,CLI,CrashReporter,UDiffAssoc,DictionaryENGB,DictionaryENUS'

# ログインの自動化を解除
$RegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
Set-ItemProperty -LiteralPath $RegPath "AutoAdminLogon" -Value "0" -type String

# タスクを消す
schtasks /Delete /tn "MyTask" /F

# イベントログに記録
Write-EventLog -LogName Application -Source test -EventID 2 -EntryType Information -Message "再起動後のスクリプトの実行しました"
```

再起動前に実行するスクリプトではタスクスケジューラにタスクを登録して、自動ログインを有効にしたのち、再起動をおこなっています。

再起動後に実行するスクリプトではインストーラを実行したのち、タスクを削除して自動ログインを無効にしています。

今回は、古い環境を考慮してschtasks コマンドを利用しましたが新しいPowerShellが使える環境では[Register-ScheduledTask](https://docs.microsoft.com/en-us/powershell/module/scheduledtasks/register-scheduledtask?view=win10-ps)の使用も検討可能です。

# リモートからの実行
リモートから再起動を制御できるようになると、同時に複数の端末に対しての自動化が可能になります。
今回は下記の記事の「方法４：schtasks (タスクスケジューラ)」と「方法５：PowerShell (WinRM)」を利用しまして再起動後にTortoiseSVNをリモートからインストールする例を考えてみました。

**別端末(Windows)のプログラムを標準機能でリモート起動する方法まとめ**
https://qiita.com/0829/items/5518256b348521ac358c




```powershell
$password = ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("WORKGROUP\IEUser", $password)
$computerName = "IEWin7"

# 再起動前管理者権限がいらないリモート側の処理
Invoke-Command -ComputerName $computerName -Credential $cred -ScriptBlock {
    Get-ChildItem -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall","HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | %{$_.GetValue("DisplayName")}
    
    # ここで管理者権限で実行させたいスクリプトを書いてしまうのも手
    Set-Content -Path c:\share\autologin.ps1 -Value @"
Set-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' 'AutoAdminLogon' -Value 1 -type String 
Set-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' 'DefaultUsername' -Value IEUser -type String 
Set-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' 'DefaultPassword' -Value Passw0rd! -type String
"@

}

# リモート側で再起動前に実行させたい管理者権限のあるタスクを登録して実行
$run = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File c:\share\autologin.ps1'
schtasks /create  /S IEWIN7 /U IEUser /P Passw0rd!  /TN MyTaskInit /sc ONCE /sd 1900/01/01 /st 00:00 /tr $run /rl highest
schtasks /run /S IEWIN7 /U IEUser /P Passw0rd! /TN MyTaskInit
schtasks /delete /S IEWIN7 /U IEUser /P Passw0rd! /TN MyTaskInit /F


# リモート側で再起動後にさせたいタスクを登録
$run = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File C:\share\after_restart.ps1'
schtasks /create /tn "MyTask"  /S IEWIN7 /U IEUser /P Passw0rd!  /tr $run /sc onlogon /rl highest /F

# 再起動(応答まで、すごく時間がかかる)
Restart-Computer -ComputerName IEWin7 -Credential  $cred -Wait -Force

# 再起動後管理者権限がいらないリモート側の処理
Invoke-Command -ComputerName $computerName -Credential  $cred -ScriptBlock {
    # 管理者権限がいらないリモート側の処理
    Get-ChildItem -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall","HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | %{$_.GetValue("DisplayName")}
}

```

(1)まず、WinRM経由で管理者権限のいらない再起動前の処理を行います。
このときに、管理者権限が必要な処理用のスクリプトを操作対象の端末に作成してます。

(2)操作対象の端末のタスクスケジューラに管理者権限の必要なスクリプトを実行するタスクを追加して実行します。

(3)再起動後に行うスクリプトをタスクスケジューラに登録します。

(4)再起動を実施します。

(5)再起動後、管理者権限の必要のない処理をWinRM経由で行います。


# まとめ
今回は自動化を行う上で、再起動があって、管理者権限も必要であるというケースに対応する方法を検討してみました。

昔、偉い人が「あきらめたらそこで試合終了ですよ…？」といいましたが、一見無理っぽいケースにおいても結構やれる方法はあったりします。

# 参考
**How to turn on automatic logon in Windows**
https://support.microsoft.com/en-us/help/324737/how-to-turn-on-automatic-logon-in-windows

**Automatic Logon in Windows 10 using PowerShell**
http://get-cmd.com/?p=4679

**Run and RunOnce Registry Keys**
https://docs.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys

**Reboot and Resume PowerShell Script**
https://www.codeproject.com/Articles/223002/Reboot-and-Resume-PowerShell-Script

**Windows で任意のコマンド（タスク）を自動実行する (schtasks)**
https://maku77.github.io/windows/admin/schtasks.html

**Continue Your Automation To Run Once After Restarting a Headless Windows System**
https://cloudywindows.io/post/continue-your-automation-to-run-once-after-restarting-a-headless-windows-system/

**In a PowerShell script, how can I check if I'm running with administrator privileges?**
https://serverfault.com/questions/95431/in-a-powershell-script-how-can-i-check-if-im-running-with-administrator-privil

**別端末(Windows)のプログラムを標準機能でリモート起動する方法まとめ**
https://qiita.com/0829/items/5518256b348521ac358c