試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

Vala言語で非同期の外部プロセス実行をオブジェクトのメンバ関数内で行った後で動作がおかしくなる?(前半)

cpufreqdのプロファイル手動切り替え /モード切り替え用システムトレイツールをC言語のGTK+で作り直し」のcpufreqd-icon(http://kakurasan.ehoh.net/software/cpufreqd-icon/)の以前のバージョン(0.9系)の機能をVala言語へ移植することに成功し、更に追加の機能として/etc/init.d/cpufreqdのinitスクリプトの制御(開始/停止/再起動)もメニュー項目から行えるようにすることとなった。*1
その中で、メニュー項目を選択したときに呼ばれる関数の中でこのスクリプトを「Vala言語で外部プロセスを実行する (GLib.Process.spawn_async_with_pipes()を使用・メインループを用いた例・メモ)」の外部プロセス非同期実行関数により呼び、GLib.ChildWatch.add()で関連付けた後始末用の関数を用いることで、スクリプトの実行終了のタイミングでlibnotifyのバルーン通知を表示するということを考えた。
外部プロセス実行の部分では「DaemonController」というクラスを用意してメニュー項目選択時にこのオブジェクトを作成し、外部プロセスの非同期実行処理を含んだメンバ関数*2を呼んだ上でGLib.ChildWatch.add()により子プロセス終了後に後始末目的のメンバ関数*3が呼ばれるようにしようとしていたのだが、子プロセスが終了した後のタイミングでランダムに落ちるか、子プロセスの後始末の部分*4で処理が失敗するという不具合が起きてしまった。
下は作成中のコードをもとにこの問題に関する部分だけをまとめたもので、実行されるコマンドもテストのためにsleepコマンドにしてある。
これを実行してシステムトレイアイコンのコンテキストメニューから「Start」か「Restart」を選ぶと「sleep 5」が実行され、終了後にlibnotifyの通知が表示されるのだが、そのタイミングでランダムに落ちる(落ちたり落ちなかったりする)。
!!注意!!これは「間違った書き方のコード」です
[任意]ファイル名: daemoncontroltest_ng.vala

using Notify;
using Gtk;

/*
 * valac --pkg gio-2.0 --pkg gio-unix-2.0 --pkg posix --pkg gtk+-2.0 --pkg libnotify -o daemoncontroltest_ng daemoncontroltest_ng.vala
 */

namespace DaemonControlTest
{
  class App
  {
    public static Gtk.StatusIcon status_icon;
    public static NotifyBalloon balloon;
    public static Gdk.Pixbuf? icon;
  }
  public enum DaemonControlAction
  {
    START,
    STOP,
    RESTART
  }
  public errordomain DaemonControlError
  {
    NOT_EXITED;
  }
  class DaemonController
  {
    DaemonControlAction action;
    uint id_childwatch;
    public DaemonController (DaemonControlAction action)
    {
      this.action = action;
    }
    ~DaemonController ()
    {
      GLib.debug ("~DaemonController ()");
    }
    void done (GLib.Pid pid, int status)
    {
      string[] actions = {"Start", "Stop", "Restart"};
      /*
       * オブジェクトが途中で破棄されるため
       * ランダムでこのあたりの動作がおかしくなる
       * (クラスのメンバが正しく参照できなくなり、落ちることもある)
       */
      if (GLib.Process.if_exited (status))
      {
        if (GLib.Process.exit_status (status) == 0)
          App.balloon.update (Notify.Urgency.NORMAL, "OK", "command(%s) succeeded".printf (actions[this.action]));
        else
          App.balloon.update (Notify.Urgency.CRITICAL, "NG", "command(%s) failed".printf (actions[this.action]));
      }
      /* 通知バルーン表示 */
      try
      {
        App.balloon.show ();
      }
      catch (GLib.Error e)
      {
        ;
      }
      /* 子プロセスの後始末 */
      GLib.Process.close_pid (pid);
      /* 終了の監視の後始末 */
      GLib.Source.remove (this.id_childwatch);
    }
    public void execute ()
    {
      GLib.Pid pid_child;
      /* 今回はテストなのでコマンド行は固定とする */
      string[] argv = {"/bin/sleep", "5"};
      try
      {
        /* 外部プロセスの非同期実行 */
        GLib.Process.spawn_async (null,
                                  argv,
                                  null,
                                  GLib.SpawnFlags.DO_NOT_REAP_CHILD,
                                  null,
                                  out pid_child);
        /* 子プロセス終了後にthis.done()を呼ぶ */
        this.id_childwatch = GLib.ChildWatch.add (pid_child, this.done);
      }
      catch (GLib.SpawnError e)
      {
        GLib.warning ("Spawn failed: %s", e.message);
      }
    }
  }
  class NotifyBalloon : Notify.Notification
  {
    public NotifyBalloon (StatusIcon status_icon)
    {
      /* 一括プロパティ代入(GLib.Object派生オブジェクトのみ) */
      GLib.Object (summary: " ", body: "", status_icon: status_icon);
      this.set_timeout (5000);
    }
    /* 「new」により親クラスのupdate()を隠す */
    public new void update (Notify.Urgency urgency, string summary, string body)
    {
      /* base.update()で隠された親クラスのupdate()を呼ぶ */
      base.update (summary, body, "");
      this.set_urgency (urgency);
      if (App.icon != null)
        this.set_icon_from_pixbuf (App.icon);
    }
  }
  class TrayMenu : Gtk.Menu
  {
    ~TrayMenu ()
    {
      /* メニューが破棄されたとき */
      GLib.debug ("~TrayMenu ()");
    }
    public static void create_and_popup_menu (Gtk.StatusIcon icon, uint button, uint time)
    {
      TrayMenu menu = new TrayMenu ();
      Gtk.Image image_start = new Image.from_stock (Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU);
      Gtk.Image image_restart = new Image.from_stock (Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU);
      Gtk.ImageMenuItem item_start = new ImageMenuItem.with_mnemonic ("_Start");
      Gtk.ImageMenuItem item_restart = new ImageMenuItem.with_mnemonic ("_Restart");
      Gtk.ImageMenuItem item_quit = new ImageMenuItem.from_stock (Gtk.STOCK_QUIT, null);
      item_start.image = image_start;
      item_restart.image = image_restart;
      /* メニューに項目を追加 */
      menu.prepend (item_quit);
      menu.prepend (new Gtk.SeparatorMenuItem ());
      menu.prepend (item_restart);
      menu.prepend (item_start);
      /* シグナル */
      item_start.activate.connect (() =>
      {  // 引数のない匿名関数
        try
        {
          DaemonController ctl = new DaemonController (DaemonControlAction.START);
          ctl.execute ();
          /* オブジェクトの寿命が切れて破棄される */
        }
        catch (DaemonControlError e)
        {
          GLib.warning ("%s", e.message);
        }
      });
      item_restart.activate.connect (() =>
      {
        try
        {
          DaemonController ctl = new DaemonController (DaemonControlAction.RESTART);
          ctl.execute ();
        }
        catch (DaemonControlError e)
        {
          GLib.warning ("%s", e.message);
        }
      });
      menu.show_all ();
      item_quit.activate.connect (Gtk.main_quit);
      menu.popup (null, null, icon.position_menu, button, time);
    }
  }
  class MainClass
  {
    public static int main (string[] args)
    {
      /* 仮のアイコン */
      try
      {
        App.icon = new Gdk.Pixbuf.from_file ("/usr/share/pixmaps/apple-red.png");
      }
      catch (GLib.Error e)
      {
        GLib.critical ("Icon not found: %s", e.message);
        return 1;
      }
      Gtk.init (ref args);
      if (! Notify.init ("daemoncontroltest"))
      {
        GLib.critical ("Couldn't initialize libnotify");
        return 1;
      }
      App.status_icon = new Gtk.StatusIcon.from_pixbuf (App.icon);
      App.status_icon.popup_menu.connect (TrayMenu.create_and_popup_menu);
      App.balloon = new NotifyBalloon (App.status_icon);
      Gtk.main ();
      return 0;
    }
  }
}

色々悩んだ末、このようになってしまう原因と対処方法が分かった。「Vala言語におけるデストラクタについてと動作テスト」の中の例とも関係するが、メニュー項目選択のハンドラの中でオブジェクトを定義し作成しても

Gtk.ImageMenuItem item_start = new ImageMenuItem.with_mnemonic ("_Start");
item_start.activate.connect (() =>
{
  try
  {
    DaemonController ctl = new DaemonController (DaemonControlAction.START);
    ctl.execute ();
    /* オブジェクトの寿命が切れて破棄される */
  }
  catch (DaemonControlError e)
  {
    GLib.warning ("%s", e.message);
  }
});

そのオブジェクトへの参照が他になく、ctl.execute()の中で必要なところ*5まで処理が完結すればよいのだが

class DaemonController
{

(中略)

  public void execute ()
  {
    GLib.Pid pid_child;
    string[] argv = {"/bin/sleep", "5"};
    try
    {
      /* 外部プロセスの非同期実行 */
      GLib.Process.spawn_async (null,
                                argv,
                                null,
                                GLib.SpawnFlags.DO_NOT_REAP_CHILD,
                                null,
                                out pid_child);
      /* 子プロセス終了後にthis.done()を呼ぶ */
      this.id_childwatch = GLib.ChildWatch.add (pid_child, this.done);
    }
    catch (GLib.SpawnError e)
    {
      GLib.warning ("Spawn failed: %s", e.message);
    }
    /* 子プロセスは動作中だがここでメンバ関数は終了 */
  }
}

この中で外部プロセスの非同期実行をするため、子プロセスが動作中でもctl.execute()の処理は終わり、メニュー項目選択のハンドラ関数の終わりに到達してオブジェクトctlの破棄処理が先に行われてしまう。
その後チュートリアルなども参考にして色々試した結果、静的メンバの(DaemonController型)オブジェクト変数を用意した上で「弱い参照のDaemonControllerオブジェクト」か「静的メンバ関数」のいずれかを用いた方法によりうまく動作するようになった。

(「Vala言語で非同期の外部プロセス実行をオブジェクトのメンバ関数内で行った後で動作がおかしくなる?(後半)」に続く)

関連記事:

使用したバージョン:

  • Vala 0.7.9

*1:initスクリプトgksudoのようなコマンドを通して管理者権限で実行するようにしている

*2:execute()とした

*3:done()とした

*4:子プロセス終了の監視を行うIDをGLib.Source.remove()に渡して監視を解除する段階

*5:子プロセスの終了後の後始末の終わりまではオブジェクトは使われるので、残っていてほしい