雑な hinananoha

やさしいせかいをさがして三千里

起動時とシャットダウン時にメールを飛ばしてくれるPowershell スクリプト

お久しぶりです

どうもお久しぶりです。こんな記事書くの久々なhinananohaです。
なんかTwitterアカウントが新しく?なりましたのでよろしくお願いします。

twitter.com

Introduction

我が家はメインサーバとしてWindows Serverを動かしているのですが、唐突に以下の機能を実装してみたくなりました。

  • シャットダウン時とスタートアップ時にメールを飛ばす
  • スタートアップ時は前回のシャットダウンに関する情報を提供する

こんな感じです。
ただメール飛ばすだけは実は簡単なのですが、後者の場合はイベントログを取得する必要があるため、まあどうしようかな、みたいな気持ちになっていました。

実際に作ってみた

作ったもの

まずさっそく結果から。この通りになります。

シャットダウン時にメールを飛ばすスクリプト

$mail = @{
    from = "";
    to = "";
    smtp_server = "";
    smtp_port = 587;
    user = "";
    password = "";
}
$date = Get-Date -Format R
$subject = "[THANATOS]System Shutdown Start."
$body ="
THANATOS System Shutdown is received.
System is shutdown at $Date .

This mail is auto-sent from Windows Server.
"

$client = New-Object Net.Mail.SmtpClient($mail["smtp_server"], $mail["smtp_port"])

# GmailはSMTP + SSLで送信する
$client.EnableSsl = $true

# SMTP Authのため、認証情報を設定する
$client.Credentials = New-Object Net.NetworkCredential($mail["user"], $mail["password"])

$msg = New-Object Net.Mail.MailMessage($mail["from"], $mail["to"], $subject, $body)

$client.Send($msg)

スタートアップ時にメールを飛ばすスクリプト

function CheckConnectivity($times) {
    $return = 0
    try{
        $Result = Get-NetConnectionProfile -IPv4Connectivity Internet -erroraction stop
    }catch [System.Management.Automation.ActionPreferenceStopException]{
        Start-Sleep -s 10
        $times = $times + 1
        if($times -eq 30){
            return 1
        }
        $return = CheckConnectivity($times)
     }
     return $return
}


function SendStartupMail {
    $check = CheckConnectivity(0)
    $startup_start_date = Get-Date -Format R ([DateTime](Get-EventLog -LogName System -InstanceId 12 -Newest 1 | Select-Object ReplacementStrings).ReplacementStrings[6])
    $startup_finish_date = Get-Date -Format R
    $last_shutdown_status = (Get-EventLog -LogName System -InstanceId 20 -Newest 1 | Select-Object ReplacementStrings).ReplacementStrings[0]
    if($last_shutdown_status -eq 'true'){
        $last_shutdown_time = Get-Date -Format R ([DateTime](Get-EventLog -LogName System -InstanceId 13 -Newest 1 | Select-Object ReplacementStrings).ReplacementStrings[0])
        $last_shutdown_info = (Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1 | Select-Object ReplacementStrings).ReplacementStrings
        $reason = $last_shutdown_info[2]
        $reason_code = $last_shutdown_info[3]
        $reboot = $last_shutdown_info[4]
        $comment = $last_shutdown_info[5]

        $body ="
THANATOS System is start and connect network.
System startup start at $startup_start_date .
System startup completed at $startup_finish_date .

Last Shutdown Status: $last_shutdown_status ($reboot)
Last Shutdown Time : $last_shutdown_time
Reason : $reason ($reason_code)
Comment : $comment


This mail is auto-sent from Windows Server.
"
    } else {
                $body ="
THANATOS System is start and connect network.
System startup start at $startup_start_date .
System startup completed at $startup_finish_date .

Last Shutdown Status: $last_shutdown_status
Last Shutdown is Bad status. You must set reason.


This mail is auto-sent from Windows Server.
"
    }
    if($check -eq 0){
        $mail = @{
            from = "";
            to = "";
            smtp_server = "";
            smtp_port = 587;
            user = "";
            password = "";
        }
        $subject = "[THANATOS]System Start"

        $client = New-Object Net.Mail.SmtpClient($mail["smtp_server"], $mail["smtp_port"])

        # GmailはSMTP + SSLで送信する
        $client.EnableSsl = $true

        # SMTP Authのため、認証情報を設定する
        $client.Credentials = New-Object Net.NetworkCredential($mail["user"], $mail["password"])

        $msg = New-Object Net.Mail.MailMessage($mail["from"], $mail["to"], $subject, $body)

        $client.Send($msg)

    } else {
        Start-Sleep -s 300
        SendStartupMail
    }
}

SendStartupMail
        


コードが汚い気がしますが気のせいです
これでメールを飛ばすと、こんな感じになります。(スタートアップ時)



ではどんな感じで作ったかを書いていきます。

Powershellでメールを飛ばす

これはいろいろな需要がある関係で各所に記事が転がっています。
我が家はSMTP over SSL/TLSなので、「Gmailにメールを送りたい」と同じ奴を見ればだいたいわかります。という事でこちらの記事を参考にさせていただきました。

thinkami.hatenablog.com

この中の System.Net.Mail の方を使いました。

シャットダウン時・スタートアップ時にPowershellスクリプトを実行

グループポリシー(gpedit.msc)で設定します。
グループポリシーの コンピューターの構成 -> Windowsの設定 -> スクリプト(スタートアップ/シャットダウン) で設定できます。
わざわざPowershellスクリプトを実行する専用のタブがあるのでそちらで設定ができます。

以上です。

Powershell スクリプトでイベントを取得し、必要な情報を取得する

イベントログを取得するPowershellコマンドは Get-EventLog です。
Get-EventLog そのものの使い方や、それをわかりやすくまとめた記事は以下の通りです。

gallery.technet.microsoft.com

www.atmarkit.co.jp

これらを用いて、必要なイベントを取得します。

前回のシャットダウンに関するイベントを取得し、そこから必要な情報を取得する

これが面倒くさい(というよりはこれが理解できるまで時間がかかりました)です。
まず、前回のシャットダウンに関する情報でほしいものは以下の通りです。

  1. 前回のシャットダウンは正常だったか (STOPエラーや強制終了、電源喪失などによるものではない)
  2. もし前回のシャットダウンが正常であれば、いつシャットダウンしたか
  3. 前回のシャットダウンは「シャットダウン」だったのか、「再起動」だったのか
  4. 前回のシャットダウンの理由は何だったのか (Windows Serverはデフォルトで「シャットダウンのイベント追跡ツール」が有効になっている)

このあたりです。では、実際のイベントログを見ながら、起動時のイベントログがどういう形になっているのか見ます。

起動時に発生するイベントログの解析

( この項目はただひたすらイベントログを解析する話なのでコードを見たい方は飛ばして構いません )

今回見るイベントログはイベント ビューアーの「Windows ログ」 -> 「システム」のログです。

まず、Windowsが起動して一番最初に記録されるイベントは 「Windowsの起動開始時刻」に関するイベントです。

ログの名前 システム
ソース Kernel-General
イベントID 12
オペコード 情報

内容はこんな感じです。

オペレーティング システムはシステム時刻 2018-01-31T05:41:44.498537000Z に起動しました。

次に(場合によっては仮想化ベースのセキュリティに関するログが出ることもありますが)ブートオプションのログが出ます。(おそらくセーフモードとかに関するものだと思います)
その次にbootmgrのユーザ入力待ちに関するログ、そしてその次にお待ちかね、「前回のシャットダウンのステータス」に関するログが出ます。

ログの名前 システム
ソース Kernel-Boot
イベントID 20
オペコード 情報

内容はこんな感じです。

前回のシャットダウンの成功状態は true でした。前回のブートの成功状態は true でした。

このログが終わると、ブートの種類、ブートメニューポリシー、FWからのブートメトリックの報告が終わって、いろいろなものが起動し始めます。

さて、次にシャットダウン時の情報を取得します。

シャットダウンの情報に関するイベントログの解析

次に、シャットダウン時に関する情報を探します。

シャットダウンに関する一番最初のログは、一部の例外を除いて一番最初に記録されるのは「シャットダウンのイベント追跡ツール」によるものです。
これは、有効になってない場合も、また、Windows Updateなどによる強制的なシャットダウン|再起動の場合も適当な理由で記録されます。

ログの名前 システム
ソース User32
イベントID 1074
オペコード 情報

内容はこんな感じです。

次の理由で、プロセス C:\Windows\System32\RuntimeBroker.exe (THANATOS) は、ユーザー THANATOS\Administrator の代わりに、コンピューター THANATOS の 再起動 を始めました: アプリケーション: メンテナンス(計画済)
 理由コード: 0x84040001
 シャットダウンの種類: 再起動
 コメント: 

C:\Windows\System32\RuntimeBroker.exeはいわゆる一般的なユーザの操作によるシャットダウン・再起動を行うときに呼び出されるものです。
例えば、Windows Updateから再起動をかけるとこうなります。(※Windows Server 2016の場合)

次の理由で、プロセス C:\Windows\system32\svchost.exe (THANATOS) は、ユーザー NT AUTHORITY\SYSTEM の代わりに、コンピューター THANATOS の 再起動 を始めました: オペレーティング システム: Service pack(計画済)
 理由コード: 0x80020010
 シャットダウンの種類: 再起動
 コメント: 

他にも、デバイスドライバのインストール後に再起動を要求されたりすると、こんな感じになります。(※Windows 10 Pro build 1709の場合)

次の理由で、プロセス msiexec.exe は、ユーザー NT AUTHORITY\SYSTEM の代わりに、コンピューター FARAWAY の 再起動 を始めました: この理由のタイトルが見つかりません
 理由コード: 0x80030002
 シャットダウンの種類: 再起動
 コメント: 'Sound Blaster Z-Series' の構成を完了または続行するために、Windows インストーラーによりシステムの再起動が実行されました。

これを使えばシャットダウンの理由はわかりそうです。

さて、ここから様々シャットダウンが行われます。すべてのシャットダウンが終わり、いちばん最後に記録されるのは、シャットダウン完了に関するログです。

ログの名前 システム
ソース Kernel-General
イベントID 13
オペコード 情報

内容はこちら。

オペレーティング システムはシステム時刻 2018-01-31T19:32:45.977382500Z にシャット ダウンします。

これでログが出そろいました。次にこれをPowershell スクリプトで取得して情報を得る方法です。

Powershell スクリプトで特定のイベントの一番新しいログの中の特定の情報を取得する

まず、Powershell スクリプトで特定のイベントを取得するのに使うのは「イベントID」です。これを使ってイベントを抽出します。
この方法は2種類あります。なぜか(もしかしたら私の解釈が間違っているのかもしれない)前者の方法で取得できない時があるので、その時は後者の方法を試してください。

Get-EventLog -LogName <LogName> -InstanceId <ID> -Newest 1
Get-EventLog -LogName <LogName> | Where-Object{$_.EventID -eq <ID>} | Select-Object -First 1

< LogName > にはログの名前、 < ID > にはイベントIDを入れます。例えば、シャットダウンに関する情報を表示するログはこのように出力されます。

PS C:\Windows\system32> Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1

   Index Time          EntryType   Source                 InstanceID Message
   ----- ----          ---------   ------                 ---------- -------
    8968 2 01 04:32    Information User32                 2147484722 次の理由で、プロセス C:\Windows\system32\winlog...

さて、ではここからイベントログの中にある情報を取得します。特定のイベントログに関係する詳細な情報を出すときは、Format-List *でできます。

PS C:\Windows\system32> Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1 | Format-List *


EventID            : 1074
MachineName        : FARAWAY
Data               : {}
Index              : 8968
Category           : (0)
CategoryNumber     : 0
EntryType          : Information
Message            : 次の理由で、プロセス C:\Windows\system32\winlogon.exe (FARAWAY) は、ユーザー NT AUTHORITY\SYSTEM の代わりに、コンピューター FARAWAY の 電源を切る を始めました: この理由のタイトルが見つかりません
                      理由コード: 0x500ff
                      シャットダウンの種類: 電源を切る
                      コメント:
Source             : User32
ReplacementStrings : {C:\Windows\system32\winlogon.exe (FARAWAY), FARAWAY, この理由のタイトルが見つかりません, 0x500ff...}
InstanceId         : 2147484722
TimeGenerated      : 2018/02/01 4:32:38
TimeWritten        : 2018/02/01 4:32:38
UserName           : NT AUTHORITY\SYSTEM
Site               :
Container          :

ほしいのはMessageの中ですが、ここから情報を取得するのは骨が折れます。そこで、特にシステム系のイベントの場合は、ReplacementStringsという項目を見ます。
この項目は、該当のログにおいて特に個々のイベントで置き換わりそうな情報が記されているっぽいです。試しにその情報を取得してみます。

イベントログの中から特定の項目を取り出すときはSelect-Objectを使います。

PS C:\Windows\system32> Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1 | Sele
ct-Object ReplacementStrings

ReplacementStrings
  ------------------
{C:\Windows\system32\winlogon.exe (FARAWAY), FARAWAY, この理由のタイトルが見つかりません, 0x500ff...}

このように表示されます。さて、この項目なんですが、実は「PSCustomObject」と呼ばれる、その名の通り、オブジェクトです。(詳細は省きますので知りたい方はぜひ調べてみてください)
なので、このようにすると中身が出てきます。

PS C:\Windows\system32> (Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1 | Sel
ect-Object ReplacementStrings).ReplacementStrings
C:\Windows\system32\winlogon.exe (FARAWAY)
FARAWAY
この理由のタイトルが見つかりません
0x500ff
電源を切る

NT AUTHORITY\SYSTEM

そして、この項目、実は配列になってます。型を取得するとSystem.String[]と出てきますので、添え字をつけて取得することができるわけでございます。

PS C:\Windows\system32> (Get-EventLog -LogName System | Where-Object{$_.EventID -eq 1074} | Select-Object -First 1 | Sel
ect-Object ReplacementStrings).ReplacementStrings[0]
C:\Windows\system32\winlogon.exe (FARAWAY)

このように。さて、これで準備はできました。あとはもうコードを書くだけです。

インターネットにつながっているか確かめる

ここまで読んだ方は「スタートアップ時にメールを飛ばす」ほうのコードによくわからん関数が入ってることに気が付くと思います。

これは、インターネットにつながっていない状態でこのスクリプトが実行されたとき、「送れなかったーすまん」って言って何もしないで終わることを防ぐためのものです。

ここでインターネットにつながっているかどうかを確認して、つながっていなかったら10秒待って再確認する……というのを30回繰り返してます。
さらに、30回繰り返しても(=5分)つながらない場合は5分間待ってもう一回SendStartupMail関数を呼び出し、接続状態を確認する……というのをこいつは延々と繰り返してます。

これに関してはGet-NetConnectionProfileで調べてみてください(投げやり)

今後やりたいこと

これで最低限のことはできるようになったのですが、いろいろ調べていくうちに、実はシャットダウン時の地点ですでにそのシャットダウンに関する情報が取得できるんじゃないかとか、電源ボタンを押すタイプのシャットダウンは挙動が違うとか、まあいろいろと見つかりまして、さらに良い感じのスクリプトを作りたいですね

久しぶりにこんなに長い記事書いた

なんかアウトプットしなければ(使命感)となったのと、この手の記事が見つからず……
ぜひ試してみてください!