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

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

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

Debian のインストール

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

sshiptables

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

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認証版)

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

$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せにゃならんのか?