簡単であるはずのことが実はとても難しかったということはよくあります。たとえば、タンポポを抜こうとしたことがありますか (以前、Scripting Guys の 1 人が "自宅の屋根" にタンポポが生えているのを見つけました)。それでは、ビデオデッキの時計が "12:00" と点滅を繰り返すのを止めようとしたことはありますか (使用されているビデオデッキの 25% が今なお "12:00" と点滅していると推定されています)。また、別の Scripting Guy は、フォルクスワーゲン パサートは、ヘッドライトを変えるより、新車を一から組み立てたほうが簡単だと気付きました。
ユーザーがドメインに最後にログオンした時刻の判定という、一見簡単そうに見える Active Directory タスクにも同じことが言えます。最終ログオン時刻の判定は、ユーザーまたはコンピュータがログオンした最終時刻を示す Active Directory の属性 lastLogon を使用すれば、ごく簡単であるはずです。単にこの属性の値を取得してレポートすることが、難しいわけありません。
意外にも、この作業はきわめて困難なのです。1 つの理由は、ログオンした最終日付と時刻の Active Directory への保存方法にまつわる困難さです。しかし、この問題はコーディングおよび計算を工夫することによって解決できます。もっと大きな問題は、lastLogon 属性がドメイン コントローラ間で複製されないことです。新しいユーザーがドメイン コントローラ A にログオンしたとしましょう。このユーザーの最終ログオン時刻を要求するスクリプトを作成し、スクリプトがドメイン コントローラ B に接続したとします。不思議なことに、このユーザーがたった今ログオンしたことは明らかなのに、スクリプトは、このユーザーはログオンしていないという応答を返します。

なぜ、Active Directory は嘘をつくのでしょうか。実際には嘘をついているわけではなく、ドメイン コントローラ B は、そのユーザーがドメイン コントローラ A にログオンした事実を把握していないのです。lastLogon 属性はドメイン内で複製されないため、このユーザーがドメイン コントローラ B にログオンしない限り、ドメイン コントローラ B はユーザーの最終ログオン時刻を把握できないのです。事実、ユーザーの最終ログオン時刻を判定するためには、ドメインのすべてのドメイン コントローラから lastLogon 属性を取得し、それらすべての値を比較することで、真の最終ログオン時刻を判定しなければなりません。
気が遠くなりますね。
ユーザーがドメインにログオンした最終時刻の判定方法を 1 日に何度も尋ねられ、辟易としている Scripting Guy は、(お願いだから質問しないでください。きっと、知りたくないと思いますよ) Windows Server 2003 をありがたく思うでしょう。Windows 2003 の Active Directory スキーマにも lastLogon 属性が存在しますが、やはりドメイン コントローラ間で複製されません。しかし、それで良いのです。なぜなら、新しい属性 lastLogonTimestamp がスキーマに導入されたからです。この属性も、ユーザーがドメインにログオンした最終時刻を追跡しますが、この新しい属性は、(信じられないことに) ドメイン コントローラ間で複製されます。ユーザーがログオンした最終時刻が知りたい場合は、スクリプトを作成して任意のドメイン コントローラに接続すれば、どのドメイン コントローラでも値はすべて同じになります。
まるで奇跡ではありませんか。
この記事では、Windows Server 2003 ドメインへのユーザーの最終ログオン時刻を返すスクリプトを示します。しかし、その前に、まだ複雑な要素がいくつか残っていることに注意してください。第一に、最終ログオン タイムスタンプは通常、ユーザーの真の最終ログオン時刻をレポートするものではないと認識することが重要です。なぜでしょうか。大勢のユーザーが 1 日に何度もログオンとログオフを繰り返すことを想像してみてください。ユーザーがログオンするたびに、その情報がドメイン全体に複製される必要があります。これにより大量の複製トラフィックが発生しますが、これにはほとんど意味がありません。一般的に、必要とされる情報は、"停滞" アカウント、つまりここ数週間ログオンしていないユーザーについてです。たいていの場合、各ユーザーの最終ログオン ステータスについての最新レポートは不要です。このため、lastLogonTimestamp は 14 日ごとに複製されます。これにより、複製トラフィックが制限されますが、ユーザーの lastLogonTimestamp は 14 日間も更新されないままです。
注: このことが問題になるのであれば、各ドメイン コントローラに接続してそのユーザーの lastLogon 属性を取得すればよいだけです。lastLogon 属性はドメイン内で複製されませんが、ユーザーがログオンするたびに、認証を行うドメイン コントローラで更新されます。しかし、"過去 2 週間ログオンしていないユーザーはいるか" という問いに答えるためには、lastLogonTimestamp で十分です。 |
lastLogonTimestamp が 100% 正確でないため、スクリプトの作成が少し簡単になります。これから説明するように、lastLogonTimestamp を私たちが理解できる日付/時刻の値に変換するためには計算が必要です。コンピュータとドメイン コントローラ間の時差の調整が必要だとしたら、計算はさらに複雑になります。しかし、時差は気にしなくてもよいでしょう。すでにご承知のように、最終ログオン時刻は 14 日間も更新されません。それなら、たった数時間の時差を気にかける必要はないでしょう。
もう 1 つの複雑な要素は、lastLogonTimestamp が 64 ビット整数で保存されるということです。lastLogonTimestamp のクエリを実行すると、2005 年 5 月 15 日 8:05 AM のような日時は返されません。代わりに、1601 年 1 月 1 日からユーザーの最終ログオン時刻までに経過した時間が 100 ナノ秒間隔の数として返されます (信じていませんね。でも、こんな作り話ができるほど Scripting Guys は賢くありません)。結果として、コードの大部分が、この奇妙な 64 ビット整数値を取得して日付と時刻に変換する作業にかかわるものです。
注: 数年前、American National Standards Institute (ANSI) によって日付カウント方式が採用されたのではないかと思い当たった方もいると思います。この方式では 1600 年 12 月 31 日が Day 0 とされています。つまり、1601 年 1 月 1 日は、公式な歴史の "初日" であり、その後の日付と時刻はすべて、1601 年 1 月 1 日 0 時以降に経過したナノ秒の数に基づきます (ちなみに、この日は月曜日です)。ANSI 10 進法日付と呼ばれるこれらの日付は、もともと COBOL プログラミング言語用に設計されたものが、Windows およびその他のオペレーティング システムに継承されたものです。 |
ところで、VBScript は lastLogonTimestamp によって返される 64 ビット整数を処理できないことはご存知でしたか。先に述べておくべきでしたが、64 ビット整数は、VBScript でサポートされていません。ただし、この問題は、次のように回避できます。ADSI の IADsLargeInterger インターフェイスを使用してこの情報を 2 つの 32 ビット整数に分解すれば、VBScript でこれら 2 つの整数を問題なく処理できます。したがって、やはり lastLogonTimestamp 属性を使用できます。単に、2 つの 32 ビット整数を足して VBScript が処理できる単一値を得るという追加ステップを使用すればよいだけです。

ここまでの内容は、ご理解いただけましたか (皆さんを遠ざけてしまっていないか不安になったのです)。警告が先走ってしまいが、ユーザーの最終ログオン時刻を返すスクリプトは、実はそれほど大変ではありません。次に、実際のスクリプトを示します。
Set objUser = GetObject("LDAP://cn=Ken Myer, ou=Finance, dc=fabrikam, dc=com")
Set objLastLogon = objUser.Get("lastLogonTimestamp")
intLastLogonTime = objLastLogon.HighPart * (2^32) + objLastLogon.LowPart
intLastLogonTime = intLastLogonTime / (60 * 10000000)
intLastLogonTime = intLastLogonTime / 1440
Wscript.Echo "Last logon time: " & intLastLogonTime + #1/1/1601#
スクリプトの冒頭はとても簡単です。単に Active Directory のユーザー アカウントにバインドし、Get メソッドを使用して lastLogonTimestamp を取得し、オブジェクト参照 objLastLogon を持つ IADsLargeInteger オブジェクトに値を保存します。
注: ADSI の良いところは、一般的に、使用するインターフェイスを宣言しなくて良いことです。すでにお気付きかもしれませんが、IADsLargeInteger オブジェクトのインスタンスの作成は行いません。この作業は ADSI 自身で行います。また、ユーザー オブジェクトの使用についても明示的に宣言しません。ADSI は、それを判断する機能を備えています。 |
このあたりが、少し危ないところです。IADsLargeInteger オブジェクトには、64 ビット整数の上位 32 ビットを格納する HighPart と、整数の下位 32 ビットを格納する LowPart という 2 つのプロパティがあります。これらを組み合わせて 1 つの値にするには、次のコード行を使用します。
intLastLogonTime = objLastLogon.HighPart * (2^32) + objLastLogon.LowPart
計算について難しく考えすぎないでください。単に HighPart 時刻に 2 の 32 乗を掛け、LowPart を足しただけです。難しい数学の問題を解くのが趣味だというのでない限り、この式が正しいと信用してください。
信じられないかもしれませんが、この 1 行のコードで、ユーザーの最終ログオン時刻が実際に判明するのです。唯一の問題は、最終ログオン時刻が 1601 年 1 月 1 日からユーザーの最終ログオンまでに経過した 100 ナノ秒間隔の数として返されることです。これは、次のような値です。
1.27588712492538E+17
まあ、よいでしょう。
この値を、もう少し処理しやすいものに変換する必要があります。ご存知のように、1 秒は 1,000,000,000 ナノ秒です。したがって、1 秒は、10,000,000 個の 100 ナノ秒間隔で構成されています (10,000,000 x 100 = 1,000,000,000)。前述のように、lastLogonTimestamp は時刻を 100 ナノ秒間隔で測定するため、この値を知る必要があります。計算をさらに一歩進めると、1 分は 600,000,000 個の 100 ナノ秒間隔で構成されています。
心配しないで。少し落ち着きましょう。大丈夫ですか。それでは、先へ進みます。この値は何の目的で使用するのでしょうか。この値がわかれば、このコード行を使用して、1601 年 1 月 1 日からユーザーの最終ログオン時刻までに何分経過したかを計算することができます (60 秒に 10,000,000 個の 100 ナノ秒間隔を掛けていることに注目してください)。
intLastLogonTime = intLastLogonTime / (60 * 10000000)
1 日は 24 時間、つまり 1,440 分であるため、このコード行によって、経過日数を知ることができます。
intLastLogonTime = intLastLogonTime / 1440
もちろん、2 個の等式ではなく、1 個の等式を使うこともできます。単に、2 個に分けた方が多少理解しやすいのでは、と思っただけです。
経過日数がわかれば、その日数を 1601 年 1 月 1 日に足すことで、意味のある日付/時刻の値を生成できます (たとえば、経過日数が 3 日であることが判明したとします。1601 年 1 月 1 日に 3 を足すことで、最終ログオン時刻は 1601 年 1 月 4 日であるとわかります)。この足し算を行うコードは次のとおりです。
Wscript.Echo "Last logon time: " & intLastLogonTime + #1/1/1601#
次のような結果が返されます。
Last logon time: 4/25/2005 2:54:09 PM
本当に複雑ですね。しかし、この方法の利点は、ドメイン内のどのドメイン コントローラに接続してもこの情報を得ることができることです。前述のように、Windows 2000 でユーザーがログオンした最終時刻を判定するには、今なお、この仰々しい計算をすべて行わなければならないのです。さらに、すべてのドメイン コントローラに接続して lastLogon 属性の値を取得し、それらすべての値を比較しなければならないのです。それに比べれば、lastLogonTimestamp は大きな前進です。
さて、次は、点滅する "12:00" を何とかできれば、良い商売となること請け合いです。