CygwinでGUIのモーダルループに入ると、シグナルが飛んでこなくなるようです。 前からそうなのかもしれませんが、SetConsoleCtrlHandlerでハンドラ(コンソールハンドラと呼ぶことにしましょう)を設定しておけばそちらに飛んだので困らなかったのです。 今はminttyから実行するとコンソールハンドラにも飛んできません。
コマンドプロンプトはWin32の正当なコンソールなので、コマンドプロンプト上で実行している場合、Cygwin上でビルドしたバイナリでもコンソールハンドラが使えます。 しかし、minttyはWin32のコンソールではないらしく(標準入力に対してコンソールAPIを呼ぶとエラーになります)、同じバイナリをmintty上で実行すると、シグナルハンドラだけはなく、コンソールハンドラにも飛んできません。
普通にGUIのプログラムならばそもそもコンソールがないので問題ない(むしろシグナル補足されたら面倒)のですが、CUIのプログラムからGUIの機能を使うときに問題になります。 特に、メッセージを受け取るだけのウィンドウを作り、ウィンドウを表示していない場合、ウィンドウを閉じる操作ができませんから、シグナルが受けられないとモーダルループを抜けられなくなります。 KILLするしかない。
例えば、以下のプログラムをCygwinのGCCで適当にコンパイルして実行するとメッセージボックスが出ます。
適当なプログラムなので、ハンドラの中で発生したイベントを見てないとか、printfを呼んでるとかは目をつぶってもらって、とりあえずコマンドプロンプトから実行した場合、Ctrl+Cを入れるとctrlHandlerに制御が移り、!
を表示して終了します。
ところが、minttyから実行した場合、メッセージボックスのOKボタンを押さないと終了できません。
#define NOMINMAX #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> BOOL WINAPI ctrlHandler(DWORD sig) { fprintf(stderr, "!"); exit(1); } void sighandler(int sig) { fprintf(stderr, "#"); exit(1); } int main(void) { SetConsoleCtrlHandler(ctrlHandler, TRUE); signal(SIGINT, sighandler); MessageBox(NULL, "cyg-sig-test", "cyg-sig-test", MB_OK); return 0; }
いろいろごちゃごちゃやっていると、とにかく時々GetMessageから制御を奪って、シグナルを検査してやればよいようです。 GetMessageを呼んでしまうとメッセージが飛んでくるまで制御が戻ってこないので、マルチスレッドにするか、PeekMessageを使うか、タイマーを使うことになります。シグナルの検査はこんなコードで十分みたい。
{ sigset_t sig; sigpending(&sig); }
これでSIGINTが流れてきて、無事にシグナルハンドラに制御が移ります。
MessageBoxだとこういう芸当はできませんから、適当にコードを書いたものを置いておきます。
cyg-sig-test.cpp (HTML)
主要な部分だけかいつまんで説明すると、WM_CREATEでタイマーを設定し、100msごとにWM_TIMERを発生させて、上記のコードでシグナルをチェックしているだけです。 これでシグナルハンドラを設定しておけば、コマンドプロンプトでもminttyでもシグナルハンドラに制御が移ります。 SetConsoleCtrlHandlerは相変わらずminttyでは使えませんが。 モーダルループを抜けた後でgetcharしているので、ここでEnterでも押せばプログラムが終了します。
ハンドラではウィンドウにWM_CLOSEを突っ込んでいるので、放っておくとWM_DESTROYが発生し、ここでPostQuitMessageが実行されるのでモーダルループが終了する、という算段です。 ハンドラの中で直接WM_QUITを投げてもいいのですが、シグナルハンドラからはPostQuitMessageで大丈夫なのに、コンソールハンドラの方はあらかじめGetCurrentThreadIdでスレッドIDを取得しておいて、PostThreadMessageを使わないとダメでした。 ここでは面倒くさいのでウィンドウオブジェクトをグローバル変数にして、PostMessageで突っ込んでいます。 このあたり、ウィンドウがいくつもある場合はうまく調停しないとイヤなことが起きそうです。
class MainWindow : public WindowBase { protected: virtual LRESULT wndproc(UINT uMsg, WPARAM wp, LPARAM lp); private: Win32Timer tm; }; LRESULT MainWindow::wndproc(UINT uMsg, WPARAM wp, LPARAM lp) { switch (uMsg) { case WM_CREATE: tm.set(get(), 1, 100); return 0; case WM_TIMER: { sigset_t sig; sigpending(&sig); } return 0; case WM_DESTROY: tm.kill(); PostQuitMessage(0); return 0; } return WindowBase::wndproc(uMsg, wp, lp); } MainWindow win; BOOL WINAPI ctrlHandler(DWORD sig) { fprintf(stderr, "!"); win.postMessage(WM_CLOSE); return TRUE; } void sighandler(int sig) { fprintf(stderr, "#"); win.postMessage(WM_CLOSE); } int main(void) { SetConsoleCtrlHandler(ctrlHandler, TRUE); signal(SIGINT, sighandler); win.create(NULL, WS_OVERLAPPEDWINDOW); win.show(); Message msg; msg.doLoop(); getchar(); return 0; }
この状態だと、DOS窓ではコンソールハンドラが呼ばれ、minttyではシグナルハンドラが呼ばれます。 SetConsoleCtrlHandlerをコメントアウトすると、DOS窓とmintty、どちらもシグナルハンドラが呼ばれて正しく動作します。 win.showをコメントアウトしても大丈夫です。
VirtualDubのフレームサーバーを作っているときにこの問題に遭遇しました。 VirtualDubのフレームサーバーはウィンドウにメッセージを投げる形で通信しています。 コマンドラインのプログラムの場合、このウィンドウは表示しないほうがスマートなのでそうしていたのですが、そうするとCtrl+Cで止まらなくなってしまいました。
作ったプログラムにはモードがふたつ、フレームサーバーを閉じると終了するモードと、動作し続けるモードがあり、後者だと止める手段がなくなります。 このときはいろいろ調べて、とりあえずSetConsoleCtrlHandlerを入れておいたのです。 以前はこれで動作していました。
ところが最近、久しぶりにそのプログラムを持ち出して実行したら止まらなくなっていました。 標準入力のカノニカル入力を止め、シグナルの発生も止め、ノンブロッキングモードに設定して、WM_TIMERで入力を吸い上げて0x03だったらPostMessageすれば当然動くのですが、入力を全部吸い上げてしまうのが面白くありません。 あちこちにprintfを入れていると、Ctrl+Cがなんとなく(?)効くことに気づきました。
WM_TIMERを使ってCygwinがシグナルを投げられるスキを作ればいいのかもしれない、と考えて思いつきで実装したのが上のコード。 将来に渡って動く保証はない(きっぱり)。
この方法だと、メッセージを受けるウィンドウをごにょごにょするだけで済み、ほかの部分をほとんどいじらなくていいメリットがあります。 実際、フレームサーバー部分のコードはいじりましたが、最終的にアプリケーションのメインのコードは修正せずに済みました。
Copyright (C) 2015 akamoz.jp
$Id: cyg-modal.htm,v 1.4 2015/10/31 01:54:06 you Exp $