Windowsのシャットダウン日時を取得する

Windowsのシャットダウン時刻を取得する

Windwosのイベントログを取得するには?

Excel VBA質問箱 IV【74506】Re:VBAシャットダウン時刻取得にて、次のコードを発見した。

リスト1 標準モジュール
Public Sub test()    '……(1)'
  Call EnumShutdownDateTime(Range("A1"))
End Sub

Public Sub EnumShutdownDateTime(ByVal r As Range)
  Dim strComputer As String    '……(2)'
 Dim objWMIService As Object
 Dim colLoggedEvents As Object
 Dim objEvent As Object
 Dim offsetRow As Long
 Dim tTimeWritten As Date
  
 strComputer = "."
 Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")    '……(3)'
 Set colLoggedEvents = objWMIService.ExecQuery _
   ("Select * from Win32_NTLogEvent Where Logfile = 'System' and " _
   & "EventCode = '6006'")    '……(4)'
  
 If colLoggedEvents.Count > 0 Then    '……(5)'
  offsetRow = 0
  Application.ScreenUpdating = False
  For Each objEvent In colLoggedEvents    '……(6)'
   tTimeWritten = _
    ConvUTCtoJSC(ParseTimeWritten(objEvent.TimeWritten))
   r.Offset(offsetRow, 0).Value = tTimeWritten
   offsetRow = offsetRow + 1
  Next objEvent
  Application.ScreenUpdating = True
 End If
  
 Set colLoggedEvents = Nothing
 Set objWMIService = Nothing
End Sub

Private Function ParseTimeWritten(ByVal v As Variant) As Date    '……(7)'
 ParseTimeWritten = _
  CDate(Mid(v, 1, 4) & "/" & Mid(v, 5, 2) & "/" & Mid(v, 7, 2) & _
  " " & Mid(v, 9, 2) & ":" & Mid(v, 11, 2) & ":" & Mid(v, 13, 2))
End Function

Private Function ConvUTCtoJSC(ByVal d As Date) As Date    '……(8)'
 ConvUTCtoJSC = DateAdd("h", 9, d)
End Function

まず、(1)の

Public Sub test()
	Call EnumShutdownDateTime(Range("A1"))
End Sub

は、エントリポイント。引数にA1セルを指定して、EnumShutdownDateTimeを呼んでいるだけ。処理の中身はEnumShutdownDateTimeに書いてある。

んで、そのEnumShutdownDateTimeプロシージャ。

まず、冒頭(2)からの6行

Dim strComputer As String
Dim objWMIService As Object
Dim colLoggedEvents As Object
Dim objEvent As Object
Dim offsetRow As Long
Dim tTimeWritten As Date

変数の宣言。

  • strComputer:
    コンピュータ名を格納する。
  • objWMIService:
    WMIサービスオブジェクトを格納する。
  • colLoggedEvents:
    Windowsのイベントログから拾い出したイベントのコレクションを格納する。
  • objEvent:
    colLoggedEventsコレクションの要素を格納する。
  • tTimeWritten:
    イベントから取得した日時を格納する。

だいたいこんな用途。

(3)の

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

では、GetObject関数を用いて、WMIサービスオブジェクトを捕まえているっぽい。

「WMI」というのは、コチラによると、

Windows Management Instrumentation

のことらしい。

(ほとんどすべての) Windows リソースへのアクセス、構成、管理、および監視を可能にする管理基盤

だということなので、こいつをゴニョゴニョすれば、Windowsそのもののあれやこれやを取得できるのでしょう(←雑過ぎる理解)。

GetObjectの引数が長ったらしいけど、1行で書くと、

"winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2"

となり、strComputerの値が「.」(このコンピュータ)なので、要するに

"winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2"

ということになるのだろう。

で、

winmgmts:

ってのは、コチラ

GetObjectの第1パラメータとして指定している“winmgmts:”は、ファイル名ではなく、WMIを表す文字列である(Windows Managementsの略だと思われる)。これはURLの先頭で利用するプロトコルを指定する「http:」のようなキーワードだと考えればよい。通常のファイルではない特別な参照方法である。

とあるので、WMIにアクセスするための特別な指定の仕方のようだ。

んで、

{impersonationLevel=impersonate}

について調べてみると、

Constant value Description Google翻訳
Impersonate 3 Allows objects to use the credentials of the caller. This is the recommended impersonation level for Scripting API for WMI calls. オブジェクトが呼び出し元の資格情報を使用することができます。これはWMI呼び出しのためのスクリプティングAPI推奨偽装レベルです。

とのこと(コチラをどうぞ)。WMIにアクセスするにあたってのセキュリティレベルか何かを指定しているのだろう。

つまり、objWMIServiceに格納するWMIサービスオブジェクトを取得するのには、次の2点、すなわち、

  • ディレクトリの指定にはwinmgmts:という特殊な文字列を使う。
  • impersonationLevelを指定する

ことが必要だということ。

このあたり、WSHあたりを勉強した方がいいのかもな。

まだまだ続く。嫌になるなあ。

(4)の

Set colLoggedEvents = objWMIService.ExecQuery _
   ("Select * from Win32_NTLogEvent Where Logfile = 'System' and " _
   & "EventCode = '6006'")

今度は、(3)で取得したWMIサービスオブジェクトのExecQueryメソッドを用いて、colLoggedEventsにイベントの集合を格納している(のだと思う。自分でもよく分かっていないw)。

ExecQueryメソッドの引数は、1行で書くと

"Select * from Win32_NTLogEvent Where Logfile = 'System' and EventCode = '6006'"

ということなので、つまりはSQLのクエリを投げているってことですか???

「Win32_NTLogEvent」というテーブルから、「LogFile」フィールドの値が「'System'」かつ「EventCode」フィールドの値が「6006」のレコードを引っ張って来い、ということでしょうか。

SQLは全くの素人なので、間違えていたら親切に教えてくだされ。

とまれ、これでcolLoggedEventsにはイベントログの集合体が格納されていることになるのだろう。

「EventCode」の「6006」ってのは「シャットダウン」を指すので、colLoggedEventsにはシャットダウンイベントに関する記録が格納されていることになる(んだよね?)。

(5)の

If colLoggedEvents.Count > 0 Then

による条件判定は、colLoggedEventsコレクション(?)のCountプロパティを用いている。Countプロパティの値が0よりも大きいということは、何らかのイベントが取得できているということだからこうしているのだろう。

(6)からの6行(実質は5行)

For Each objEvent In colLoggedEvents
  tTimeWritten = _
    ConvUTCtoJSC(ParseTimeWritten(objEvent.TimeWritten))
  r.Offset(offsetRow, 0).Value = tTimeWritten
  offsetRow = offsetRow + 1
Next objEvent

では、colLoggedEventsコレクションの中身について、まず、

tTimeWritten = ConvUTCtoJSC(ParseTimeWritten(objEvent.TimeWritten))

でコレクションの要素(objEvent)のTimeWrittenプロパティから取得した日時(を表す文字列)を、(7)のParseTimeWritten関数を用いて標準の日付時刻形式に変換し、その上で(8)のConvUTCtoJSC関数で日本の時刻に変換している。

TimeWrittenプロパティから取得できる値が、例えば「2017年11月4日の15時37分00秒」だと20171104153700という何とも気の利かない形であること、及び

UTC協定世界時)」とは、世界各地の標準時を決めるときの基準となる「世界標準時」のことです。たとえば日本の標準時(JST)は「UTC」よりも 9時間進んでいるため「UTC+09:00」と表示されます。

ということ(コチラより)から、二重に変換せねばならん。ああめんどくさい。

まあ、そんなわけで、

tTimeWritten = ConvUTCtoJSC(ParseTimeWritten(objEvent.TimeWritten))

を通過した後、変数tTimeWrittenには、めでたくシャットダウンイベントの起きた日時が格納されているのだ。

あとは、

r.Offset(offsetRow, 0).Value = tTimeWritten

で1回目ならoffsetRowが「0」なのでA1セルにtTimeWrittenに格納されている日時を書き込み、

offsetRow = offsetRow + 1

でoffsetRowをインクリメントして次のループへ。

よって、A1セルからスタートして、下へ下へシャットダウン日時を書き込んで行くことになる。

ついでに、(7)の

Private Function ParseTimeWritten(ByVal v As Variant) As Date
 ParseTimeWritten = _
  CDate(Mid(v, 1, 4) & "/" & Mid(v, 5, 2) & "/" & Mid(v, 7, 2) & _
  " " & Mid(v, 9, 2) & ":" & Mid(v, 11, 2) & ":" & Mid(v, 13, 2))
End Function

は、colLoggedEventsコレクションの要素のTimeWrittenプロパティから取得した日付時刻文字列をDate型の形に整形してDate型に変換するFunction。Mid関数を使った非常に原始的な処理だ。

あと、(8)の

Private Function ConvUTCtoJSC(ByVal d As Date) As Date
 ConvUTCtoJSC = DateAdd("h", 9, d)
End Function

は単純至極。TimeWrittenプロパティで取得した時刻を9時間進めることによって、UTCをJSCに変換している。

実行結果

エントリポイントであるtestプロシージャを実行すると、

f:id:akashi_keirin:20171104174622j:plain

こんなふうに、シャットダウン日時が上から下へと書き込まれた。

おわりに

WMIとか、初めて手を出したのだが、このあたりをしっかり勉強したらかなり面白いことができそうな気がしてきた。

今回は、人のコードを自分なりに説明しただけ、という形になってしまったが、ちょっとづつ分かってきたので、自分なりに手を加えていこうと思う。

@akashi_keirin on Twitter

追記

Function化しました。

akashi-keirin.hatenablog.com