Hatena::ブログ(Diary)

Okiraku Programming RSSフィード Twitter

2017-04-24 Node.jsでUNIX-likeにパイプ処理

Node.jsでUNIX-likeにパイプ処理

巨大なテキストデータ標準入力(pipe等)から受け取り、1行ずつ何か処理をして、結果を標準出力(これもpipe等)に書くプログラムを書こうとしてハマるパターン。

(なお文字コードのことはここでは忘れたことにするので、別途対応が必要かもしれない。)


例えば、ssh remote-host cat huge-file.txt | node process-data.js | xz -c > compressed-data.xz などと使うような想定。



ダメな例1:
var chunk;
while(chunk = process.stdin.read())
  process.stdout.write(`data: ${chunk}`);
}
{ Error: EAGAIN: resource temporarily unavailable, read errno: -35, code: 'EAGAIN', syscall: 'read' }

process.stdin.fdは非同期openされたfdを返すため、fs.readするとpipeにデータが来なくなった瞬間、例外になる。


ダメな例2:
process.stdin.on('readable', () => {
  var chunk = process.stdin.read();
  if (chunk !== null) {
    process.stdout.write(`data: ${chunk}`);
  }
});

一見うまく行くように見える。というかAPIドキュメントに載っているソースである。が、書き出し先がストールすると、見る見るうちにnode.jsメモリ使用量が増加していく(node echo.js < /dev/urandom | sleep 99999 などとして試してみれば良い)。最後には

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory

お亡くなりになる。


ダメな例3:

fdは、実は単に 0 と指定すれば同期readとなる(というかshellがopen(2)したfdがそのまま使われる)。しかし…

const fs = require("fs");
fs.readFile(0, function (err, data) {
  // ... 処理 & 書き出し
}

これも全データを一度に読み込もうとしてしまうため、メモリが枯渇する。少しずつ読み込もう。


ダメな例4:
const fs = require("fs");
var BUFFER_SIZE = 4096;
var buffer = new Buffer(BUFFER_SIZE);
while (true) {
  var n = fs.readSync(0, buffer, 0, BUFFER_SIZE);
  if (n <= 0)
    return null;
  // bufferを使って何か処理して結果を出力。ここではbufferをそのまま出力する。
  fs.write(process.stdout.fd, buffer);
}

またしてもメモリが枯渇する。fs.writeはどんどんメモリwriteすべきものを積んでしまうのだ…。process.stdout.writeも同じである。かといってfs.writeSyncにすると、

Error: EAGAIN: resource temporarily unavailable, write

pipeがストールした瞬間、例外。process.stdout.fdも非同期openされたfdなのである。


やっと一応動く例

fs.writeSyncにして、かつfdとして 1 を渡そう。

const fs = require("fs");
var BUFFER_SIZE = 4096;
var buffer = new Buffer(BUFFER_SIZE);
while (true) {
  var n = fs.readSync(0, buffer, 0, BUFFER_SIZE);
  if (n <= 0)
    return null;
  // buffer.slice(0, n)を使って何か処理して結果を出力。ここでは入力をそのまま出力する。
  fs.writeSync(1, buffer.slice(0, n));
}

行で分割したい

1行ずつstdinを同期的に読んで返すreadline関数を作ってみる。改行で入力チャンクが切れるとは限らないので、チャンク同士を繋いでやる処理が必要になる。


const fs = require("fs");
var readline = (function() {
  var BUFFER_SIZE = 65536;
  var buffer = new Buffer(BUFFER_SIZE);
  var lines = [];
  return function() {
    if (lines.length > 1)
      return lines.shift();
    while (true) {
      var n = fs.readSync(0, buffer, 0, BUFFER_SIZE);
      if (n <= 0)
        return lines[0] || null;
      var newlines = buffer.slice(0, n).toString().split("\n");
      if (lines.length == 1)
        lines[0] += newlines.shift();
      lines = lines.concat(newlines);
      if (lines.length > 1) {
        return lines.shift();
      }
    }
  };
})();

var line;
while ((line = readline()) != null) {
  fs.writeSync(1, line + "\n");
}

BUFFER_SIZEはデフォルトパイプバッファサイズである64KBにしてみた。


しかし、これはC言語か何か?というような代物になってしまった。node.jsプログラミングパラダイムでなんとかしたい。


Stream APIを使う

Streamクラスの pipe を使えばバッファをいい感じに管理してくれて、入力にバックプレッシャーを伝えることができる。処理部分はstream.Transformを使ってTransformStreamとして実装する。

var stream = require('stream');

// 大文字に変換する
var transform = new stream.Transform({
    transform: function (chunk, encoding, callback) {
      callback(null, chunk.toString().toUpperCase());
    }
});

process.stdin
  .pipe(transform)
  .pipe(process.stdout);

1行ずつ改行区切りで処理するなら、byline npmが便利。

LineStreamをpipeで挟むだけで、改行区切りで送ってくれるようになる。

空行は消えるので注意。維持したければ LineStreamのコンンストラクタに {keepEmptyLines: true} を指定すれば良い。


var stream = require('stream');
var LineStream = require('byline').LineStream;

var lineStream = new LineStream();

var transform = new stream.Transform({
    transform: function (chunk, encoding, callback) {
      callback(null, chunk.toString().toUpperCase() + "\n");
    }
});

process.stdin
  .pipe(lineStream)
  .pipe(transform)
  .pipe(process.stdout);

問題点:Cっぽいやつより4倍遅い。。バッファの量の設定等で早くなるかは不明。

2017-03-19 zshのgitブランチ表示を定期更新

プロンプトのgitブランチ表示を定期更新

Gitブランチ名をzshプロンプトに表示させているのですが、別端末でブランチを切り替えたりすると、普通は現状が反映されません。心配だったらenterキーを押せば良いのですが、端末がたくさん開いてるときに限って間違ったブランチ作業してしまいそうになるので、これを防ぐために1分に1度プロンプトを更新するようにしました。ついでにプロンプトの時刻も更新されます。


関係ありそうな .zshrc の部分を切り出すと、こんな感じ。補完候補(menu select)表示中に更新が起こると候補が消えてしまうという課題がありますが…

zmodload zsh/datetime # $EPOCHSECONDS, strftime等を利用可能に

#gitブランチ名表示
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%c%u%b'
zstyle ':vcs_info:git:*' actionformats '%c%u%b|%a'

#git情報更新
update_vcs_info() {
  psvar=()
  LANG=en_US.UTF-8 vcs_info
  [[ -n "$vcs_info_msg_0_" ]] && psvar[1]="$vcs_info_msg_0_"
}

#プロンプトを毎分更新
reset_tmout() { TMOUT=$[60-EPOCHSECONDS%60] }
precmd_functions=($precmd_functions update_vcs_info reset_tmout)
redraw_tmout() { zle reset-prompt; reset_tmout }
TRAPALRM() { update_vcs_info; redraw_tmout }

#プロンプト
unsetopt promptcr               # 改行のない出力をプロンプトで上書きするのを防ぐ
setopt PROMPT_SUBST
PROMPT="%F{green}[%m-%T]%f%# "
RPROMPT="%(?..%F{red}-%?-)%F{green}[%1(v|%F{yellow}%1v%F{green} |)%n:%~]%f"
[[ -n "$SSH_CLIENT" ]] && PROMPT="%F{green}[%F{cyan}%B%m%b%F{green}-%T]%f%# "


追記

menuselectが消えてしまう件は、以下のように$WIDGET (直前に入力したキーのアクション) と$_lastcomp[insert] (最後に表示した補完の情報)の値を調べて、menu selectが表示されていると思われる場合にはresetを止めることで回避できそうです。なお該当する $WIDGET の値はキーバインドによって変わる可能性があります。自分環境で何にすべきかは bindkey "^I" でTabキーに割り当てられているアクションを調べれば分かります。

ただし、古いzsh(5.1未満)でTRAPALRMの中で$WIDGET評価しようとするとSEGVで死んでしまうバグがあるようで、そのような環境では諦めるしかありません。。

autoload -U is-at-least
precmd_functions=($precmd_functions reset_tmout reset_lastcomp)
reset_lastcomp() { _lastcomp=() }
if is-at-least 5.1; then
    # avoid menuselect to be cleared by reset-prompt
    redraw_tmout() {
        [ "$WIDGET" = "expand-or-complete" ] && [[ "$_lastcomp[insert]" =~ "^automenu$|^menu:" ]] || zle reset-prompt
        reset_tmout
    }
else
    # evaluating $WIDGET in TMOUT may crash :(
    redraw_tmout() { zle reset-prompt; reset_tmout }
fi
TRAPALRM() { check_gitinfo_update; redraw_tmout }

...
追記ここまで

なお変更の有無を表示する check-for-changes は巨大レポジトリだとあからさまに遅くなるので入れてません。


しかし、環境によって vcs_info が遅いことがあるので、ディレクトリgit HEADが移動された時以外は実行しないようにし、さらにそのチェックも別のプロセスを立ち上げたりすることなくzshプロセス内で処理が完結するようにした版が以下。


やってることは、precmdでコマンドを記録しておいて、gitが実行されたりディレクトリ移動が起きたらvcs_info、それ以外でも、各コマンド実行後と1分ごとに.git/HEADの更新日時を見て、前回のチェック時刻から更新が起きていたらvcs_info、という感じです。更新日時のチェックはextended_globの更新日時を元にマッチさせる機能を使っているので、外部コマンドの呼び出したりはしない(つまりforkしない)ため高速なはず。

続きを読む

2017-03-15 flymakeでrubocop

flymakeでrubocopを環境に合わせて実行

rubyを書く際にコーディングスタイルをrubocopでチェックしたい時、手動でチェックしていると忘れることがあるので違反があったらその場でエディタに警告して欲しいわけですが、これをemacsのflymakeでやる方法のメモ。

f:id:NeoCat:20170315095639p:image:w360

.emacsなどをいろんな環境で共有しているので、マシンによってrubocopが入っていたりなかったり、あってもグローバルでなく個々のbundleに入っていたりといった状況に対応できるようにします。(rubocopがない場合はruby -cによりシンタックスチェックのみ行います。)

(flymakeよりflycheckの方がいいのかもしれないですが、パッケージ等を追加で入れなくてもemacsについてくることを重視してflymakeにしてます。なんか時代錯誤 *1 )


まず、下記のshell scriptを ~/bin/ とか適当な場所に置きます。PATHが通ってなくても構いませんが、権限は実行可能にしておきます。

#!/bin/sh
RUBOCOP="rubocop --format emacs"

exists () {
    which "$1" >/dev/null 2>&1
}

# use rubocop directly
exists rubocop && exec $RUBOCOP "$@"

# use rubocop in the bundle
CWD="$PWD"
abspath="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
until [ "$PWD" = "/" ]; do
    if [ -f Gemfile.lock ]; then
        if grep rubocop Gemfile.lock >/dev/null; then
            exec bundle exec $RUBOCOP $abspath 2>&1
        fi
        break
    fi
    cd ..
done
cd "$CWD"

# if no rubocop is available, just use ruby syntax check
ruby -c "$@"

あとは .emacs に、flymakeでrubyスクリプト編集中にflymake-ruby.shでチェックをかけて、違反があれば赤色表示になるように設定を追加します。以下では M-n / M-p キーでエラーのある位置に飛ぶとともに、エラー内容をminibufferに表示するようにしています。

(ruby-modeやflymake自体の設定などは適宜環境に合わせてください。なお以下ではバッククォートが \ (円マーク) になってしまっていますのでご注意を。)

;; ruby mode
(autoload 'ruby-mode "ruby-mode" nil t)
;(autoload 'ruby-mode "ruby-electric" nil t)
(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))

;; flymake
(require 'flymake)
(global-set-key "\C-cd" 'flymake-popup-current-error-menu)
(global-set-key "\M-n" 'flymake-goto-next-error)
(global-set-key "\M-p" 'flymake-goto-prev-error)
(defun display-error-message ()
  (message (get-char-property (point) 'help-echo)))
(defadvice flymake-goto-prev-error
    (after flymake-goto-prev-error-display-message) (display-error-message))
(defadvice flymake-goto-next-error
    (after flymake-goto-next-error-display-message) (display-error-message))
(ad-activate 'flymake-goto-prev-error 'flymake-goto-prev-error-display-message)
(ad-activate 'flymake-goto-next-error 'flymake-goto-next-error-display-message)

;; flymake for ruby
(defun flymake-ruby-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-file  (file-relative-name
                       temp-file
                       (file-name-directory buffer-file-name))))
    (list "~/bin/flymake-ruby.sh" (list local-file))))
(push '(".+\\.rb$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '(".+\\.rake$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("Rakefile$" flymake-ruby-init) flymake-allowed-file-name-masks)
(push '("^\\(.*\\):\\([0-9]+\\): \\(.*\\)$" 1 2 nil 3) flymake-err-line-patterns)
(push '("^\\(.*\\):\\([0-9]+\\):[0-9]+: \\(.\\): \\(.*\\)$" 1 2 3 4) flymake-err-line-patterns)
(add-hook 'ruby-mode-hook
          '(lambda ()
             ;; Don't want flymake mode for ruby regions in rhtml files and also on read only files
             (if (and (not (null buffer-file-name)) (file-writable-p buffer-file-name))
                 (flymake-mode t))
             ))

rubocop自体の設定は RuboCopの設定アレコレ - Qiita などを参考に。

*1:そもそもemacsが…

2017-01-12 コマンドの入出力が繋がった端末を移動させる

コマンドの入出力が繋がった端末を移動させる

時間のかかるコマンドを起動した後、「あ、tmux(とかscreen)の中で実行すればよかった…」と気づくことがたまにあるわけですが、そんな時に強制的に端末を移動させる方法として、gdbをアタッチし、そのコマンドファイルディスクリプタ(fd)の示す先を変更してしまうという手があります。


具体的な手順としては、open(2)で移動先の端末を開き、そのfdをdup2(2)の第1引数、移動前の端末に繋がっているfdを第2引数に指定して差し替え、最後にclose(2)で最初にopenした端末を閉じます。


ただし厳密には、対象が端末の場合、各種の状態を持っているので、これをsttyコマンドで合わせる必要があります。そうでないと、対象がviemacsのような端末制御を行うソフトウェアの場合、正しく操作できなくなります。


またコマンドが入力を受け取る場合、移動先の端末でシェル等がreadしていると、入力が部分的にシェルに吸われてしまって操作が著しく困難になってしまうので、そのようなプロセスは止めておく必要があります。以下のスクリプトではSTOPシグナルを送って眠らせるようにしてあるのですが、screentmuxは、端末の子プロセスが寝たことを検知するとCONTシグナルを送ってすぐに起こしてしまうようです。仕方ないので手動でsleepでもさせておくしかなさそうです。。とりあえずスクリプトが実行された端末が移動先になっている場合にはスクリプト内でsleepさせています*1


このほかの工夫として、移動元の親プロセス(通常はシェル)を待機状態から復帰させるために、一旦対象コマンドにSTOPシグナルを送ってサスペンドしたのち、少し待ってCONTシグナルで復帰させています。ついでに、端末のサイズが変わったことをWINCHシグナルでコマンドに通知してあげます。これにより画面の再描画も必要に応じて行われます(あるいは単に無視されます)。


上記の処理をするのが本記事末尾のスクリプトです。

./switch-tty.sh 移動するブロセスID 移動先の端末またはファイル

のように指定して使います。


試してみるには、Linux上(またはssh)で端末を2つ開き、一方で何かのコマンド (例えば vi ) を起動し、もう一方で (必要ならtmuxなどを実行したのち)

./switch-tty.sh `pidof vi` `tty`

などと実行すると、

Current stdin: /dev/pts/5
Target fds: 0 1 2
session leader: 23965
Switching to this TTY
Continue? (y/N) 

と確認が出ますので、yを入力します。するとおもむろにgdbごにょごにょ動いたのち、スクリプトを実行した端末にviが移動してきます。あとは、移動元の端末で disown を実行すれば、閉じてしまうことができます。


(なお、移動先でviを終了してもすぐにはシェルは帰ってきません。これはコマンドの終了を1分に1回しかチェックしていないためです*2。対象のコマンド挙動次第ですが、 ctrl-C を入力すればsleepが中断されてすぐにシェルが戻ってくるかもしれません。)


以下、スクリプト

https://gist.github.com/NeoCat/f662cfd71c65eed7b59baed14eb3400c

*1:元々いたプロセスを終了してしまうわけにはいきません。セッションリーダーが閉じると端末そのものが閉じられてしまうからです

*2:自分の子プロセスでないプロセスの終了を待機するいい方法がないため

2016-12-31 iPhoneのバッテリー残量を取得

iPhoneのバッテリー残量を取得

iPhoneバッテリーがかなりヘタってちょっと使うとすぐに電源が落ちてしまうようになってしまったので、自分で交換してみました。やり方を紹介したページを見つつ、バッテリー\2000 + 工具セット \1000 で無事交換成功し、日中そこそこ使っても丸一日持つようになりました。


f:id:NeoCat:20161218073904j:image:w400


さて、バッテリー寿命を良くする方法として、80〜90%ほど充電されたら繋ぎっぱなしにせずに充電をやめると良いという説があるようです。真偽のほどは定かではありませんし、大きな効果があるならそういった機能実装されているでしょうから、おそらくいちいち気にするほどの効果はないのでしょう。とはいえ、意識しなくとも自動的に充電が止まってくれるのならそうしてみても良いかな?という気になったので、やり方を考えてみました。


まず、iPhoneバッテリー残量を取得する方法ですが、なんらかのアプリを使えないでしょうか? しかしiOSの場合、アプリバックグラウンドで定期的に処理をするのは、アクセサリへのアクセスや、音楽再生位置情報を使用するアプリに限られており、電池監視のためにはトリッキーなことをする必要があるので候補から外しました。


次に考えられるのはUSB越しで取得する方法です*1。最近のLinuxデスクトップ(Fedora 25で確認)にiPhoneUSB接続すると、upowerコマンドiPhoneバッテリー値を取得できます。下記はiPadの場合ですが、まず一覧で名前を確認し、

$ upower -e
/org/freedesktop/UPower/devices/computer_3_1
/org/freedesktop/UPower/devices/mouse_0003o046Do1024x000A
/org/freedesktop/UPower/devices/keyboard_0003o046Do2011x000B
/org/freedesktop/UPower/devices/DisplayDevice

それっぽいデバイス指定すると、

$ upower -i /org/freedesktop/UPower/devices/computer_3_1
  native-path:          /sys/devices/pci0000:00/0000:00:14.0/usb3/3-1
  vendor:               Apple_Inc.
  model:                iPad
  serial:               *****************************************
  power supply:         no
  updated:              20161231221652(14 seconds ago)
  has history:          yes
  has statistics:       no
  computer
    warning-level:       none
    percentage:          100%
    icon-name:          'battery-full-charged-symbolic'

という感じで情報が出力されるので、このpercentageを見るというのが一つの方法です。GUIで設定(gnome-control-center)→電源 で残量を確認できるのもこの情報を表示しています。


これを見て、80%を超えたらUSBの電源をOFFにする方法が取れるでしょう。LinuxからのUSBの電源制御には、最近だと以下のハブが使えるそうです。

LinuxからUSB HUBの電源のON/OFFを制御してみる - memoメモ


ただ欠点として、USBから切断してしまうとそれ以降の情報は取れないので、充電完了後に使用していると電池が減っていってしまうということになります*2


別のやり方として、iTunesインストール済みのMacまたはWindowsであれば、libimobiledeviceを導入することで、Wi-Fi経由でも電池残量などの情報が取得できます。たまたまWiFiで電源をON/OFFできるコンセントを作ってあったので、全てWiFi越しでやれるということもあり、今回はこの方法を試してみました。


まず、libimobiledeviceを導入します。Macであればbrewで一発です。とはいえiOS10のせいか、HEADでないとうまく動作しませんでした。

$ brew install --HEAD libimobiledevice

これで、

idevice_id -l コマンドUSBまたはWiFiで繋がっているiOSデバイスシリアルを調べておき、ideviceinfoでバッテリー情報を取得します。

$ ideviceinfo -u ****************(idevice_idで表示されたシリアル) -q com.apple.mobile.battery
BatteryCurrentCapacity: 100
BatteryIsCharging: false
ExternalChargeCapable: true
ExternalConnected: true
FullyCharged: false
GasGaugeCapability: true
HasBattery: true

BatteryCurrentCapacityが%単位でのバッテリー残量です。

rubyであれば

$IDEVICEINFO_CMD = '/usr/local/bin/ideviceinfo'

def get_batt(device_id)
  result = `#{$IDEVICEINFO_CMD} -u #{device_id} -q com.apple.mobile.battery`
  if result =~ /BatteryCurrentCapacity:\s*(\d+)/
    return $1.to_i
  end
  nil
end

というような関数バッテリー残量を取得できるようになります。


ただし,iPhoneスリープ状態になっている場合、WiFiには90〜300秒に一度、数秒間しか接続されないようで、この瞬間しか情報がとれません。とりあえず、2秒に一度くらいポーリングをかけることにします*3。また、それでもかなり長いことWiFiに接続してこないこともあるようなので、そのような場合には一度充電をOFF→ON(またはON→OFF)することで、iPhoneをウェイクアップさせれば情報がとれます*4


この結果を見て、80%以上で充電停止、80%未満で再開するようにします。また、充電中にMacスリープしてしまうと充電されっぱなしになるので、充電中はcaffeinateコマンドスリープ抑制しています。全体的にはこんなスクリプトになりました。


30%充電の状態からこのスクリプトを動かしながら充電を試して見た結果、こんな感じになりました。運悪く80%になるわずか手前でWiFi接続が切れたらしく、80%を3%ほど超えたところで充電が止まりました。


f:id:NeoCat:20161231214258p:image:w500


そのままiPhoneを使っていると、79〜80%で維持されるように充電がON/OFFされます。


f:id:NeoCat:20161231214259p:image:w500


でもこれ結局充放電してるわけで、バッテリーに優しい気はしない。。まあ、あくまで実験ということで。

*1Bluetooth LEBattery Serviceを使ってとれないか試したのですが、いざ取得しようとすると認証要求されてしまい失敗しました。

*2:定期的に再接続して充電残量をチェックすればいいかも

*3:本当はlibimobiledeviceから接続イベントを取れれば良いのですが、ポーリングでも大した負荷ではないのて今回は適当に済ませてしまいました。

*4:この時、いちいち接続音やバイブレータが鳴りますが…