X11: NetWM (4) - ping

ping
クライアントのウィンドウが正しく動作しているか、それとも制御できない状態になっているかを、ウィンドウマネージャが確認することで、応答不可能な場合は、ウィンドウマネージャが自動でクライアントを強制終了できます。
ウィンドウのプロパティ
この ping の機能を使いたい場合は、各クライアントウィンドウに対して、以下のプロパティを設定する必要があります。

_NET_WM_PID プロパティに、このウィンドウを所有しているクライアントの「プロセス ID」をセットします。
(type = "CARDINAL", format = 32)

プロセス ID は、unistd.h の getpid 関数で取得できます。

また、この場合、ICCCM 仕様の WM_CLIENT_MACHINE プロパティに、ホスト名をセットする必要があります。
このプロパティがない場合、プロセスの終了ではなく、XKillClient() によるクライアントの強制終了になります。

また、_NET_WM_PING の ClientMessage を受け取れるようにするため、ICCCM 仕様の WM_PROTOCOLS プロパティに、_NET_WM_PING アトムを追加します。
ClientMessage イベント
ウィンドウマネージャがクライアントウィンドウの応答を確認したいタイミングで、以下の ClientMessage イベントが送られてきます。

window = クライアントのウィンドウ
message_type = (Atom) WM_PROTOCOLS
format = 32
data.l[0] = (Atom) _NET_WM_PING
data.l[1] = タイムスタンプ
data.l[2] = クライアントウィンドウ

クライアントがこのイベントを受け取った場合は、読み込んだイベントの xclient.window の値を、ルートウィンドウに変更して、同じイベント構造体をそのまま XSendEvent() で送信して、ウィンドウマネージャに応答を返します。
(XSendEvent の引数は、NetWM (1) で説明しているのと同じ値を指定します)

※window 以外のメンバ値は、変更してはいけません。

ウィンドウマネージャは、一定時間待っても、クライアントからこの応答が返らなかった場合は、中身のクライアントウィンドウが制御できない状態になっているものと判断し、必要に応じてユーザーに確認メッセージを表示した後、クライアントウィンドウの _NET_WM_PING プロパティから取得したプロセス ID を使って、クライアントのプロセスを強制終了することができます。
動作
このイベントは、主に、ウィンドウ装飾の閉じるボタンなどが押されて、クライアントに WM_DELETE_WINDOW が送られた後に来ます。

ユーザーがウィンドウを閉じようとした時に、制御できない状態のクライアントをそのまま放置させることなく、自動で強制終了することができます。

ウィンドウマネージャの実装によって違いがあるかもしれませんが、OpenBox の場合は、以下のような動作になります。

  • 閉じるボタンを押した時、応答が返れば、何もしない。
  • 閉じるボタンを押した時に応答が返らなかった場合、数秒後に再び _NET_WM_PING が来る。
    その後の数回の _NET_WM_PING に対しても応答が返らなかった場合、ウィンドウを強制終了させるかの確認メッセージが表示される。
  • 確認メッセージが表示されている間も、定期的に _NET_WM_PING が来る。
    その間に応答が返った場合、確認メッセージは自動的に閉じられる。
  • ユーザーが確認メッセージに対して、キャンセル (強制終了しない) を選択した場合、ウィンドウタイトルが「応答なし」になって、定期的に _NET_WM_PING が来る。
    その間に応答が返った場合、ウィンドウタイトルが元に戻る。
  • ユーザーが強制終了を選択した場合、クライアントが強制終了する。

一度応答が返らなかったとしても、しばらくすると応答が返ってくる場合があるので、ウィンドウマネージャは常に応答を確認し続ける必要があります。
プログラム
左ボタン押しで、_NET_WM_PING 時の応答を返す/返さないの切り替えができます。
それ以外のボタンで、終了します。
※閉じるボタンを押した時は、ウィンドウを閉じません。

ウィンドウ装飾の閉じるボタンを押した時の、_NET_WM_PING のイベントを確認してください (応答が返れば、何も起こらない)。

また、一度左ボタンを押して、応答を返さないようにした後、再び閉じるボタンを押した時の _NET_WM_PING の動作を確認してください (閉じるボタンを押した後、しばらく時間を置いてみてください)。

$ cc -o run d11-ping.c util.c -lX11

<d11-ping.c>
#include <stdio.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include "util.h"

static void _set_client_machine(Window win)
{
    char m[64],*pstr;
    XTextProperty tp;

    gethostname(m, 64);
    m[63] = 0;

    printf("hostname: %s\n", m);

    pstr = m;

    if(XmbTextListToTextProperty(g_disp.disp,
        &pstr, 1, XStdICCTextStyle, &tp) == Success)
    {
        XSetWMClientMachine(g_disp.disp, win, &tp);

        XFree(tp.value);
    }
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    Atom atom_ping,atoms[2];
    int fsend = 1;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);
    init_locale();

    win = create_test_window2(disp, 200, 200, 0, ButtonPress);

    //プロパティ

    atom_ping = GET_ATOM("_NET_WM_PING");

    atoms[0] = g_disp.atoms[ATOM_WM_DELETE_WINDOW];
    atoms[1] = atom_ping;
    
    set_prop32_array(win, g_disp.atoms[ATOM_WM_PROTOCOLS], XA_ATOM, atoms, 2);

    set_prop32_one(win, GET_ATOM("_NET_WM_PID"), XA_CARDINAL, getpid());

    _set_client_machine(win);

    //イベント

    XMapWindow(disp, win);

    while(1)
    {
        XNextEvent(disp, &ev);

        switch(ev.type)
        {
            case ButtonPress:
                if(ev.xbutton.button == Button1)
                {
                    fsend ^= 1;
                    printf("* send_ping: %s\n", (fsend)? "yes": "no");
                }
                else
                    goto END;
                break;
            case ClientMessage:
                if(ev.xclient.message_type == g_disp.atoms[ATOM_WM_PROTOCOLS])
                {
                    if(ev.xclient.data.l[0] == g_disp.atoms[ATOM_WM_DELETE_WINDOW])
                        printf("[WM_DELETE_WINDOW]\n");
                    else if(ev.xclient.data.l[0] == atom_ping)
                    {
                        //_NET_WM_PING

                        printf("[_NET_WM_PING] timestamp(%ld) window(0x%lx)\n",
                            ev.xclient.data.l[1], ev.xclient.data.l[2]);

                        if(!fsend) break;

                        ev.xclient.window = g_disp.root;

                        XSendEvent(disp, g_disp.root, False,
                            SubstructureNotifyMask | SubstructureRedirectMask, &ev);
                    }
                }
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}