Xvfbで作った仮想ディスプレイにリモートデスクトップ接続する

昨日の続き

仮想ディスプレイのスクリーンショットを撮って中の様子がわかるようになったのはよいですが、確認するたびにxwdでキャプチャしてconvertで変換してってやるのは大変です。静止画だとよくわからないってのもあります。

調べてたらどうやらVNCリモートデスクトップ接続のようなことができそうだったのでやってみました。

必要なもの

  • fluxbox
  • x11vnc

fluxboxはウィンドウマネージャ。x11vncはきっとX Windows System用のVNCサーバなのだと思います。

この2つをインストールします。

makoto@hp-laptop:~$ sudo apt-get install fluxbox x11vnc


CentOSだと標準リポジトリには入っていなかったので、rpmforgeからインストールします。

makoto@hp-laptop:~$ wget http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm
makoto@hp-laptop:~$ sudo rpm -Uhv rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm
makoto@hp-laptop:~$ sudo yum install fluxbox x11vnc --enablerepo=rpmforge

fluxboxとx11vncを起動

その前にXvfbを起動しておきます。「-screen」オプションの後にスクリーン番号と解像度と色数を指定しました。これでフルカラーな感じになるはずです。
起動する前に環境変数DISPLAYに「:1」を設定しておきます。

makoto@hp-laptop:~$ export DISPLAY=:1
makoto@hp-laptop:~$ Xvfb :1 -screen 0 1024x768x24 &


fluxboxをバックグラウンドで起動します。

makoto@hp-laptop:~$ fluxbox &


x11vncをこれまたバックグラウンドで起動します。

makoto@hp-laptop:~$ x11vnc -display :1 -bg -nopw -listen localhost -xkb

オプション

-display ディスプレイ番号を指定
-bg バックグラウンドで起動
-nopw パスワードなし
-listen 接続を待ち受けるIPアドレス
-xkb XKEYBOARDという拡張機能を有効にする

「-listen」は「localhost」だと外から接続できないみたいです。「0.0.0.0」を指定したら外から接続できました。

VNC Viewerでリモートデスクトッ付接続

VNCが起動したので、リモートデスクトップ接続をしてみます。リモートデスクトップ・ビュアーというアプリを起動して、localhostに接続します。

接続したところです。ディスプレイに何も起動していないので何もないですね。

コマンドラインからfirefoxを起動したところです。マウスやキーボードが普通に使えるので、ここからURLを指定してページを開くこともできます。

yahooのページを開いてみました。色数を24bitに指定したので、キレイに写っています。

サーバーでWeb画面のスクリーンショットを取る

テストをしていて不具合を見つけたときなど、Web画面のスクリーンショットをとって資料に貼りつけたりすることがよくありますが、キャプチャツールを起動して画面ショットをとってファイルを保存してというのは結構面倒です。

サーバー側で画面ショットがとれたら、URLから画面ショットの画像を生成してダウンロードするようなものを作って、それを叩くブックマークレットを作って…とか夢が広がります。というわけで調べてみました。

既存のサービス

URLからサムネイルを作成するようなサービスは色々あるみたいですが、スクリーンショットを撮りたいのは公開していない検証環境みたいなページだったりするので、サムネイル作成サービスみたいなのはちょっと違いました。

Xvfb (X Virtual Frame Buffer)

X Windows Systemがインストールされていないシステムで、仮想的なフレームバッファを実現するXvfbというソフトウェアがあるみたいです。「フレームバッファ」というのが何だかわからなかったのですが、どうやらディスプレイに描画されるドットの集まりをフレームと呼んで、そいつをバッファリングして高速にディスプレイへの出力を行うとかそんな感じのものだと思います。つまり、よくわかりません><

よくわかりませんが、このXvfbを使えばXが入ってない環境でも仮想的なディスプレイを作ることができ、この仮想ディスプレイにWebプラウザを立ち上げて、ページのスクリーンショットを撮ることができるみたいです。

とりあえず手元のUbuntuで試してみます。

Xvfbを使ってみる Ubuntu

まずはXvfdをインストールします。画面ショットをpng形式に変換するためにImageMagickという画像ライブラリもインストールしておきます。

makoto@hp-laptop:~$ sudo apt-get install xvfb imagemagick

インストールしたXvfbを起動します。これで仮想ディスプレイが起動するわけです。目には見えないけど、ディスプレイのようなものが1つ増えた感じでしょうか。

makoto@hp-laptop:~$ Xvfb :1

なにかエラーメッセージが出ていますが気にしないことにします。フォントが初期化できない?リストから削除する?英語はよくわかりません。

[dix] Could not init font path element /usr/share/fonts/X11/cyrillic, removing from list!

仮想ディスプレイが起動したので、次はfirefoxを起動してこの仮想ディスプレイ上に表示します。「表示します」といっても仮想ディスプレイなので目には見えません。
DISPLAY環境変数にディスプレイ番号をセットしてからfirefoxを起動すると、そのディスプレイ上にfirefoxが表示されるみたいです。firefoxに限らずX Windows Systemで動くGUIアプリケーションは実行時のDISPLAY環境変数に指定されたディスプレイに表示される気がする。そんな気がする。

makoto@hp-laptop:~$ DISPLAY=:1; firefox

またすごい勢いでメッセージが出ました。最後の方にfailedとか書いてありますが、プロセスは終了せずに走り続けているので気にしないことにします。

5 XSELINUXs still allocated at reset
SCREEN: 0 objects of 84 bytes = 0 total bytes 0 private allocs
DEVICE: 4 objects of 24 bytes = 96 total bytes 0 private allocs
CLIENT: 0 objects of 128 bytes = 0 total bytes 0 private allocs
WINDOW: 0 objects of 16 bytes = 0 total bytes 0 private allocs
PIXMAP: 1 objects of 8 bytes = 8 total bytes 0 private allocs
GC: 0 objects of 44 bytes = 0 total bytes 0 private allocs
CURSOR: 0 objects of 4 bytes = 0 total bytes 0 private allocs
CURSOR_BITS: 0 objects of 4 bytes = 0 total bytes 0 private allocs
DBE_WINDOW: 0 objects of 12 bytes = 0 total bytes 0 private allocs
TOTAL: 5 objects, 104 bytes, 0 allocs
4 DEVICEs still allocated at reset
DEVICE: 4 objects of 24 bytes = 96 total bytes 0 private allocs
CLIENT: 0 objects of 128 bytes = 0 total bytes 0 private allocs
WINDOW: 0 objects of 16 bytes = 0 total bytes 0 private allocs
PIXMAP: 1 objects of 8 bytes = 8 total bytes 0 private allocs
GC: 0 objects of 44 bytes = 0 total bytes 0 private allocs
CURSOR: 0 objects of 4 bytes = 0 total bytes 0 private allocs
CURSOR_BITS: 0 objects of 4 bytes = 0 total bytes 0 private allocs
DBE_WINDOW: 0 objects of 12 bytes = 0 total bytes 0 private allocs
TOTAL: 5 objects, 104 bytes, 0 allocs
1 PIXMAPs still allocated at reset
PIXMAP: 1 objects of 8 bytes = 8 total bytes 0 private allocs
GC: 0 objects of 44 bytes = 0 total bytes 0 private allocs
CURSOR: 0 objects of 4 bytes = 0 total bytes 0 private allocs
CURSOR_BITS: 0 objects of 4 bytes = 0 total bytes 0 private allocs
DBE_WINDOW: 0 objects of 12 bytes = 0 total bytes 0 private allocs
TOTAL: 1 objects, 8 bytes, 0 allocs
[dix] Could not init font path element /usr/share/fonts/X11/cyrillic, removing from list!
Xlib:  extension "RANDR" missing on display ":1".

(firefox-bin:9312): LIBDBUSMENU-GTK-CRITICAL **: dbusmenu_menuitem_property_set_shortcut: assertion `gtk_accelerator_valid(key, modifier)' failed

次に「xwd」というコマンドで仮想ディスプレイのスクリーンショットを撮ります。これまで目に見えなかった仮想ディスプレイですが、スクリーンショットを撮ることで中の様子を見ることができるわけです。

makoto@hp-laptop:~$ xwd -display :1 -root -out test.xwd
-display ディスプレイ番号
-root ディスプレイ全体を撮る
-out 出力ファイル名

出力されたxwdファイルは、最初にインストールしたImageMagickのconvertコマンドで変換することができます。

makoto@hp-laptop:~$ convert test.xwd test.png

こんなスクリーンショットが撮れました。
たしかにfirefoxの姿が撮影できています。素晴らしいです。

まとめるとこうなります。

  • Xvfbをインストールする
  • ディスプレイ番号を指定してXvfbを起動
  • 起動した仮想ディスプレイ上にfirefoxを起動
  • xwdで仮想ディスプレイのスクリーンショットを撮る
  • ImageMagickをインストールしてconvertコマンドで画像形式を変換
  • 変換した画像を眺めてニヤニヤする

Xvfbを使ってみる CentOS

CentOSでもやってみました。Xvfbのパッケージ名が長いです。「X Windows Systemがインストールされてない環境でも」っていうのは間違いだったようです。Xがない状態ではXvfbは起動してくれませんでした。

# 必要なソフトをインストール
sudo yum groupinstall "X Window System"
sudo yum install xorg-x11-server-Xvfb
sudo yum install ImageMagick
sudo yum install firefox

# Xvfbとブラウザを起動
export DISPLAY=:1
Xvfb :1 &
firefox &

# スクリーンショットをとって変換
xwd -root -display :1 -out test.xwd
convert test.xwd test.png

こんなスクリーンショットがとれました。

まとめ

とりあえずCUICentOS上でFirefoxスクリーンショットを撮ることができました。
でも色々と課題が残ってます。

  • 毎回スクリーンショット撮って確認するのは面倒。リモートデスクトップみたいに操作したい
  • 画質がよくない
  • firefoxのウィンドウだけ撮れればいいのにフルスクリーンになっちゃう
  • URLを指定してページを開きたい
  • firefoxのウィンドウサイズを指定して起動したい
  • firefoxを起動するといつもクラッシュから立ち直ったような画面になってるのがイヤ

この辺を調べてみようと思います。

1つの文が複数行に渡る場合の書き方

1つの文が長くなりすぎてしまい、途中で改行したい時があります。

pythonには文の終わりを表す「;」(セミコロン)がないため、改行が文の区切りになります。文の途中で改行すると、2つめの行は新しい文の始まりとみなされて、文法エラーになってしまうのです。

print '1234567890' +
      'abcdefghijklmnopqrstuvwxyz' +
      'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
$ python multiline-sequence.py
  File "multiline-sequence.py", line 1
    print '1234567890' +
                       ^
SyntaxError: invalid syntax

行のおわりに「\」(バックスラッシュ)を書くと、文が次の行とつながって解釈されるので、1つの文を複数の行に分けて記述することができます。

print '1234567890' + \
      'abcdefghijklmnopqrstuvwxyz' + \
      'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
$ python multiline-sequence.py
1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

カッコで囲まれた部分は自動的に次の行とつながる

{}、()、[]など、カッコで囲まれたところが改行されるときは、バックスラッシュなしでも次の行とつなげてくれます。

# coding: utf-8
eto = ['ねずみ', 'うし', 'とら', 'うさぎ',
       'たつ', 'へび', 'うま', 'ひつじ',
       'さる', 'とり', 'いぬ', 'いのしし']   

どう書く? ピラミッドを作る

ピラミッドを作る どう書く?org

帰りの電車で見つけて楽しそうだったので書いてみた。

ピラミッドなので、一番下の階(?)からコツコツ建てていく感じです。

# pyramid.py
# coding: utf-8

"""num階建てのピラミッドを返す"""
def pyramid(num):
  return build_pyramid(ground(num))

"""num階建てピラミッドの一階を返す"""
def ground(num):
  return "*" * (num * 2 - 1)

"""下の階を受け取って一段上の階を返す"""
def next_floor(floor):
  return floor.replace("*", " ", 1)[:-1]

"""下の階を受け取って、その上に建つピラミッドを返す"""
def build_pyramid(under_floor):
  current_floor = next_floor(under_floor)
  if current_floor.strip() == "":
    return under_floor
  return build_pyramid(current_floor) + "\n" + under_floor

if __name__ == '__main__':
  import sys
  print pyramid(int(sys.argv[1]))

テスト

# test_pyramid.py
# coding: utf-8

from pyramid import *
import unittest

class TestPyramid(unittest.TestCase):
  def test_4_floor(self):
    self.assertEquals(_4_floor_pyramid(), pyramid(4))

  def test_3_floor(self):
    self.assertEquals(_3_floor_pyramid(), pyramid(3))

  def test_ground_of_4_floor(self):
    self.assertEquals("*******", ground(4))

  def test_ground_of_3_floor(self):
    self.assertEquals("*****", ground(3))

  def test_ground_of_0_floor(self):
    self.assertEquals("", ground(0))

  def test_next_floor_for_1st(self):
    self.assertEquals(" ***", next_floor("*****"))

  def test_next_floor_for_2nd(self):
    self.assertEquals("  *", next_floor(" ***"))

def _4_floor_pyramid():
  return \
    "   *\n" + \
    "  ***\n" + \
    " *****\n" + \
    "*******"

def _3_floor_pyramid():
  return \
    "  *\n" + \
    " ***\n" + \
    "*****"\

if __name__ == '__main__':
  unittest.main()

Keyクラスが2つある

Google App Engine PythonでデータストアのLow Level APIを触っていて、Keyクラスが2箇所で定義されていることに気が付きました。

from google.appengine.api.datastore import Key
from google.appengine.api.datastore_types import Key

datastoreモジュールとdatastore_typesモジュール。どっちからインポートしても同じように動作しました。クラスの別名みたいなものなのかな。

調べてみた

datastore_typesモジュールは、その名の通りデータストアに保存できる独自のデータ型を定義しているモジュールでした。「電話番号」型や「メールアドレス」型のようなクラスが並んでいます。
datastore_types.py
その中に「Key」クラスの定義もありました。データストアのデータ型としての「Key」クラスなので、おそらくReferencePropertyみたいな、他のエンティティを参照するデータ型に使われるんじゃないかなと思います。


datastoreモジュールの方にはKeyクラスの定義がありませんでした。「class Key」でgrepしても出てきません。しょうがないので「Key」で検索したら見つかりました。

Key = datastore_types.Key

datastore.py
なんだこりゃー!

びっくりした

pythonはリフレクションとかなしに「クラスへの参照」を変数に代入できるみたいです。クラスだけでなく関数やモジュールまでも、普通の代入で変数に入ります。関数が代入された変数は、そのまま()をつけるだけで実行することができました。
職業Javaプログラマにはちょっと刺激が強いです。。。

import datetime
mymodule = datetime
myclass = datetime.date
myfunction = datetime.date.today
myfunction() # datetime.date(2011, 4, 12)

本題に戻って

GAE/Pyでは「Key」クラスが以下の2モジュールで定義されている。

クラス定義はdatastore_typesモジュールの方で実装されていて、datastoreモジュールのKey変数がdatastore_types.Keyクラスを指している。実際はどちらも全く同じクラス。
モジュール名から予想するに、Keyクラスを他のエンティティのプロパティとして使用する場合はdatastore_typesモジュールからインポート。独立したエンティティがあって、それを識別するためのキーとして使用するならdatastoreモジュールからインポートするのがいいんじゃないかと思いました。

GAE/PyでLow Lovel API - エンティティの生成と保存と読み込み

Low Level APIでエンティティを生成したりデータストアに保存したりデータストアから読み込んだりする。

エンティティの生成

Entityクラスのコンストラクタにカインド名とIDまたはnameを渡すとエンティティを生成することができる。

from google.appengine.api.datastore import Entity

entity = Entity('mykind', id=1) # カインド名とIDを渡して生成
entity = Entity('mykind', name='myentity') # カインド名とnameを渡して生成


エンティティクラスはdictを継承しているので、dictオブジェクトと同様にキーと値のペアをデータとしてセットすることができる。セットしたキーと値のペアはデータストアに保存される。

from google.appengine.api.datastore import Entity

entity = Entity('mykind', id=1)
entity['firstname'] = 'Ishida'
entity['lastname'] = 'Makoto'


dictなのでupdate()関数とかkeys() value() items()といった関数を使うことができる。

from google.appengine.api.datastore import Entity

entity = Entity('mykind', id=1)
entity.update({
  'firstname': 'Ishida',
  'lastname': 'Makoto',
})

entity.keys() # ['firstname', 'lastname']
entity.values() # ['Ishida', 'Makoto']
entity.items() # [('firstname', 'Ishida'), ('lastname', 'Makoto')]

データストアにエンティティを保存する

datastoreモジュールのPut関数でデータストアにエンティティを保存する。

from google.appengine.api import datastore
from google.appengine.api.datastore import Entity

entity = Entity('mykind', id=1)
entity.update({
  'firstname': 'Ishida',
  'lastname': 'Makoto',
})

datastore.Put(entity) # データストアにエンティティを保存する

エンティティのリストを渡して、複数のエンティティを一気に保存することもできる。

from google.appengine.api import datastore
from google.appengine.api.datastore import Entity

entity1 = Entity('mykind', id=1)
entity2 = Entity('mykind', id=2)
entity3 = Entity('mykind', id=3)

datastore.Put([entity1, entity2, entity3]) # 複数のエンティティを一度に保存する


Put()関数は戻り値として保存したエンティティのキーを返す。複数エンティティを一度に保存した時はキーのリストを返す。このキーを使ってデータストアからエンティティを読み込むことができる。

データストアからエンティティを読み込む

datastoreモジュールのGet()関数にエンティティのキーを表すKeyオブジェクトを渡すことで、データストアからエンティティを読み込むことができる。指定したキーを持つエンティティがデータストアにない場合、EntityNotFoundErrorが投げられる。

from google.appengine.api import datastore
from google.appengine.api.datastore_types import Key

key = Key.from_path('mykind', 1)
entity = datastore.Get(key) # データストアからエンティティを読み込む

Get()関数も複数のキーを同時に指定して複数のエンティティを読み込むことができる。

from google.appengine.api import datastore
from google.appengine.api.datastore_types import Key

key1 = Key.from_path('mykind', 1)
key2 = Key.from_path('mykind', 2)
key3 = Key.from_path('mykind', 3)

[entity1, entity2, entity3] = datastore.Get([key1, key2, key3]) # データストアから複数のエンティティを読み込む

GAE/PyでLow Level API - Keyオブジェクトを触ってみる 2日め

本題とは関係ないけどKeyクラスにはto_path()という関数があって、バッククォート演算子よりもあっさりした文字列表現が得られるということをついさっき知りました。

バッククォート演算子

`key`
# datastore_types.Key.from_path(u'mykind', 1, _app=u'ishida-makot-py')


to_path()関数

key.to_path()
# [u'mykind', 1]


バッククォート演算子は「自身を表す文字列をpythonソースコードとして」返す演算子。クラス定義の「__repl__()」関数で実装されている。Python3ではなくなるみたい。
to_path()関数はKeyクラスに実装されている関数。カインド名と「IDか名前」でなるリストを返す。スッキリして見やすいのでこっちを使おう。

Keyのパス

Keyクラスはエンティティを一意に識別するための情報を「パス」っていうもので表現しているみたい。from_path()とto_path()の関数名にある「パス」「パス」っていうのはフォルダ階層みたいなツリー構造で要素の位置を示すために使う「/usr/local/bin」みたいな文字列だ。

['parentkind', 1, 'childkind', 1, 'grandchildkind', 1]

というリストになるので、これは

/親キー/子キー/孫(自分)キー

のようなイメージなのだ

データストアがエンティティを一意に識別するためのキー情報はツリー構造のパスみたいになってるんだな。

エンコードとデコード

Keyクラスは自分自身を文字列としてエンコードする機能を持っている。機能を持っているというか、、、__str__()関数がエンコードされた文字列を返すように実装されている。Keyの文字列表現はエンコードされた文字列なのだな。

デコードはクラスのコンストラクタで実装されてた。Keyクラスのコンストラクタにエンコードされた文字列を渡すと文字列がデコードされてKeyオブジェクトに復元される。

こんな感じ

    key = Key.from_path('mykind', 1)

    encoded = str(key)
    print encoded # agx0ZXN0YmVkLXRlc3RyEAsSBm15a2luZCIEbmFtZQw

    decoded = Key(encoded)
    print decoded.to_path() # ['mykind', 1]
 
    self.assertEqual(key, decoded)