DelegateでSSLリバースプロキシ

Delegateは、RELAY=delegateというモードにおいて、

http://(Delegateサーバ)/-_-(目的のURL)

のような形でリバースプロキシを構築できますが、Delegateサーバ自体にSSLを設定し、

https://(Delegateサーバ)/-_-(目的のURL)

とし、目的のURLにhttp、httpsの両方を許容するように設定したい場合、多少工夫が必要でしたので、そのメモです。

設定

STLS="fcl,fsv:https"
-P443
SERVER=https
RELAY=delegate

STLSのfsv:httpsの部分が肝で、目的のURLがhttpsのときのみ、SSLを使用するようにします。
:httpsを書かないと、目的のURLがhttpのときに空のページが帰ってきますし、fsv自体はずすと、逆にhttpsのときにうまくいきません。
書いてみると何の変哲もないような感じなのですが、マニュアルには、

SSL が使用できなくてもセッションを継続したい場合、 "-" を "fsv" または、"fcl" の前につけます。

とあるので、-fsvでOKじゃんというところで思考停止し、大分ハマりました。

Click Onceアプリケーションにコマンドラインから引数を渡す

Visual StuidioのExpress版の発行機能を使って作成したClick Onceアプリケーションには、コマンドラインから直接引数を渡すことができません。
(通常、HTTPサーバに配備し、.applicationのURLに、URLパラメータで渡す必要があると思います。)
HTTPサーバを用意できる場合であれば、それで全く問題ありません。
ただし、種々の事情でそれが難しいケースもあると思います。
その場合に、コマンドラインでなんとか引数を渡す方法を考えてみました。

アプリ側でexeのパスを記憶する

Click Onceからアプリケーションをセットアップすると、インストール後にexeが起動されます。
ただし、exeは

C:\Users\[ユーザー名]\AppData\Local\Apps\2.0\L4AX7N83.7TE\B80T7A4X.MEW

Windows7調べ。最後の2階層は環境によって違うかも?)
の下にバージョン毎にコピーされ、静的に決定することができません。
そこで、exeが起動された際に、テキストファイル等に自分自身のexeのパスを記録しておくようにしておきます。

static class Program
...
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            ...
            WriteExePathToStartupConfig();
            ...
        }

        /// <summary>
        /// EXEパスを設定ファイルに書き込みます。
        /// </summary>
        private static void WriteExePathToStartupConfig()
        {
            using (StreamWriter writer = new StreamWriter(@"C:\MyApp\startup.txt", false, Encoding.GetEncoding(932)))
            {
                writer.Write(Assembly.GetEntryAssembly().Location);
            }
        }
...
}

すると、C:\MyApp\startup.txtには次のような文字列が残ります。

C:\Users\jfuruya\AppData\Local\Apps\2.0\L4AX7N83.7TE\B80T7A4X.MEW\myap..tion_bb87480e67debe12_0001.0000_9a89019a5cc6f2c5\MyApp.exe

こういった要件では、アプリは引数なしでは動作できないと思われますので、引数が渡されなかった場合は、exeのパスの記録だけ行って終了するなどの動作にすれば、ユーザには、アプリのインストールがされただけのように見えます。

上記の情報を利用した起動プログラムを用意する

上記のexeのパスの情報を利用し、アプリの起動を行うプログラムを用意します。
呼び出し側は、このプログラムに引数を渡して起動します。
下記は、vbsを利用した例です。

Set arg = WScript.Arguments
If arg.Count < 2 Then
	MsgBox("プログラムの呼び出し方法が正しくありません。")
	WScript.Quit
End If

company_code = arg(0)
emp_id = arg(1)

Set fso = CreateObject("Scripting.FileSystemObject")
Set file = fso.OpenTextFile("C:\MyApp\startup.txt")

exe_path = file.ReadAll

file.Close
Set file = Nothing
Set fso = Nothing

command_line = """" & exe_path & """ " & company_code & " " & emp_id

Set shell = CreateObject("WScript.Shell")

shell.Run command_line

Set shell = Nothing

こうすれば、HTTPサーバに配備されていないClick Onceアプリケーションでも、アップグレード時などにexeのパスが最新になりますので、常に最新のexeパスでアプリを起動することができます。

トリッキーな方法にはなるので、呼び出しそのものだけを考えると、クラスライブラリとして作成し、外部から呼び出す方がすっきりしているかもしれません。
それと、[コントロールパネル] -> [プログラムと機能]からアプリをロールバックされた場合、exeのパスは戻らないので、スタートメニューから手動で1回アプリを起動する必要があると思います。

SeasarのpublicフィールドでFormTableを使う

FormTableにコントロールカラムを配置するためには、通常のTableで使用するColumnの替わりにFieldColumnを使用する必要がありますが、これはSeasarで推奨されるpublicフィールドに対応していません。
publicフィールドに対応させるには、FieldColumnを継承し、getPropertyメソッドをオーバーライドして、publicフィールドに対応させなければいけません。

     public class PublicFieldFormTableColumn extends FieldColumn {
...
          @Override
          public Object getProperty(String name, Object row) {
               try {
                    String[] names = new String[] { name };
                    if (name.indexOf('.') >= 0) {
                         names = name.split("\\.");
                    }
                    Class<?> target = row.getClass();
                    Object value = row;
                    for (String propertyName : names) {
                         Field field = target.getField(propertyName);
                         value = field.get(value);
                         target = field.getType();
                    }
                    return value;
               } catch (Exception ex) {
                    throw new RuntimeException(ex);
               }
          }
     }

なおこれは、S2Clickに含まれるPublicFieldColumnの実装方法をそのまま用いています。

FormTableをAjax化する際の注意点

click-extrasには、表形式のフォームを実現するための、FormTableというコンポーネントがあります。
http://click.avoka.com/click-examples/table/form-table.htm
これはTableを拡張したコンポーネントなので、前回の記事にあるようにページリンクのAjax化が可能です。
また、サブミットボタンをAjax化したいというニーズもあるでしょう。
(これは、通常のSubmitをS2Clickに含まれるAjaxSubmitに置き換えれば実現できます。)
ただ、やってみるといくつか注意しなければいけないポイントがありましたので、順に説明します。

フォーム要素にDateFieldが含まれる場合の注意

DateFieldを使用すると、カレンダーをセットアップするためのscriptタグがDateFieldの数だけHTMLに追加され、ページがロードされたときに1回ずつ実行されます。

<head>

<script id="table_form_date_0-js-setup" type="text/javascript">
Click.addLoadEvent(function(){
Event.observe('table_form_date_0-button', 'click', function(){ calendar = new CalendarDateSelect($('table_form_date_0'), {  minute_interval: 1, popup_by: 'table_form_date_0-button',  embedded: false,  footer: false,  buttons: false,  time: false,  formatValue: 'yyyy/MM/dd',  year_range: [1930,2050] });});});
</script>
<script id="table_form_date_1-js-setup" type="text/javascript">
Click.addLoadEvent(function(){
Event.observe('table_form_date_1-button', 'click', function(){ calendar = new CalendarDateSelect($('table_form_date_1'), {  minute_interval: 1, popup_by: 'table_form_date_1-button',  embedded: false,  footer: false,  buttons: false,  time: false,  formatValue: 'yyyy/MM/dd',  year_range: [1930,2050] });});});
</script>

ところが、AjaxでFormTableのHTMLの置き換えを行うと、これらのscriptタグが実行されないので、カレンダーのアイコンをクリックしても、カレンダーが開かなくなります。
そこで、FormTableのHTMLをAjaxのレスポンスとして戻す際、その中に含まれるDateFieldセットアップ用のJavaScriptもレスポンスの一部として戻し、Ajaxのレスポンスを処理する際に実行してあげます。
まず、Java側でレスポンスを戻す際に、FormTableのHTMLとともに、そのheaderElementに含まれるDateFieldセットアップ用のJavaScriptを収集し、配列として戻します。

     public boolean onPageMove() {
          String tableHTML = table.toString();
          Set<String> setupJsScripts = getSetupJsScripts();
          Map<String, Object> result = new HashMap<String, Object>();
          result.put("tableHTML", tableHTML);
          result.put("setupJsScripts", setupJsScripts);
          renderJSON(result);
          return false;
     }

     private Set<String> getSetupJsScripts() {
          Set<String> scripts = new HashSet<String>();
          for (Element e : table.getHeadElements()) {
               if (e instanceof JsScript) {
                    JsScript js = (JsScript) e;
                    if (js.getId() != null && js.getId().endsWith("-js-setup")) {
                         scripts.add(js.getContent());
                    }
               }
          }
          return scripts;
     }

JavaScript側でレスポンスを処理する際に、HTMLの置き換えを行った後、受け取ったJavaScriptの配列の中身を全て実行します。

var movePageComplete = function (res) {
     var json = eval('(' + res.responseText + ')');
     $('tab').innerHTML = json['tableHTML'];
     executeSetupScripts(json['setupJsScripts']);
...
}
var executeSetupScripts = function (scripts) {
     for (var i = 0; i < scripts.length; i++) {
          eval(scripts[i]);
     }
}

SubmitをAjaxSubmitに置き換える場合の注意

AjaxSubmitを使用すると、それが含まれるフォームのonsubmitにAjax呼び出しを行って本来のサブミット動作をキャンセルするイベントハンドラを登録するscriptタグが追加され、ページがロードされたときに1回だけ実行されます。

<tr class="buttons"><td class="buttons"><input type="submit" name="ajaxSubmit" id="table_form_ajaxSubmit" value="Ajax Submit"/><script type="text/javascript">
$('table_form').onsubmit = function(){
  this.request({ method: 'post', onComplete: ajaxSubmitComplete}); return false;}
</script>
</td></tr>

しかし、AjaxでFormTableのHTMLの置き換えを行った場合、そのscriptタグが実行されないので、ボタンを押すと普通にフォームがサブミットされてしまいます。
これを防ぐため、HTMLの置き換えを行った後、FormTable内のフォームのonsubmitに上記イベントハンドラを追加します。
"table-form"は、FormTableのformタグに自動的に割り振られるidです。

var movePageComplete = function (res) {
     var json = eval('(' + res.responseText + ')');
     $('tab').innerHTML = json['tableHTML'];
...
     $('table_form').onsubmit = function () {
          this.request({ method: 'post', onComplete: ajaxSubmitComplete});
          return false;
     };
}

※このコードだけを実行時にAjaxSubmitから取得することが難しかったので、出力されたHTMLから抽出しました。ただし、フォームのID等は変わる可能性がありますので、もう少し良いやり方があるかもしれません。

以下、今回の全てのサンプルコードです。

TableのページリンクをAjax化する

Click ExampleにあるTable Ajax Page
http://click.avoka.com/click-examples/ajax/table/table-ajax.htm
には、TableコンポーネントのページリンクをAjax化する方法が示されていますが、S2ClickのAjaxLinkを使用すると、もっと簡単に同じことを実現することができます。
S2ClickPageを継承したPageクラスを作成し、その中でTableを継承したインナークラスを宣言します。
ここで、Table#getControlLinkメソッドをオーバーライドして、ActionLinkの替わりにAjaxLinkのインスタンスを返します。

public class AjaxLinkInTablePage extends S2ClickPage {
...
	public Table table = new Table() {

		@Override
		public ActionLink getControlLink() {
			if (controlLink == null) {
				AjaxLink al = new AjaxLink(AjaxLinkInTablePage.this,
						"onPageMove");
				al.addAjaxHandler(AjaxUtils.ON_COMPLETE, "movePageComplete");
				controlLink = al;
			}
			return controlLink;
		}
	};
...

onPageMoveはリンクがクリックされた際にJava側で呼び出されるメソッド名です。
movePageCompleteは、レスポンスが正常に返った後に呼び出されるJavaScript側の関数名です。
onPageMoveでは、S2ClickPage#renderJSONを使用し、テーブルのHTMLをJSONのレスポンスとして戻します。

public class AjaxLinkInTablePage extends S2ClickPage {
...
	public boolean onPageMove() {
		Map<String, Object> jsonObj = new HashMap<String, Object>();
		jsonObj.put("tab", table.toString());
		renderJSON(jsonObj);
		return false;
	}
...

movePageCompleteでは、サーバから受け取ったJSONをパースし、その内容でHTMLを書き換えます。

...
<head>
$imports
<script>
var movePageComplete = function (res) {
	var json = eval('(' + res.responseText + ')');
	$('tab').innerHTML = json['tab'];
};
</script>
</head>
<body>
<div id="tab">
$table
</div>
...

以下、今回書いた全サンプルコードです。

Androidのライフサイクルを実機で検証してみた

Androidのライフサイクルについては色々なところで解説されているが、実際に端末で操作を行った際に、OnCreate等のメソッドがどう呼ばれるのかがいまいち分かりづらかったので、軽くまとめてみた。
検証に使用した端末は、Nexus S(2.3.6)。

検証用Activity

次のような実装に。

  • ライフサイクルに関連する7つのメソッド(よく解説の図とかに載ってるやつ)を全てオーバーライドし、呼び出されたメソッドに対応する文字列をログに出力する。
  • 上記に加え、onRestoreInstanceState/onSaveInstanceStateも同じように見る。
  • finish()を呼んだときの挙動も見るため、Viewにボタンを1個配置し、クリックされたときにfinish()を呼ぶ。
  • onRestore()でActivityのインスタンスインスタンスフィールド、クラスとstaticフィールドのhashCode()を出力する。
HelloAndroidActivity.java
public class HelloAndroidActivity extends Activity implements OnClickListener {

	private static final String TAG = "LifeCycleTest";

	private Object instanceField = new Object();

	private static Object staticField = new Object();

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		Log.d(TAG, "----------OnCreate");
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		((Button) findViewById(R.id.finish_button)).setOnClickListener(this);
	}

	@Override
	protected void onStart() {
		Log.d(TAG, "----------OnStart");
		super.onPause();
	}

	@Override
	protected void onResume() {
		Log.d(TAG, "----------OnResume");
		Log.d(TAG, String.format(
				"Activity-Class:hashCode=%1$x,static field hashCode:%1$x", this
						.getClass().hashCode(),
				HelloAndroidActivity.staticField.hashCode()));
		Log.d(TAG, String.format(
				"Activity-Instance:hashCode=%1$x,instance field hashCode:%1$x",
				this.hashCode(), this.instanceField.hashCode()));
		super.onResume();
	}

	@Override
	protected void onRestart() {
		Log.d(TAG, "----------OnRestart");
		super.onPause();
	}

	@Override
	protected void onPause() {
		Log.d(TAG, "----------OnPause");
		super.onPause();
	}

	@Override
	protected void onStop() {
		Log.d(TAG, "----------OnStop");
		super.onPause();
	}

	@Override
	protected void onDestroy() {
		Log.d(TAG, "----------OnDestroy");
		super.onPause();
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		Log.d(TAG, "----------onRestoreInstanceState");
		super.onRestoreInstanceState(savedInstanceState);
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		Log.d(TAG, "----------onSaveInstanceState");
		super.onSaveInstanceState(outState);
	}

	@Override
	public void onClick(View v) {
		finish();
	}

}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<Button
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:id="@+id/finish_button"
	android:text="終了">
</Button>

色々動かしてみる。

アプリを起動
10-28 15:05:12.542: DEBUG/LifeCycleTest(25813): ----------OnCreate
10-28 15:05:12.562: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:05:12.562: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:05:12.566: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:05:12.566: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40516160,instance field hashCode:40516160
バックボタンでアプリを閉じる
10-28 15:05:43.085: DEBUG/LifeCycleTest(25813): ----------OnPause
(中略)
10-28 15:05:43.605: DEBUG/LifeCycleTest(25813): ----------OnStop
10-28 15:05:43.605: DEBUG/LifeCycleTest(25813): ----------OnDestroy
もう一回起動
10-28 15:06:05.660: DEBUG/LifeCycleTest(25813): ----------OnCreate
10-28 15:06:05.664: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:06:05.664: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:06:05.664: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:06:05.664: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=4051f0a8,instance field hashCode:4051f0a8
ホームボタンでアプリを閉じる
10-28 15:06:55.167: DEBUG/LifeCycleTest(25813): ----------onSaveInstanceState
10-28 15:06:55.167: DEBUG/LifeCycleTest(25813): ----------OnPause
再び起動
10-28 15:07:14.578: DEBUG/LifeCycleTest(25813): ----------OnRestart
10-28 15:07:14.578: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:07:14.578: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:07:14.578: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:07:14.578: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=4051f0a8,instance field hashCode:4051f0a8
finish()
10-28 15:07:40.613: DEBUG/LifeCycleTest(25813): ----------OnPause
(中略)
10-28 15:07:41.058: DEBUG/LifeCycleTest(25813): ----------OnStop
10-28 15:07:41.058: DEBUG/LifeCycleTest(25813): ----------OnDestroy
もう一回起動
10-28 15:08:05.117: DEBUG/LifeCycleTest(25813): ----------OnCreate
10-28 15:08:05.121: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:08:05.121: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:08:05.121: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:08:05.121: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40517968,instance field hashCode:40517968
スクリーンオフ
10-28 15:08:20.882: DEBUG/LifeCycleTest(25813): ----------onSaveInstanceState
10-28 15:08:20.882: DEBUG/LifeCycleTest(25813): ----------OnPause
スクリーンオン
10-28 15:08:39.031: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:08:39.031: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:08:39.031: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40517968,instance field hashCode:40517968

パスワードロックをかけているが、ロックを外す前にログが出た。

端末の向きを変える→戻す
10-28 15:16:04.257: DEBUG/LifeCycleTest(25813): ----------onSaveInstanceState
10-28 15:16:04.257: DEBUG/LifeCycleTest(25813): ----------OnPause
10-28 15:16:04.257: DEBUG/LifeCycleTest(25813): ----------OnStop
10-28 15:16:04.257: DEBUG/LifeCycleTest(25813): ----------OnDestroy
10-28 15:16:04.261: DEBUG/LifeCycleTest(25813): ----------OnCreate
10-28 15:16:04.265: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:16:04.265: DEBUG/LifeCycleTest(25813): ----------onRestoreInstanceState
10-28 15:16:04.265: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:16:04.265: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:16:04.265: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40538598,instance field hashCode:40538598
(中略)
10-28 15:16:07.468: DEBUG/LifeCycleTest(25813): ----------onSaveInstanceState
10-28 15:16:07.468: DEBUG/LifeCycleTest(25813): ----------OnPause
10-28 15:16:07.468: DEBUG/LifeCycleTest(25813): ----------OnStop
10-28 15:16:07.468: DEBUG/LifeCycleTest(25813): ----------OnDestroy
10-28 15:16:07.468: DEBUG/LifeCycleTest(25813): ----------OnCreate
10-28 15:16:07.472: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:16:07.472: DEBUG/LifeCycleTest(25813): ----------onRestoreInstanceState
10-28 15:16:07.476: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:16:07.476: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:16:07.476: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40539f20,instance field hashCode:40539f20
電話がかかってきた→切る
10-28 15:17:06.281: DEBUG/LifeCycleTest(25813): ----------onSaveInstanceState
10-28 15:17:06.281: DEBUG/LifeCycleTest(25813): ----------OnPause
(中略)
10-28 15:17:06.406: INFO/power(109): *** set_screen_state 1
10-28 15:17:06.609: DEBUG/LifeCycleTest(25813): ----------OnStop
(中略)
10-28 15:17:09.699: DEBUG/LifeCycleTest(25813): ----------OnRestart
10-28 15:17:09.699: DEBUG/LifeCycleTest(25813): ----------OnStart
10-28 15:17:09.699: DEBUG/LifeCycleTest(25813): ----------OnResume
10-28 15:17:09.699: DEBUG/LifeCycleTest(25813): Activity-Class:hashCode=40515d80,static field hashCode:40515d80
10-28 15:17:09.699: DEBUG/LifeCycleTest(25813): Activity-Instance:hashCode=40539f20,instance field hashCode:40539f20

まれにOnDestroy()まで呼ばれ、OnCreate()から始まるときもある。

分かったこと

大体書籍やWebに書いてあるのと同じ動作であったが、下記は個人的に今回分かったこと。

  • onRestoreInstanceState()は画面の回転以外では呼ばれない。
  • ホームボタンを押した際の動作は、アプリの終了ではなく一時停止。(ホームアプリが起動してアプリが一時停止した状態なのだろう。)
  • バックボタンとfinish()時の動作は同じ。(これらは終了扱いか。)
  • OnDestroy()が呼ばれるとインスタンスは破棄されるが、クラスはアンロードされない。

qemu-kvmにACPIのshutdownシグナルを送る方法

libvirt使いなさいよって話なんだと思うけど…。
https://wiki.archlinux.org/index.php/QEMU#Starting_qemu_virtual_machines_on_boot
を読んでいたら、

qemu_vm1="-enable-kvm -m 512 -hda /dev/mapper/vg0-vm1 -net nic,macaddr=DE:AD:BE:EF:E0:00 \
 -net tap,ifname=tap0 -serial telnet:localhost:7000,server,nowait,nodelay \
 -monitor telnet:localhost:7100,server,nowait,nodelay -vnc :0"
qemu_vm1_haltcmd="echo 'system_powerdown' | nc.openbsd localhost 7100" # or netcat/ncat

て書いてあって。
-monitorオプション付きでVMの外からQEMUのモニターに接続できるようにしておいて、そこにnetcatでsystem_powerdownコマンドを発行すれば良いと。
そもそも-monitorオプションを知らなかったとかだった。orz
さくらさんのブログ
http://research.sakura.ad.jp/2010/03/08/kvm-install/
にも書いてあったりして、完全に調査不足。

VMの起動(Scientific Linux 6.1の場合)

qemu-kvmでゲストを起動する際に

-monitor telnet::4444,server,nowait

を付ける。

ゲスト側の準備(Scientific Linux 6.1の場合)

ゲスト側でACPIのイベントを監視する必要があるので、

# yum -y install acpid
# chkconfig acpid on
# /etc/init.d/acpid start

としてacpidを動かしてお…こうとするのだが、なぜかデーモンの起動に失敗するので、一度再起動する。

system_powerdownコマンドの発行

ゲストが再起動した後、ホスト側で、

# echo 'system_powerdown' | nc localhost 4444

とする。
VNC仮想マシンのコンソールを見ていると、ゲストがACPIのシャットダウンシグナルを受け取って、シャットダウンされる。
ただ、上記コマンドの発行自体は一瞬で終わるため、ホスト終了時に全てのゲストを安全にシャットダウンさせるには、プロセスの起動状況を確認する等の手間が必要だと思う。
(その場合、上記英語のURLにあるスクリプトが参考になりそう。)
まあそもそもlibvirt(ry