g_spawn_async_with_pipes を使って、実行結果を出力する方法、、、未完

GLib で子プロセスを起動するには、g_spawn 系の関数をつかうと便利なのですが、子プロセスを非同期的に実行させながら、子プロセスの出力と終了ステータスを取得するのが意外に面倒だったので、忘れないように備忘録。

子プロセス終了時に出力をやめると、バッファリングして書き出される標準出力の文字列が取得できないとか、イベントループを回さないとコールバック関数が呼ばれないとか、非常に悩みました。

今回は、STDOUT が閉じた時にイベントループを終了するという酷い処理になっています。本来ならコールバック関数それぞれにイベントループの参照を追加して、コールバック関数内で参照を減らして、ゼロになったら終了するのが良いのだろうな〜なんて思っていますが、よくわからないので動けば良いというプログラムです。

ビルド時に、毎回 CFLAGS や LDFLAGS を指定するのが面倒なので、Makefileを書きました。

CFLAGS = -pthread -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include
LDFLAGS = -pthread -lgio-2.0 -lgobject-2.0 -lgmodule-2.0 -lgthread-2.0 -lrt -lglib-2.0  

all: spawn

clean:
	rm -f spawn *~

CFLAGS と LDFLAGS は、

$ pkg-config --cflags gio-2.0
-pthread -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include  
$ pkg-config --libs gio-2.0
-pthread -lgio-2.0 -lgobject-2.0 -lgmodule-2.0 -lgthread-2.0 -lrt -lglib-2.0  

の結果をそのまま使っています。

肝心のソースは spawn.c という名前で保存してください。

#include <glib.h>

static void
cb_child_watch (GPid pid, gint status, GMainLoop *loop)
{
	g_print ("Close Pid Status: %d\n", status);
	g_spawn_close_pid (pid);
}

static gboolean
cb_out_watch (GIOChannel *channel, GIOCondition cond, GMainLoop *loop)
{
	gchar *string;
	gsize size;
	GError *error = NULL;

	if (cond == G_IO_HUP)
	{
		g_print ("Close STDOUT\n");
		g_io_channel_unref (channel);
		g_main_loop_quit (loop);
		return FALSE;
	}

	g_io_channel_read_line (channel, &string, &size, NULL, &error);
	g_assert_no_error (error);
	g_print (string);
	g_free (string);

	return TRUE;
}

static gboolean
cb_err_watch( GIOChannel *channel, GIOCondition cond, GMainLoop *loop)
{
	gchar *string;
	gsize  size;
	GError *error = NULL;

	if (cond == G_IO_HUP)
	{
		g_print ("Close STDERR\n");
		g_io_channel_unref (channel);
		return FALSE;
	}

	g_io_channel_read_line (channel, &string, &size, NULL, &error);
	g_assert_no_error (error);
	g_print (string);
	g_free (string );

	return TRUE;
}

main(int argc, char **argv )
{
	gchar *command_line = "ls";
	gchar **args;
	GPid pid;
	gint out, err;
	GIOChannel *out_ch, *err_ch;
	GError *error = NULL;
	GMainLoop *loop;

	g_type_init ();
	loop = g_main_loop_new (NULL, TRUE);

	g_shell_parse_argv (command_line, NULL, &args, &error);
	g_assert_no_error (error);

	g_spawn_async_with_pipes (NULL, args, NULL,
		G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL,
		NULL, &pid, NULL, &out, &err, &error);
	g_assert_no_error (error);
	g_strfreev (args);
	g_child_watch_add (pid, (GChildWatchFunc)cb_child_watch, loop);

	out_ch = g_io_channel_unix_new (out);
	err_ch = g_io_channel_unix_new (err);

	g_io_add_watch (out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, loop);
	g_io_add_watch (err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, loop);

	g_main_loop_run (loop);
}

最初の関数 cb_child_watch (GPid pid, gint status, GMainLoop *loop) が、子プロセスの終了を監視するコールバック関数です。単純に、終了ステータスを表示して子プロセスを終了しています。g_pid_close_pid()はLinuxでは不要らしいのですが、一応記述しています。

static void
cb_child_watch (GPid pid, gint status, GMainLoop *loop)
{
	g_print ("Close Pid Status: %d\n", status);
	g_spawn_close_pid (pid);
}

次の関数 gboolean cb_out_watch (GIOChannel *channel, GIOCondition cond, GMainLoop *loop) と gboolean cb_err_watch( GIOChannel *channel, GIOCondition cond, GMainLoop *loop) は、それぞれ標準出力と標準エラー出力を監視して、入力があった (GIOCondition が G_IO_IN の)ときとパイプが閉じられた(GIOCondition が G_IO_HUP の)ときに処理してます。
G_IO_HUP のときは、パイプが閉じられたことを表示して g_io_channel_unref() でチャネルを閉じ、それ以外(G_IO_IN)のときは、g_io_channel_read_line() でパイプから一行読み込み、そのまま出力しています。

これらのGIOFunc()型の関数の返り値は gboolean で、GIOChannel を削除(unref)したときには FALSE を返し、それ以外では TRUE を返すようにしてください。

2つの関数の違いは、gboolean cb_out_watch () だけで、 G_IO_HUP のときに、GMainLoop *loop を終了しているところです。これが酷いところです。

static gboolean
cb_out_watch (GIOChannel *channel, GIOCondition cond, GMainLoop *loop)
{
	gchar *string;
	gsize size;
	GError *error = NULL;

	if( cond == G_IO_HUP )
	{
		g_print ("Close STDOUT\n");
		g_io_channel_unref (channel);
		g_main_loop_quit (loop);
		return FALSE;
	}

	g_io_channel_read_line (channel, &string, &size, NULL, &error);
	g_assert_no_error (error);
	g_print (string);
	g_free (string);

	return TRUE;
}
static gboolean
cb_err_watch( GIOChannel *channel, GIOCondition cond, GMainLoop *loop)
{
	gchar *string;
	gsize  size;
	GError *error = NULL;

	if (cond == G_IO_HUP)
	{
		g_print ("Close STDERR\n");
		g_io_channel_unref (channel);
		return FALSE;
    }

	g_io_channel_read_line (channel, &string, &size, NULL, &error);
	g_assert_no_error (error);
	g_print (string);
	g_free (string );

	return TRUE;
}

さて、最後に main 関数です。 command_line は、お好みのものを書いてください。終了ステータスを知りたいので、 false や true で終了コードが変わるのを見ることもできます。PATH にある実行形式のファイルなら絶対パスにする必要はありません。

GIOを使うためのおまじないの g_type_init() で実行環境を適宜取得、設定して、g_main_loop_new() でGMainLoopを生成します。GMainContext はデフォルトを使用するので、NULLにしています。

次に、command_line 文字列を g_shell_parse_argv() で文字列配列に変更します。

そして、g_spawn_async_with_pipes() でパイプ有りで非同期の子プロセスを生成します。標準出力と標準エラー出力のファイル識別子を書き込むポインタと子プロセスのGPidを書き込むポインタを渡しています。オプションは、 G_SPAWN_SEARCH_PATH と G_SPAWN_DO_NOT_REAP_CHILD で、G_SPAWN_SEARCH_PATH は、PATH から実行形式を検索してくれるために必要です。G_SPAWN_DO_NOT_REAP_CHILD が肝心なオプションで、これを書くことで子プロセスを親プロセスが自動的に終了しないようになります。そのため、終了ステータスが入手できます。

次に、子プロセス終了用のコールバック関数を登録します。
g_child_watch_add() で、子プロセスの終了時に実行する関数 cb_child_watch を登録します。

次に、ファイル識別子からGIOChannelを生成します。Linuxなので、g_io_channel_unix_new()を使っています。Windows環境なら、g_io_channel_win32_new_fd() を使うみたいです。

そして、g_io_add_watch() を使って GIOChannel を監視する関数を登録します。条件として、 G_IO_IN と G_IO_HUP を監視します。 G_IO_IN は入力があった時、 G_IO_HUP はパイプが閉じられた時に登録した関数を呼び出すことを伝えます。

最後に g_main_loop_run() でシグナル(イベント)を待ち続けています。

void
main(int argc, char **argv )
{
	gchar *command_line = "ls";
	gchar **args;
	GPid pid;
	gint out, err;
	GIOChannel *out_ch, *err_ch;
	GError *error = NULL;
	GMainLoop *loop;

	g_type_init ();
	loop = g_main_loop_new (NULL, TRUE);

	g_shell_parse_argv (command_line, NULL, &args, &error);
	g_assert_no_error (error);

	g_spawn_async_with_pipes (NULL, args, NULL,
		G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL,
		NULL, &pid, NULL, &out, &err, &error);
	g_assert_no_error (error);
	g_strfreev (args);
	g_child_watch_add (pid, (GChildWatchFunc)cb_child_watch, loop);

	out_ch = g_io_channel_unix_new (out);
	err_ch = g_io_channel_unix_new (err);

	g_io_add_watch (out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, loop);
	g_io_add_watch (err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, loop);

	g_main_loop_run (loop);
}

ビルドと実行結果は以下のようになりました。

$ make clean
rm -f spawn *~
$ make
cc -pthread -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include  -pthread -lgio-2.0 -lgobject-2.0 -lgmodule-2.0 -lgthread-2.0 -lrt -lglib-2.0    spawn.c   -o spawn
$ ./spawn 
Makefile
Close STDERR
Close Pid Status: 0
spawn
spawn.c
Close STDOUT

まあ、普通のプログラムでは、gtk_main() と gtk_main_quit() を使うので、こんな面倒なことはしなくても良いと思います。

気が向いたら、GMainLoopを使って、子プロセスとパイプの監視を完全に行う方法を調査してみたいと思います。