PhantomJS 1.9.xでウェブフォントを使う方法

phantomjs

ヘッドレスブラウザのPhantomJSはWeb fonts未対応

Web fontsは昨今とても一般的に使われるようになった技術です。
例えば、Twitter Bootstrapfont awesomeなどで、いわゆる “Glyphicons” として使われることが多いように思います。

PhantomJSというGUIを持たないブラウザがあります。
このブラウザは、ウェブサービスのフロントエンドのテスト自動化にとても便利なツールです。
JavaScriptで自動化のための処理を記述できて、スクリーンキャプチャも出来てとても便利です。

Web fontsに対応させることは出来るが再ビルドが必要

しかしながら、現行の1.9.xではWeb fontsに対応していません。
理由は、WOFFファイルにPhantomJSがまだ対応していないためです。
Web fontsを有効にするための手順 がありますが、PhantomJSをビルドしなおす必要があります。
ビルドは結構時間がかかり、大変です。

ビルド済みWeb Fonts対応版PhantomJS

PhantomJS with Webfonts Support – Binary Build – Arunoda Susiripala – Web Geek というサイトでは、ビルド済みのWeb Fonts対応版PhantomJSを公開しています。バージョンは1.9.0のようです。

ビルド済みのWeb Fonts対応版PhantomJS 1.9.0をダウンロード

作者に感謝しつつ、使いましょう!

PhantomJS + CasperJSで テスト終了時に警告が出まくる問題への対処方法

phantomjs

警告の内容

CasperJSでテストを書いていると、スクリプト終了時にPhantomJSが次のような大量の警告を吐いて止まらなくなります。

Unsafe JavaScript attempt to access frame with URL about:blank from frame with URL ...
Domains, protocols and ports must match.

これではまともにテスト結果が見られない上、CIも回せません。

PhantomJS v1.9.8はバギーなのでv1.9.7を使う

以下のスレッドにヒントが書かれていました。

To anyone who has a problem with this. Downgrade PhantomJS to 1.9.7 and run casperjs with the –ssl-protocol=tlsv1 to be functionally completely the same as PhantomJS 1.9.8.
There won’t be an updated PhantomJS 1.x version which fixes this issue.

Mac OSXでの v1.9.7 のインストール法

brewだと1.9.8が入ってしまいます。
バイナリを本家から直接ダウンロードして使います。

ただし、最新版へのリンクしかありませんので、バージョン番号を変えて直接リンクを開いてください。
一応、こちらにリンクも貼っておきます。

brewでインストールした既存のphantomjsはアンインストールしておくことを忘れないでください。

[node.js] mongooseでクエリをタイムアウトさせるには

MongoDB

tl;dr

  • MongoDBではv2.6から cursor.maxTimeMS()によって処理をタイムアウトさせられる
  • 各ドライバから使用可能
  • mongooseでは mongoose.Query#maxTime メソッドで指定可能

サーバ側で処理をタイムアウトさせるには

MongoDBでは、v2.6からサーバ側で処理の実行タイムリミットを設定できます。
処理のタイムリミットは、cursor.maxTimeMS() メソッドを呼ぶことで設定できます。
この maxTimeMS を設定すると、指定したタイムリミットを超えて検索やアップデートが実行された場合、その処理を中止します。

以下のようにコンソールで試せます:

db.collection.find({description: /August [0-9]+, 1969/}).maxTimeMS(50)

各ドライバでのmaxTimeMSの使用方法

各ドライバといっても調べたものだけを列挙しておきます。

PHP

例:

$cursor = $collection->find();
$cursor->maxTimeMS(2000);

try {
    $results = iterator_to_array($cursor);
} catch (MongoExecutionTimeoutException $e) {
    echo "query took too long!";
}

ちなみに、PHPにはMongoCursor::timeout もあります。
このメソッドはクライアント側でタイムアウト処理を行うものです。
サーバ側では処理をキャンセルしないので、注意です。

node.js

こちらに詳しく書いてあります。
例:

var MongoClient = require('mongodb').MongoClient;

MongoClient.connect("mongodb://localhost:27017/test", function(err, db) {
    // Get an aggregation cursor
    var cursor = db.collection('data')
        .find({"$where": "sleep(1000) || true"})
        .maxTimeMS(50);

    // Get alll the items
    cursor.toArray(function(err, items) {
        console.dir(err);
        console.dir(items);
        db.close();
    });
});

mongooseでタイムアウトさせるには

MongoDBのnode.jsにおけるODM(Object Data Mapping)のmongooseでもmaxTimeMSに対応しています。
v3.8.13 から利用できるようになりました。
該当の変更は こちら
実際には子分モジュールの mquery で実装されています。

使い方はシンプル:

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var Cat = mongoose.model('Cat', { name: String });
Cat.find({name: 'Zildjian' }).maxTime(1000).exec(function(err, docs) {
    console.log(err); // --> { [MongoError: operation exceeded time limit] name: 'MongoError' }
});

operation exceeded time limit というメッセージのエラーが得られます。

このように、mongoose.Query オブジェクトを返すメソッドでmaxTimeメソッドが使えます。
Model#saveメソッド、Model#remove では残念ながら使えないようです。

エラーを判別しにくい問題

Errorオブジェクトにはエラー種別を明示的に判別するコードがありません。
そのため、とても不安な方法ですがエラーメッセージで判別する方法しか見当たりません(他に良い方法をご存知の方がおられましたらぜひご教授ください)。
メッセージによるタイムアウト判別は以下のようになります:

if(err.message === 'operation exceeded time limit') {
   // retry
}

なぜPHPにはMongoExecutionTimeoutExceptionがあるのにnode.jsでは無いんだ?

参考

WebpackベースのアプリケーションをKarmaでテストする

banner

Webpackベースのアプリケーションをテストしたい

Webpackとは

Webpackはモジュールの依存関係を解決するプリプロセッサです。
依存関係の記述には、ES6、CommonJSやAMDのスタイルが使えます。
誤解を恐れずに言えば、node.js感覚で他のモジュールをrequireできるようにしてくれます。
webpackでソースコードをコンパイルすると、*.bundle.jsというような名前のファイルが出力されます。
これをアプリケーション側で事で、同期的・非同期的なモジュールの依存関係の解決が可能となります。

テストしやすい方法の検討

要件としては以下のとおりです:

  1. Webpackで書いたアプリケーションの各ユニットあるいは各ふるまいを、出来るだけ簡単にテストしたい
  2. 任意のCI(Continuous Integration)でも動かしたい

使用するツール群

前述の要件を満たすツールの組み合わせがたくさんあります。
今回は以下の構成でテストを走らせてみます:

  • Karma – テストランナー
  • PhantomJS – ヘッドレスブラウザ
  • Mocha – テストフレームワーク
  • Chai – アサーション
  • webpack – プリプロセッサ
  • bower – パッケージ管理

各ツールの詳しい説明は、それぞれの本家をご参照ください。

テストの実施準備手順

設定項目や手順の意味を理解するために、敢えてバラバラに分けて説明します。
筆者はMac OS Xの環境で行いました。

必要なパッケージのインストール

$ npm install --save-dev 
    karma 
    karma-chai 
    karma-mocha 
    karma-phantomjs-launcher 
    karma-webpack

Karmaの設定

karma.conf.jsで以下のファイルを作成します。

// Karma configuration
// Generated on Wed Nov 26 2014 23:12:23 GMT+0900 (JST)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha', 'chai'],


    // list of files / patterns to load in the browser
    files: [
      'test/**/*_test.js'
    ],


    // list of files to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      // add webpack as preprocessor
      'test/**/*_test.js': ['webpack', 'sourcemap']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // karma watches the test entry points
    // (you don't need to specify the entry option)
    // webpack watches dependencies
    // webpack configuration
    webpack: {
    }

  });
};

Bowerのモジュールを読み込めるようにする

設定ファイル冒頭に以下を追記します:

var webpack = require("webpack");
var path = require("path");
var bowerPath = path.join( __dirname, "bower_components" );

webpackの設定に以下を追記します(参考: usage with bower)

    webpack: {
      resolve: {
        // Tell webpack to look in node_modules, then bower_components when resolving dependencies
        // If your bower component has a package.json file, this is all you need.
        modulesDirectories: ["node_modules", bowerPath]
      },
      // Define a new plugin that tells webpack to look at the main property in bower.json files when resolving dependencies.
      // For marionette, we need it to load the CJS version, which we specify with as ["main", "1"] in the args below.
      plugins: [
        new webpack.ResolverPlugin([
          new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin( "bower.json", ["main", ["main", "1"]] )
        ], ["normal", "loader"])
      ]
    }

これで、テストのソースコードからbowerのモジュールをrequire("jquery");というように記述して読み込めるようになりました。

テスト対象のサンプルモジュール

簡単なjqueryを用いるサンプルモジュールです。
index.js で以下のようなファイルを用意してください。

var $ = require('jquery');

module.exports = function() {
  var div = $('<div />');
  $('body').append(div);
  return div;
};

依存関係にある jquery をbowerでインストールしておきます:

$ bower install --save jquery

テストの用意

test/main_test.js で以下のようなファイルを作成します:

var createElement = require('../index.js');

describe('jQuery sample', function() {

  it('should create div element', function() {
    expect(createElement).to.be.Function;
    var div = createElement();
    expect(div).to.be.Array;
  });

});

処理内容としては、先ほど作成したサンプル関数(createElement)を読み込みます。
次にその関数を呼び出し、結果を検査しています。

テストの実行

以下のコマンドを実行します:

$ karma start

結果:

$ karma start
INFO [karma]: Karma v0.12.28 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
Hash: 606dfda4c9d20a78987a
Version: webpack 1.4.13
Time: 8ms
webpack: bundle is now VALID.
webpack: bundle is now INVALID.
Hash: 1b62ba3e4c6b38edee7e
Version: webpack 1.4.13
Time: 944ms
                Asset    Size  Chunks             Chunk Names
_js/test/main_test.js  693312       0  [emitted]  test/main_test.js
chunk    {0} _js/test/main_test.js (test/main_test.js) 247722 [rendered]
    [0] ./test/main_test.js 245 {0} [built]
    [1] ./index.js 126 {0} [built]
    [2] ./bower_components/jquery/dist/jquery.js 247351 {0} [built]
webpack: bundle is now VALID.
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket Mfny5tbQCtBeW9kEu0TR with id 80467111
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 SUCCESS (0.011 secs / 0.002 secs)

動きましたね!

さらに改善する

ソースマップ

Webpackでコンパイルしたテストがコケた時、示される行番号が参考にできません。
ソースマップを有効にすることで、コンパイル前の行番号を取得できます。

$ npm install --save-dev karma-sourcemap-loader

preprocessorsに加えます:

preprocessors: {
    'test/test_index.js': ['webpack', 'sourcemap']
}

また、webpackにソースマップを生成するよう設定します:

webpack: {
  // ...
    devtool: 'inline-source-map'
}

実行すると以下のように元の位置が併せて示されます:

PhantomJS 1.9.8 (Mac OS X) Hello test Is it right? FAILED
        TypeError: '[object Object]' is not a function (evaluating 'should(T)')
            at /Users/nora/Development/tmp/karma-test2/test/main_test.js:53:0 <- webpack:///./test/main_test.js:7:0
PhantomJS 1.9.8 (Mac OS X): Executed 1 of 1 (1 FAILED) ERROR (0.006 secs / 0 secs)

npmを宇宙に見立ててパッケージと依存関係をビジュアライズする「npm universe」

npm-university-1

npm-university-2

npm名前空間を宇宙、パッケージを星として、依存関係を線で繋ぎ描写しています。
まるで宇宙にインクをこぼしたかのような印象。
有名なパッケージ名で検索すると面白いですよ!

操作方法

  • WASDキーで移動
  • Lキーで依存関係の表示切り替え
  • Spaceキーでステアリングモードの切り替え

参考

chef-clientが処理の途中で403 Forbiddenエラーになる時の対処

Chef

認証に問題が無いのに404 Forbiddenになる

chef-clientを走らせていて、以下のようなエラーにたまに出くわします:

198.xx.xxx.xxx ================================================================================
198.xx.xxx.xxx Error executing action `create` on resource 'template[/var/www/mysite.com/shared/config/database.yml]'
198.xx.xxx.xxx ================================================================================
198.xx.xxx.xxx
198.xx.xxx.xxx Net::HTTPServerException
198.xx.xxx.xxx ------------------------
198.xx.xxx.xxx
198.xx.xxx.xxx 403 "Forbidden"
198.xx.xxx.xxx
198.xx.xxx.xxx
198.xx.xxx.xxx Resource Declaration:
198.xx.xxx.xxx
198.xx.xxx.xxx ---------------------
198.xx.xxx.xxx # In /var/chef/cache/cookbooks/rails/recipes/production.rb
198.xx.xxx.xxx
198.xx.xxx.xxx  40: template node[:rails][:app_root]+"/shared/config/database.yml" do
198.xx.xxx.xxx  41:     owner "root"
198.xx.xxx.xxx  42:     group "xx-dev"
198.xx.xxx.xxx  43:     mode 0775
198.xx.xxx.xxx  44: end
198.xx.xxx.xxx  45:
198.xx.xxx.xxx
198.xx.xxx.xxx Compiled Resource:
198.xx.xxx.xxx
198.xx.xxx.xxx ------------------
198.xx.xxx.xxx
198.xx.xxx.xxx # Declared in /var/chef/cache/cookbooks/rails/recipes/production.rb:40:in `from_file'
198.xx.xxx.xxx
198.xx.xxx.xxx template("/var/www/mysite.com/shared/config/database.yml") do
198.xx.xxx.xxx   provider Chef::Provider::Template
198.xx.xxx.xxx   action "create"
198.xx.xxx.xxx   retries 0
198.xx.xxx.xxx   retry_delay 2
198.xx.xxx.xxx   path "/var/www/mysite.com/shared/config/database.yml"
198.xx.xxx.xxx   backup 5
198.xx.xxx.xxx   source "database.yml.erb"
198.xx.xxx.xxx   cookbook_name "rails"
198.xx.xxx.xxx   recipe_name "production"
198.xx.xxx.xxx   mode 509
198.xx.xxx.xxx   owner "root"
198.xx.xxx.xxx   group "xx-dev"
198.xx.xxx.xxx end
198.xx.xxx.xxx
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] ERROR: Running exception handlers
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Saving node information to /var/chef/cache/failed-run-data.json
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] ERROR: Exception handlers complete
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
198.xx.xxx.xxx [2014-05-29T20:16:34+00:00] FATAL: Net::HTTPServerException: template[/var/www/mysite.com/shared/config/database.yml] (rails::production line 40) had an error: Net::HTTPServerException: 403 "Forbidden"

しかし、最初はうまく走っていたので、認証周りで何かおかしいとは考え難い現象です。

no_lazy_loadを指定してみる

もしあなたのchef-clientが、十数分とか長い時間動作しているとしたら、以下のフラグをclient.rbに追記すると改善するかもしれません:

no_lazy_load true

chef-clientは必要に応じてcookbook_filetemplateを取得します(lazy-load)。
しかし、長い時間走らせていると、認証がタイムアウトするようです。
これを防ぐため、no_lazy_load trueを指定することでlazy-loadを無効にします。
これによって、はじめに必要なデータをすべて取得するようになります。

参考

[Mac OSX] VPNが頻繁に切断されてしまう時の対処方法

OpenVPN-Logo-200x200

社内へのアクセスにVPNを利用している人は多いと思います。
Mac OSXは標準でPPTPやL2TPなどのプロトコルに対応しています。
しかしながら、設定内容は合っているのに接続直後に切断されてしまったり安定しないケースがしばしばあります。
そんな時の対処方法をまとめました。
以下の方法についてそれぞれ説明します。

  1. MTU値を調整する
  2. PPTPマルチパススルー機能のついたルータを買う
  3. 自動再接続スクリプトを書く

MTU値を調整する

個人的にはこの方法が最も有効です。
MTU(Maximum Transmission Unit)とは、一度にネットワークへ送信できる最大のパケットサイズの事です。
大きいほど一度に沢山のデータを送信できますが、エラー率も上がり再送信コストが大きいというトレードオフがある設定値です。
これを調整する事で、切断の頻度を改善できます。

現在のMTU値を確認

まずは、現在のMTU値を確認しましょう。
「ターミナル.app (Terminal.app)」を開いて以下のコマンドを入力してください。

$ sudo ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether xx:xx:xx:xx:xx:xx
    inet6 xxxx::xxxx:xxx:xxxx:xxx8%en0 prefixlen 64 scopeid 0x4
    inet 10.200.2.130 netmask 0xffff0000 broadcast 10.200.255.255
    nd6 options=1<PERFORMNUD>
    media: autoselect
    status: active
ppp0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
    inet 10.100.0.5 --> 10.255.254.0 netmask 0xffff0000

en0 はイーサネットのネットワークです。つまりLANですね。 ppp0 はPPTPのVPNです。
そして、en0のMTUは 1500 で、 ppp0のMTUは1500 である事が確認できます。
問題はこのppp01500という値です。

MTU値を変更

/etc/ppp/ip-up というパスのファイルを作成して、以下の内容を記述します。

#!/bin/sh

/sbin/ifconfig ppp0 mtu 1300

また、ファイルのパーミッションを755 に変更してください。

$ sudo chmod 755 /etc/ppp/ip-up

ip-upというファイルは、VPNに接続するたびに実行されるスクリプトです。
これで、接続時にMTU値が1300に調整されるようになりました。
試しに再接続してみてください。
この値をいろいろ変えてみて、安定する値を探ってください。

僕の場合はMac OSX Yosemiteで、 500 で安定しています。

PPTPマルチパススルー機能のついたルータを買う

複数人がVPNに接続する、または複数台が同時にVPNに接続する環境の人に有効です。

ルータは、VPNのパケットをアドレス変換(NAT)しないで通す機能があり、これをPPTPパススルーと言います。
家庭用のルータなどでは、この機能は1台までしか使えず、同時に複数の接続があると正しく対処できません。
そのため、同時接続して通信すると混線が発生して頻繁な切断を引き起こします。
この問題は、PPTPマルチパススルーに対応したルータを使う事で解決します。

自動再接続スクリプトを書く

上記2つのアプローチでも尚安定しない場合は、自動で再接続するApple Scriptを書くのが有効です。
簡単ですので、試してみてください。Yosemiteでも動作確認済みです。

ネットワーク名を確認する

VPN-network-preferences

この画面では、VPN接続名は「VPN (PPTP)」である事がわかります。
これを覚えておきます。

AppleScriptを書く

以下のようなアイコンの「アプリケーション」→「ユーティリティ」→「スクリプトエディタ」を開きます。

Script Editor

新規作成して、以下のスクリプトを貼り付けてください。

on idle
  tell application "System Events"
    tell current location of network preferences
      set myConnection to the service "VPN (PPTP)"
        if myConnection is not null then
          if current configuration of myConnection is not connected then
            connect myConnection
          end if
        end if
      end tell
    return 120
  end tell
end idle

「VPN (PPTP)」のところを、あなたのVPN接続名に置き換えてください。
以下のような画面になります。

VPN-Script Editor

できたら、以下のような設定で保存します。

Script Editor Export

スクリプトを起動する

保存したスクリプトを、VPNが切断された状態で起動してみてください。
自動でVPNが接続されましたか?やりましたね!
ためしに手動で切断してみてください。しばらくすると、再接続を試みるはずです。
接続状態をチェックする間隔は2分間ですが、変更できます。
スクリプト中のreturn 120という箇所を、任意の秒数に変更してください。

参考