あふで peco を使う

Windows ファイラーの あふw で、peco を使ってフォルダ履歴をいい具合に使いたい。業務上、あっちこっちの Windows Server を UNC パスによりあふで見まくってて、よく使うフォルダ登録だけではきつくなってきたので、履歴からさくっとアクセスできるといいなぁと。

ということで、いろいろやってたら、bat 書いて、perl 書いて、jscript 書いての実装となってしまったが、非常に実用的になった。migemogrep も併せると日本語フォルダにも対応できて最高。

以下のとおり実装した。それぞれのスクリプトは C:\local\bin\afxw\script に置く。

afxw-peco.bat

このバッチはあふから実行するもの(Shift-h に割り当て)で、あふのフォルダ履歴を peco に食わせて、絞り込んだ結果を js を介してあふに渡す。js に渡す前に iconv で cp932 に変換する。Windows バッチの書き方がいまいちよくわからん…。

@echo off

cd /d C:\local\bin\afxw\script

for /f "delims=" %%i in ('perl afxw-hist.pl ^| peco ^| iconv -f UTF-8 -t CP932') do (
    cscript afxw-cd.js "%%i"
    break
)

afxw-hist.pl

あふのオートメーションサーバ機能でフォルダ履歴を取ってきて、出力する。さくっと Perl で。

#!/usr/bin/env perl

use 5.010;
use strict;
use warnings;
use utf8;
use Encode;
use Win32::OLE;

my $afxw = Win32::OLE->new('afxw.obj');

my (@dirs, %h);
for my $window (0, 1) {
    my  $count = $afxw->HisDirCount($window);
    for (my $i = 0; $i < $count; $i++) {
        my $dir = decode('cp932', $afxw->HisDir($window, $i));
        push @dirs, $dir unless $h{$dir};
        $h{$dir} = 1;
    }
}

say encode('utf8', $_) for @dirs;

afxw-cd.js

指定したフォルダをあふのオートメーションサーバ機能で開く。

var args = WScript.Arguments;
var dir = args(0);

var afxw = WScript.CreateObject("afxw.obj");
var cd = '&EXCD -P"' + dir + '"';
afxw.Exec(cd);

afxw.ini

フォルダ履歴はデフォルト 36 個なので、増やす。あまり多すぎると重くなるのかしら。

HISMAX=4096

afxw.key

あふで Shift-h すると、peco (コマンドプロンプト) が起動するように。

K0026="1072C:\local\bin\afxw\script\afxw-peco.bat"

Vimperator で読み込む設定ファイルを分岐する

.vimperatorrc を git で管理してて、家の Mac と職場の Windows で設定ファイルを共用してる。

で、C-k で Emacs よろしくなことがしたいため、以下を設定したところ、Mac での挙動がおかしくなるので、これを Windows のみに適用したい。

inoremap <C-k> <S-End><C-x>
cnoremap <C-k> <S-End><C-x>

調べたら services.get("environment") で環境変数が取れるようなので、次のようにすれば Windows のときのみそれ用の設定ファイルを読み込むができ、それに上記の設定を書けばいい感じになった。

javascript <<EOM
if ( services.get("environment").get("OS").match(/Windows/i) ) {
  io.source("~/vimp/_win_vimperatorrc", false);
}
EOM

追記

コメントで config.features を教えていただいた。で、ソースを読んでたら

has: function (feature) config.features.indexOf(feature) >= 0

とあったので、次のようにするとスマート!

javascript <<EOM
if ( liberator.has("Windows") ) {
  io.source("~/vimp/_win_vimperatorrc", false);
}
EOM

ビジュアルモードで選択した要素の最後にキャレットを移動する

ブラウザで長い文章を読むとき、読んでるパラグラフなどを選択しながら読む癖があって、vimperator だと c v hjkl でやるわけだが、ずっと選択しっぱなしがイヤなので、ほどほどのところで Esc して選択解除したりする。するとキャレットが選択要素の最初になって、どこまで読んだかわからなくなりキーッとなって、これが相当なストレスであった。

で、なにげに caret-hint.js を読んでたら、こんな感じでやればいいのかとヒントを得た(まさに caret-hint !)ので、次のとおり書いてみた。というか、ほとんど caret-hint.js のパクリだけど。これで c v hjkl で選択していって、適当なところで n すると選択範囲の最後にキャレットが移動して、捗るわー。

mappings.addUserMap(
  [modes.VISUAL],
  ['n'],
  'Move selection tail',
  function () {
    let win = new XPCNativeWrapper(window.content.window);
    let s = win.getSelection();

    if (s.rangeCount <= 0)
       return false;

    let f = [s.focusNode, s.focusOffset];
    s.collapse.apply(s, f);
  }
);

さらに、次を定義すると m したら最後にキャレットが移動した後ビジュアルモードを終えるようになり、n と使い分けるとなかなかいい感じ。

vmap m n<ESC><CR>

さくらVPS 2G で VirtualBox による Windows 環境の構築

これまで「さくら VPS 512」を2つ契約してて Debian と Windows をそれぞれ使っていたが、2012-03-29 にさくら VPS がリニューアルして、メモリもハードディスクも増量し、2G プランも登場したことから、仮想化でやれば 2G プラン1本でいけるんじゃないかと思い、移行してみた。

さくら VPS の OS は Debian とし、それに仮想化ソフトウェア VirtualBox を入れ、ゲスト OS として Windows XP x86 を構築する。さくら VPS は KVM なので、Windows は仮想化の仮想化での動作となる。

ちなみに、仮想化された Windows クライアントにアクセスするには Windows SA または Windows VDA のライセンスが必要となる。

Debian のインストール

まずは普通に Debian squeeze amd64 をインストールする(i386 のほうがよかったかもしれないが、どうなんだろ)。VirtualBox を GUI でサクッとやるため、インストール時にデスクトップ環境も入れる。

ssh と iptables

OS インストール後、なにはともあれ ~/.ssh/authorized_keys に公開鍵を登録し ssh と iptables を設定する。

ssh のポート番号を 25622 に変更し、公開鍵認証のみとする。

$ sudo vi /etc/ssh/sshd_config
Port 25622
Protocol 2
PermitRootLogin no
RSAAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no

$ sudo /etc/init.d/ssh restart

/etc/network/if-pre-up.d に iptables のスクリプトを置き、ssh だけ開ける。

$ sudo vi /etc/network/if-pre-up.d/firewall
#!/bin/sh

IPTABLES='/sbin/iptables'

# Remove All Rule
$IPTABLES -F
$IPTABLES -Z
$IPTABLES -X

# Drop ALL
$IPTABLES -P INPUT DROP
$IPTABLES -P FORWARD DROP
$IPTABLES -P OUTPUT ACCEPT

# Accept Loopback
$IPTABLES -A INPUT  -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT
$IPTABLES -A OUTPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT

# PING
$IPTABLES -A INPUT -p icmp --icmp-type 0 -j ACCEPT
$IPTABLES -A INPUT -p icmp --icmp-type 8 -j ACCEPT

# SSH
$IPTABLES -A INPUT -p tcp --dport 25622 -j ACCEPT

$ sudo chmod +x /etc/network/if-pre-up.d/firewall
$ sudo /etc/network/if-pre-up.d/firewall

VirtualBox 4.1 のインストール

Oracle VM VirtualBox から deb パッケージを落としてきて、dpkg でインストールする。依存関係で怒られたので、apt-get -f upgrade する。

$ wget http://download.virtualbox.org/virtualbox/4.1.12/virtualbox-4.1_4.1.12-77245~Debian~squeeze_amd64.deb
$ sudo dpkg -i virtualbox-4.1_4.1.12-77245\~Debian\~squeeze_amd64.deb
$ sudo apt-get -f upgrade
$ sudo usermod -a -G vboxusers username

VirtualBox への Windows x86 のインストール

GNOME デスクトップの [アプリケーション]-[システムツール]-[Oracle VM VirtualBox] から VirtualBox をいぢるわけだが、X の解像度が 800x600 でつらい。xorg.conf を作ってゴニョるも、うまく解像度を上げることができない…。VirtualBox の設定さえ終えたら X は殺すのでまぁいいかと深追いせず進める。下図のとおり VirtualBox 画面右下の Cancel, OK が隠れてどっちがどっちかわからんで困ったけども…。

で、ウィザードで新規仮想マシンを作り、以下のような構成とした(ほとんどデフォルト)。

OS Windows XP x86
メモリ 768MB
ハードディスク 30GB, VDI, Dynamically allocated
チップセット PIIX3, IO APIC オフ, EFI オフ
プロセッサ数 1, PAE/NX オフ
ビデオメモリ 16MB, 3D/2D オフ
ストレージ PIIX4
ネットワーク NAT, ホストオンリー

プロセッサ数は2個にしたかったが、KVM 上のため仮想化支援機能が有効でないので、残念ながら1個の割り当てにしかできない。ネットワークはグローバル IP アドレスが1つしかもらえないので当然 NAT になるが、ホストからゲストにアクセスしたい (後述) ので、ホストオンリーの NIC を追加する (DHCP ではなくstatic で設定した)。

あとは iso からブートして普通に Windows をインストールすれば出来上がり。Windows のインストール後、[デバイス]-[Guest Additions のインストール] を実行し、ドライバ類をインストールする。

リモートデスクトップ

外部から Windows を操作するため、リモートデスクトップ機能を使いたい。VirtualBox には VRDP 機能 (リモートディスプレイ) があるが、ゲストは Windows なので直接 MS-RDP でやることにする。試してないけど、たぶんそのほうが性能的にもいい気がする。

また、ゲストは NAT なので、外部から直接アクセスするには VirtualBox のポートフォワーディング機能を使うのが一般的だろうが、せっかく Debian の裏に隠れているので、ssh トンネリングでリモートデスクトップ接続することにした。通信も暗号化されるし。といった理由で、ホスト (Debian) からゲスト (Windows) に通信できるようにするため、前述のとおり、ゲストにホストオンリーのネットワークを追加した。

ゲストのホストオンリー NIC の IP アドレスが 192.168.56.101 のとき、PuTTY の場合、次のように設定すれば、ssh 接続後 mstsc /v localhost:33891 で Windows にリモートデスクトップ接続できるようになる。

L33891 192.168.56.101:3389 

Mac とかの場合 ~/.ssh/config に次のように書くと楽ちん。

Host debian
  HostName example.jp
  User username
  Port 25622
  LocalForward 33891 192.168.56.101:3389
  IdentityFile ~/.ssh/id_rsa

Debian の不要なサービスの停止

初期設定さえ済んでしまえば Debian のウィンドウシステムは不要なので gdm3 を停止する。併せて、以下の不要なサービスを止める。sysv-rc-conf で止めるとよい。

  • bluetooth
  • avahi-daemon
  • cups
  • gdm3
  • saned
  • network-manager
  • fancontrol
  • loadcpufreq
  • cpufrequtils
  • portmap
$ sudo apt-get install sysv-rc-conf
$ sudo sysv-rc-conf

コマンドによる VirtualBox の操作

VirtualBox はコマンドが充実している。以下のコマンドで仮想マシンの起動・停止ができる。xp のところは仮想マシンの vmid を。

起動
$ VBoxManage startvm xp --type headless
停止 (というか状態保存して停止)
$ VBoxManage controlvm xp savestate
起動している仮想マシンの表示
$ VBoxManage list runningvms

自動起動と自動停止

上記のコマンドを叩くスクリプトを作って /etc/init.d に置きランレベル2に登録すれば、ホストの起動時にゲストも自動起動できる。スクリプトは http://www.glump.net/howto/virtualbox_as_a_service#create_the_initd_script あたりがいいかも。これを使う場合、Required-Start の vboxnet を削除して、vboxballoonctrl-service を追加する必要がある。

$ sudo insserv virtualbox-xp

なお、上記スクリプトによりホスト停止時に savestate されるが、以下のとおり /etc/default/virtualbox を作っておけば、/etc/init.d/vboxdrv によりホストを落としたときに savestate してくれる。

$ sudo vi /etc/default/virtualbox
SHUTDOWN_USERS="username"
SHUTDOWN=savestate

ベンチマーク

参考までに、これまでのネイティブ(?)な on KVM と今回の on VirtualBox on KVM でのベンチマークを取ってみた。

on KVM


on VirtualBox on KVM

数値で示されているとおり、さすがに VirtualBox を挟んでいるため半分以下の性能になっており、実際も VPS ネイティブな環境に比べると少々もっさり感があるが、ガリガリ使うわけではないし、主な用途である TARGET (競馬ソフト) の操作や JRA-VAN DataLab. データの PostgreSQL (on Debian) への登録など、特に遅延することはなく動作に問題はない。

まとめ

「さくらVPS 512」2台で運用してたものが「さくらVPS 2G」1台で運用できるようになり、月額1,960円 (980 * 2) が月額1,480円とコスト削減が図れ、お財布的にも優しい構成となった。また、現在のさくら VPS ではできない仮想マシンの管理ができるようになり、バックアップや今後の移行も容易となった。

これもひとえにさくらのサービス向上によるもので、ほんとありがたい。

ビバ、仮想化 !

Read It Later からはてブに登録する

多くの iPhone アプリでは、あとで読む系は Read It Later (以下 RIL) が多いので、RIL に流しているが、通常ははてブを使っているため、RIL に登録した URL をはてブに集約したい。というか、仕事から現実逃避したいので、コード書きたい。

というわけで、以下のようなスクリプトを作り、cron で回して、昨日以降に RIL に登録した URL を自動的にはてブに登録するようにしたった。なお、RIL の API にアクセスするため、API Key を取得する必要がある。

#!/usr/bin/perl

# Read It Later に昨日以降登録されたものを
# Hatena Bookmark に登録する

use 5.010;
use strict;
use warnings;
use utf8;
use Data::Dumper;
use JSON;
use DateTime;
use DateTime::TimeZone;
use LWP::Simple;
use LWP::UserAgent;
use WebService::Hatena::Bookmark::Lite;

my $ril_url  = 'https://readitlaterlist.com/v2/get?username=%s&password=%s&apikey=%s&since=%s';
my $ril_user = 'user';
my $ril_pass = 'password';
my $ril_key  = 'apikey';
my $hatena_user = 'user';
my $hatena_pass = 'password';

my $tz = DateTime::TimeZone->new(name => 'Asia/Tokyo');

# 昨日のエポック秒
my $yesterday = DateTime->today(time_zone => $tz)->subtract(days => 1);
my $ril_since = $yesterday->epoch();

# RIL から JSON を取得する
my $ril = sprintf($ril_url, $ril_user, $ril_pass, $ril_key, $ril_since);
my $json = get($ril);

# こんな JSON が返ってくる
#my $json = '{"status":1,"list":{"74006":{"item_id":"74006","title":"Google","url":"http:\/\/www.google.co.jp\/","time_updated":"1325814579","time_added":"1325814579","state":"0"}},"since":1325737324,"complete":0}';

my $data = decode_json($json);
my $list = $data->{list};

my $bookmark = WebService::Hatena::Bookmark::Lite->new(
    username  => $hatena_user,
    password  => $hatena_pass,
);

# time_added でソート
for my $key ( sort { $list->{$a}{time_added} <=> $list->{$b}{time_added} } keys %$list ) {
    my $url = $list->{$key}{url};
    $url = expand_uri($url) if $url =~ m|^http://t\.co/|; # t.co は展開する

    # 登録された日時をコメントにする
    my $dt = DateTime->from_epoch(
        epoch => $list->{$key}{time_added},
        time_zone => $tz,
    );
    my $comment = $dt->strftime('%Y-%m-%d %H:%M:%S');

    $bookmark->add( url => $url, comment => $comment );
    say $comment;
    say $url;
}

# 短縮 URL を展開する
sub expand_uri {
    my $uri = shift;

    my $ua = LWP::UserAgent->new;
    my $res = $ua->head($uri);
    return $res->request->uri;
}

フォロワーの差分(OAuth 版)

http://d.hatena.ne.jp/chabom/20100818/1282092637 のコードはBasic 認証でもう使えないので、OAuth 対応した。

利用するには、Consumer key, Consumer secret, Access Token, Access Token Secret は Twitter Developer Platform — Twitter Developers でアプリを登録して取得する必要があります。

signature 作るところで、16 進表記は大文字にしないといけないのに気づかずにハマッた。System.Web.HttpUtility.UrlEncode は小文字で出力される。バカチン。

Hexadecimal characters in encodings MUST be upper case

http://oauth.net/core/1.0/#encoding_parameters
$API_URL    = "https://api.twitter.com/1/statuses/followers.xml"
$API_METHOD = "GET"
$OLD_FILE   = "followers_old.txt"

# System.Web.HttpUtility.UrlEncode だと 16 進数が小文字になってダメなので
function Encode-URL($s){
    ($s.ToCharArray() | foreach {
			if ($_ -match "[a-z]|[-,_,.,~,)]|[0-9]") { 
				$_
			} else {
				"{0}{1:X}" -f "%", [System.Text.Encoding]::ASCII.GetBytes($_)[0]
			}
		}) -join ""
}
	
function Create-OAuthHeader {
	param ( [string]$url, [string]$method )

	$CONSUMER_KEY    = "Consumer key"
	$CONSUMER_SECRET = "Consumer secret"
	$TOKEN           = "Access Token"
	$TOKEN_SECRET    = "Access Token Secret"

	# unix-time
	$timestamp = [int](((Get-Date) - [DateTime]"1970/01/01 09:00:00").TotalSeconds)

	$rand = New-Object System.Random
	$nonce = $rand.next(123400, 9999999).ToString()

	$params = @("oauth_consumer_key=$CONSUMER_KEY",
							"oauth_nonce=$nonce",
							"oauth_signature_method=HMAC-SHA1",
							"oauth_timestamp=$timestamp",
							"oauth_token=$TOKEN",
							"oauth_version=1.0") | sort
	
	# signature を作る
	$base = @( $method, (Encode-URL $url), (Encode-URL ($params -join "&")) )
	$base = $base -join "&"
	$hmac = New-Object System.Security.Cryptography.HMACSHA1
	$hmac.Key = [System.Text.Encoding]::ASCII.GetBytes($CONSUMER_SECRET + "&" + $TOKEN_SECRET)
	$hash = $hmac.ComputeHash( [System.Text.Encoding]::ASCII.GetBytes($base) )
	$signature = Encode-URL( [System.Convert]::ToBase64String($hash) )
	
	$params += "oauth_signature=$signature"
	return $params -join ","
}

$wc = New-Object System.Net.WebClient

$users = @()
$cursor = -1
while ($cursor -ne 0) {
	$header = Create-OAuthHeader $API_URL $API_METHOD
	$wc.Headers.Set("Authorization", "OAuth $header")

	$xml = [xml] $wc.DownloadString($API_URL + "?cursor=$cursor")
	$cursor = $xml.users_list.next_cursor

  # $users に 現在のfollowersを入れる
	$xml.users_list.users.user | foreach { $users += $_.screen_name }
}
$wc.Dispose()

# diff
Compare-Object $(Get-Content $OLD_FILE) $users -SyncWindow 100

# 現在のfollowersをold.txtに出力する
Set-Content -Path $OLD_FILE -Value $users

"current followers: " + $users.length

フォロワーの差分(Basic認証版)

このコードは、TwitterのBASIC認証廃止に伴い利用できません。

$URL = "http://twitter.com/statuses/followers.xml?cursor="
$USER = "username"
$PASSWD = "password"
$OLD_FILE = "followers_old.txt"

$nc = New-Object System.Net.NetworkCredential($USER, $PASSWD)
$wc = New-Object System.Net.WebClient
$wc.Credentials = $nc

$users = @()
$cursor = -1
while ($cursor -ne 0) {
	$xml = [xml] $wc.DownloadString($URL + $cursor)
	$cursor = $xml.users_list.next_cursor

  # $users に 現在のfollowersを入れる
	$xml.users_list.users.user | foreach { $users += $_.screen_name }
}
$wc.Dispose()

# diff
Compare-Object $(Get-Content $OLD_FILE) $users -SyncWindow 100

# 現在のfollowersをold.txtに出力する
Set-Content -Path $OLD_FILE -Value $users

"current followers: " + $users.length

ってか、結局、Basic認証っていつまで使えるのだろう? こういうのもいちいちOAuthせにゃならんのか?