目次へ戻る §24へ戻る §26へ進む

25. Windows 7 のコンソール・アプリで logoff 時に長い処理を行うには

2011.11.2 (鈴)

25.1 はじめに

本章は 第 12 章 の広い意味での続編として C# を扱います。 ここで見るようなソース・コードの色分けは 前回 設定した Emacs で見ることができます。

今の Windows は特に開発環境を入れなくても C# コンパイラ csc を備えています。 環境変数 PATH の設定を変更してパスを通すかわりに,$HOME/bin や /usr/local/bin などに下記のようにシンボリック・リンクを作ることもできます。 パスを通す先のコマンド数が少ないときは,この方が簡単で後の管理も楽です。

01:~/bin$ ln -s /cygdrive/c/Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe .

下記のようにして C# プログラムをコンパイルできます。 プログラム中の文字列定数やコメントには UTF-8 が使えます。 コンパイラから UTF-8 として認識され,内部的に正しく Unicode 文字列として扱われます。 UTF-8 ですから,ソース・ファイルを Cygwin のテキスト操作コマンドで問題なく扱うことができます。

01:~/src$ csc LogoffActionTest.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

01:~/src$

ただし,端末プログラムとして mintty を使っている場合は,内部の Unicode 文字列が Windows にとってのデフォルト・エンコーディングである Shift_JIS に変換されて ANSI 系 API で出力されますから, コンパイラからの日本語メッセージが化けます。 第 24.2.1 節で説明したように csc LogoffActionTest.cs | nkf などとしてください。

25.2 Windows 7 の logoff/shutdown 処理

Windows の logoff および shutdown の処理は XP までと Vista 以降で大きく変わっています。 詳細は Application Shutdown Changes in Windows Vista を参照してください。とりわけ問題なのは次の点です。

Windows Vista will also not allow console applications or applications that have no visible top-level windows to block shutdown.
(私訳) Windows Vista はまた, コンソール・アプリケーション,または何らトップレベル・ウィンドウを表示していないアプリケーションが, シャットダウンをブロックすることを許しません。

これは Windows 7 も同じです。 ただし,トップレベル・ウィンドウを表示していないアプリケーションについては, ShutdownBlockReasonCreate 関数を使って,ブロックの理由を暗転画面で表示するようにすれば, WM_ENDSESSION のハンドラ内でセッション終了の進行を止めることができます。 C# では,例えば,次のようにします (この例では Thread.Sleep(15*1000) でわざと 15 秒間スリープしています。実際にはここに何か長い処理が入ります)。

    protected override void WndProc (ref Message msg) {
        if (msg.Msg == WM_ENDSESSION) {
            try {
                ShutdownBlockReasonCreate (Handle,
                                           "ファイルに書き出しています。");
                using (StreamWriter sw = new StreamWriter ("poi.txt")) {
                    sw.WriteLine (DateTime.Now);
                    Thread.Sleep (15*1000);
                    sw.WriteLine (DateTime.Now);
                }
            } finally {
                ShutdownBlockReasonDestroy (Handle);
            }
        } else {
            base.WndProc (ref msg);
        }
    }

しかし,コンソール・アプリケーションについては,少なくとも表向きは回避方法がありません。

25.3 コンソール・アプリで logoff/shutdown をブロックする方法

コンソール・アプリケーションで logoff/shutdown をブロックするための正攻法はありませんが, 次の方法を使えば,事実上,問題を回避できそうです。

以下は,この考えに基づく試作クラスです。

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

// logoff 処理を行うための隠しウィンドウ
public class LogoffActionForm: Form {
    string LogoffMessage;
    Action LogoffAction;

    const int WM_ENDSESSION = 0x16;

    [DllImport ("user32.dll", CharSet=CharSet.Unicode)]
        extern static bool ShutdownBlockReasonCreate (IntPtr hWnd, string str);

    [DllImport ("user32.dll")]
        extern static bool ShutdownBlockReasonDestroy (IntPtr hWnd);

    protected override void WndProc (ref Message msg) {
        if (msg.Msg == WM_ENDSESSION) {
            try {
                ShutdownBlockReasonCreate (Handle, LogoffMessage);
                LogoffAction ();
                ShutdownBlockReasonCreate (Handle, "お待たせしました。");
                Thread.Sleep (600); // 0.6 秒だけ表示する。
            } finally {
                ShutdownBlockReasonDestroy (Handle);
            }
        } else {
            base.WndProc (ref msg);
        }
    }

ShutdownBlockReasonCreate 関数と ShutdownBlockReasonDestroy 関数をネイティブ DLL から extern static メソッドとして取り込みます。 WndProc メソッドでは WM_ENDSESSION に対して logoff/shutdown の終了処理を行います。 終了処理本体 LogoffAction と表示用メッセージ LogoffMessage は,このクラスの利用者が供給します。

    // このフォームのウィンドウをはじめから隠す (ややトリッキー)。
    protected override void OnLoad (EventArgs ea) {
        BeginInvoke (new Action (() => Hide ()));
    }

    protected LogoffActionForm (string message, Action action) {
        LogoffMessage = message;
        LogoffAction = action;

        // Hide するまでの一瞬でも姿を隠すようにする。
        Opacity = 0.0;
        ShowInTaskbar = false;
    }

実装上の都合で設ける隠しウィンドウですから,あくまで人の目に触れないようにします。

そこはかとなく存在を主張するようにしたければ,OnLoad メソッドを無くし, コンストラクタでの透明度指定を Opacity = 0.1; 程度にしてみてください。 幽霊のように現れます。
    // logoff 時に message を表示して action を時間無制限で実行する。
    // この作業を行う新しいスレッドを返す。
    public static Thread Start (string message, Action action) {
        Form form = new LogoffActionForm (message, action);
        Thread th = new Thread (() => {
                Application.Run (form);
            });
        th.Start ();
        return th;
    }
}

Start はこのクラスが定義する唯一の公開メソッドです。 隠しウィンドウのインスタンスを作り,新しいスレッドでそれを走らせます。

Thread のコンストラクタ引数にある () => { … } は C# 3.0 で導入されたラムダ式です。 Action 型の引数を Start メソッドの第1引数ではなく第2引数にしている点にも注意してください。 これは実引数として長いラムダ式をあまり不自然にならずに,じかに書けるように意図してのことです。 実例は下記のコードを見てください。
// LogoffActionForm の使用例
public class Test {
    static void Main () {
        // logoff 時にはファイルを書き出す。
        LogoffActionForm.Start ("ファイルを書き出しています。", () => {
                using (StreamWriter sw = new StreamWriter ("poi.txt")) {
                    sw.WriteLine (DateTime.Now);
                    Thread.Sleep (15*1000);
                    sw.WriteLine (DateTime.Now);
                }
            });

        // それまではただ延々と羊を数える。
        for (int i = 1;; i++) {
            Console.WriteLine ("羊が" + i + "匹");
            Thread.Sleep (3000);
        }
    }
}
これは隠しウィンドウ LogoffActionForm の使用例です。 終了メッセージの文字列と,終了処理のラムダ式を Start の実引数として与えます。 ここでのコンソール・アプリケーションとしての処理は,ただ延々と羊を数えてコンソールに表示します。

25.4 実行例

ソース・ファイルは ここ にあります。 コンソール・アプリケーションですから,/t:winexe オプションを与えずに下記のようにコンパイルします。

01:~/src$ csc LogoffActionTest.cs

作った LogoffActionTest.exe をデスクトップに置いてダブルクリックで実行します。

ここでスタートメニューからログオフすると 5 秒ほどの猶予の後,画面が暗転し, 終了メッセージ「ファイルを書き出しています」が表示されます。 そのまましばらく待つと,メッセージが一瞬だけ「お待たせしました」に代わった後,ログオフします。



再びログオンすると,終了処理で作られたファイル poi.txt がデスクトップにあります。 確かに期待されたとおりの時刻が記録されています。



「平成」表記は,コントロール パネル → 時計、言語、および地域 → 日付、時刻または数値の形式の変更 → 追加の設定... → 日付 → カレンダーの種類 で「和暦」を選択することで設定できます。

25.5 おわりに

本章では Vista 以降の Windows の一つの問題に一応の解決策を示しました。 また,Cygwin の各コマンドと Emacs のもとで C# がちょうど Unix での C 言語のように, いつでもさしあたりすぐ使え,OS の用意するライブラリを利用できる手軽で本格的なプログラミング言語となることを示しました。

ちなみに本章での C# コードの括弧の置き方は Vala の流儀にならったものです。 C# としては模範的ではありませんから書き方の手本にはしないでください。

Copyright (c) 2011 OKI Software Co., Ltd.