Unyablog.

のにれんのブログ

OCaml 入門途中の感想

最近京大の五十嵐先生が公開しているテキストを使って OCaml に入門していた。

このテキストは object まで行かないので入門したとは言えそうにないが、一区切り付いたので初心を記録する意味で雑に感想を書いておく。

主に Haskell を考えながら書いています、が Haskell もそんなに知らないです。

  • 型推論強くて良い
  • ref は便利ですね
    • 実用的!
  • type 、簡単に型推論してくれるのは良い
    • 変数名被ったらダメなのはつらい
  • IO 周りが楽なので良い
  • 対話環境は ghci の方が便利
  • セミコロン難しい
    • セミコロンの数で意味が変わる
    • リストよくカンマと間違える
    • 全体的に他の言語と違う作法の使い方をしているので引っかかりやすい
      • 考えられた結果なのか対抗心なのかは知らない
  • 括弧が面倒
    • Haskell における $ がほしい
    • 最近?入ったらしいけどコミュニティでは使われているのかな
    • OCaml でこんなことを言っているし LISP だとどうなることやら
  • 全体的な結合順に違和感がある
    • とりあえず括弧つければ直るが
    • 慣れの問題ではありそう
  • 遅延評価じゃないのは分かりやすい
    • ただ無限リストなどを作るのは大変になる

Haskell のほうが記号が多くて便利だし、楽しさはあると思う 。ラムダ式などはシュッと書きたい。

でもシンプルな構文により初見で分かりやすいのも長所である。どっちもどっちだ(まだ途中なので本当にシンプルなのか分からないけど)。

構文はあまり好きではない(嫌いでもない)けど、諸概念は良いし納得できるものも多いので難しいところだなあという感じ。

とりあえず Haskell の入門を再開したくなった。

予防線張りすぎでは?

まだ途中だからね!

その他

最近とにかく強い静的型付け言語を使いたい気持ちが高まっていて、python のような軽い文法で無いかなあと探していたら nim があった。また使いたい。

他の現代的な言語として Kotlin や Swift は最高だと思っているけど、エディタでサクッと書くには向いてない気がするんですよね。。

pipenv を使ってみる

Python における bundler といえば (超ざっくり言えば) virtualenv になるのですが*1、イマイチ使いにくいなと思っていたら pipenv というものが登場していた。ちょっと使ってみたのでメモ。

何するやつ

github.com

  • Pipfile(.lock)を元に良い感じに virtualenv 作成 + pip install してくれるやつ
    • Pipfile が使える便利 virtualenv wrapper という感じ
  • requests と同じ作者なので流行りそう

インストール

$ pip3 install pipenv

pipenv 環境作成

--three と指定すると Python3 だけの環境が作られる。--two も同様。

$ pipenv --three

Creating a Pipfile for this project...
Creating a virtualenv for this project...
⠋Already using interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5/bin/python3
Also creating executable in /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /home/nonylene/.local/share/virtualenvs/pipenv-test-Iy4gNUM5

home 以下に virtualenv を作成されたくない場合は環境変数 PIPENV_VENV_IN_PROJECT を 1 にすれば .venv/ に作成される。この辺 --path で指定したいですね。

Pipfile

PipfileGemfile と同じ要領で使えるもの。toml で記述する。

[[source]]
verify_ssl = true
url = "https://pypi.python.org/simple"

pip 2.0 に向けて Pipfile の仮実装があり、これを使っているらしい。

Pipfile を主に実装した人は pipenv 作った人と同じ人だった。実装は落ち着いているけど pip に取り入れられるのはいつになるのか…。

github.com

パッケージ追加

pipenv install [package] でインストールするとパッケージが virtualenv にインストールされ、 Pipfile も置き換えてくれる。

$ pipenv install requests

Installing requests...
Collecting requests
  Downloading requests-2.13.0-py2.py3-none-any.whl (584kB)
Installing collected packages: requests
Successfully installed requests-2.13.0

Adding requests to Pipfile's [packages]...
P.S. You have excellent taste! ✨ 🍰 ✨

最後の行は作者のライブラリを入れた時に表示される。良い話。

$ cat Pipfile

[[source]]
verify_ssl = true
url = "https://pypi.python.org/simple"

[packages]
requests = "*"

lock

Pipfile.lock の生成。

$ pipenv lock

Locking [dev-packages] dependencies...
⠹Locking [packages] dependencies...
⠹Updated Pipfile.lock!

Pipfile.lock が生成される。 json らしい。人間が触るものではない。

$ cat Pipfile.lock

{
    "_meta": {
        "hash": {
            "sha256": "da2810af0c3b5333e0de2fce9bea2a228812e2014e5f5fe3b1c533badc6c24e4"
        },
        "requires": {},
        "sources": [
            {
                "url": "https://pypi.python.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "requests": {
            "hash": "sha256:1a720e8862a41aa22e339373b526f508ef0c8988baf48b84d3fc891a8e237efb",
            "version": "==2.13.0"
        }
    },
    "develop": {}
}%

activate virtualenv

$ pipenv shell を用いることで、virtualenv の activate された shell が起動する。

$ pipenv shell

Launching subshell in virtual environment. Type 'exit' or 'Ctrl+D' to return.

$ python3
Python 3.5.2 (default, Nov 17 2016, 17:05:23)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> requests.get("http://example.com")
<Response [200]>

run

$ pipenv run を使えば bundle exec っぽいことができる。これが便利。

$ pipenv run python3 -c "import requests; print(requests.get('http://example.com').text)"
<! doctype html>
...

ただ spawn していて新しいプロセス立ててるので pipe が上手くいってなく、パイプやリダイレクションができない。*2

追記: マージされたので今後は大丈夫になるでしょう。

github.com

その他

  • pipenv install をすると Pipfile.lock || Pipfile にもとづいてインストール・ virtualenv 構築してくれる。
  • その他アップデートコマンドなどもある。 ( README.md にだいたい書いてある )
  • 個人的にはインストールパスを記憶させてほしいけどまだできなさそう

Remember where the virtual environment was created · Issue #252 · kennethreitz/pipenv · GitHub

まとめ

Pipfile をお先に扱える + virtualenv の便利 wrapper として bundler 感覚で使えるのが良いと思いました。積極的に使っていこうと思います。

*1:ざっくり言い過ぎ

*2: exec してくれ~という issue たてた。

vim-keymaps でキーマップを簡単に切り替える

Vim のキーマップを簡単に切り替えられるプラグイン vim-keymaps を作りました。

github.com

下のように、複数のキーマップを設定することができ、簡単に切り替えることが出来ます。下では <C-k> でキーマップを切り替えています。

f:id:nonylene:20170322005931g:plain

今すぐダウンロード!

使い方

各キーマップを示すディクショナリを入れた配列を g:keymaps に設定し、後はキーマップを変更するためのキーを適当に割り当てる or コマンドを打つと使えます。詳しくは README.md で。

初めは接続されているキーボードでなんとか判別できないかなと考えましたが、 ssh 先でキーボードの配列を読むのは不可能なので手動で変更するようにしました。

経緯

私は普段、コーディングするアプリケーション上では数字と記号を入れ替えて使っています。例えば 1! になり、 !1 になります。

瞬時に括弧などが打てるので非常に便利に使っていたのですが、キーボードによって数字と記号の関係は異なるので、JIS 配列に合わせたマッピングを書いていると US キーボードでは崩壊してしまいます。記号だけではなく数字も入力できなくなるので非常に困る。

しかし、いろいろあって先月から US 配列の mac を使うことになりました。

mac のキーボードのみ使うなら .vimrc を書き直して US にマッピングし直せば良いのですが、普段接続して使っている HHKB はもちろん JIS 配列。本体キーボードで使う時だけ paste モードで使う運用をしていました。

しかし paste モードのままでは tab で(空白ではなく) tab が入力されたり、数字記号以外の各種マッピングが効かないなど困ります。

これはもうキーマップをキー配列ごとに変更するしかない。 せっかくの機会なのでプラグインを自分で作りました。

作成

vimプラグインを書くことは初めてでした。まともに vimscript も書いたこと無かったし。

書くときは下のサイトを参考にしたり、有名なプラグインのコードを見たりしながら書きました。

mattn.kaoriya.net

thinca.hatenablog.com

vim のドキュメントも結構読みました。読みやすい英語だったし、充実していて良かった。

vimscript, はじめは変数のスコープ定義が特殊だったりしてとっつきにくいですが、書けてくるとなかなか楽しいです*1

内部の話

設定ファイル

設定ファイルをどのように記述しようか迷って、結局 vimscript の配列を使うことにしました。ただ、 vimscript で配列や辞書型を複数行に記述しようとすると割りと面倒で、行の最初に \ を書くことで行の継続としなければなりません。あくまで行の継続なので、途中にコメントを書くことは出来ません…。( vim は一行ごとのコメントしかできない ) README を書いてる時にその事実に気づいて修正しました…。

マッピングの数は結構多いので、.vimrc に長大な配列を記述することになります。以下は自分の設定 を半分ぐらい省略したもの です。

let g:keymaps =  [
      \  {
      \    'name': 'JP',
      \    'keymap': {
      \      'noremap!': {
      \        '1': '!',
      \        '3': '#',
      \        '4': '$',
      \        '5': '%',
      \        '6': '&',
      \        '!': '1',
      \        '"': '2',
      \        '#': '3',
      \        '$': '4',
      \        '%': '5',
      \        '&': '6',
      \        "'": '7',
      \        '(': '8',
      \        ')': '9',
      \      },
      \      'imap': {
      \        '2': '<Plug>delimitMate"',
      \        '7': "<Plug>delimitMate'",
      \        '8': '<Plug>delimitMate(',
      \        '9': '<Plug>delimitMate)',
      \      },
      \      'cnoremap': {
      \        '2': '"',
      \        '7': "'",
      \        '8': '(',
      \        '9': ')',
      \      },
      \    },
      \  },
      \  {
      \    'name': 'PASTE',
      \    'paste': 1
      \  },
      \]

dein のように toml や json に分けれればなあと思ったのですが、 vimscript では標準ライブラリでシュッとパースすることはできません*2Python バインドでも使えば良いのですが、標準では入っていないことが多いので諦めました*3

map 方法

この g:keymap からマッピングするのは非常に素朴で、コマンド、左辺、右辺をスペースでつなげて放り込んでいます。なのでオプションを適当に入れても動くし、他のコマンドも打てちゃいます(キーマップ削除する時にエラー出ると思いますが)。

execute(l:cmd . ' ' . pair[0] . ' ' . pair[1])

特に立派な API は無かったのでこうしました。スペース等も自分で書くのと同じようにエスケープすれば動きます。

unmap 方法

デフォルト設定ではキーマップ変更時に以前のマッピングを削除しています。よって、マッピングと対になるアンマッピングコマンドを生成する必要があります。

map - Vim日本語ドキュメント

これも素朴に1文字目と2文字目をパースしています。

function! s:create_unmap(origin)
  let l:first = a:origin[0]
  if l:first is 'n'
    if a:origin[1] is 'o'
      " noremap
      let l:cmd = 'unmap'
    else
      let l:cmd = 'nunmap'
    endif
  elseif l:first is 'm'
    " unmap
    let l:cmd = 'unmap'
  else
    let l:cmd = l:first . 'unmap'
  endif

  if a:origin =~ '!'
    let l:cmd .= '!'
  endif

  return l:cmd
endfunction

paste モード関連

このプラグインは paste モードをサポートしているのですが、paste モードに入ってしまうとマッピングが全て外れるのでキーマップの切り替えができません。

set pastetoggle=<同じキー>

とすることで vim の機能によって paste モードを抜けることができるのですが、vim の機能で抜けているので次のキーマップには移動されず paste 用キーマップのままになってしまいます。

結局、autocmd を使うことによって paste モードの変更を検知して、フック上でキーマップを変更することにしました。

augroup keymaps
  autocmd!
  autocmd OptionSet paste call s:on_change_paste()
augroup END

function! s:on_change_paste()
  " sometimes v:option_new not working
  if g:keymaps_paste_auto_rotate && !&paste
        \ && get(keymaps#get_current_keymap(), 'paste', 0)
    " exit paste mode -> rotate
    call keymaps#rotate_keymap()
  endif
endfunction

lightline との連携

keymaps#get_current_keymap_name で現在のキーマップ名を取れるようにしたので、これを lightline に表示すれば現在のキーマップを簡単に見れます。最初に示した gif もそうしています。

paste モード変更時にもうまくするには lightline も paste を検知できるように autocmd を新しく設定する必要があります。

augroup local
  autocmd!
  autocmd OptionSet paste call lightline#update()
augroup END

その他

  • <SID> をキーマップに使っているとその <SID> は実行時のスクリプトに依存するのでプラグイン用のスコープになってしまう。
    • 同じような理由から、 <expr> をキーマップに使って関数を実行したい場合、呼び出す関数はグローバルでなければならなかったりする。
  • autoload/ に多くを分割したのだけど、最初のキーマップを設定するために起動時に読み込まれるので逆効果になっているかもしれない。
    • まあ重いスクリプトではないので大丈夫だと思うけど。
  • vimscript 、 endforend にできるが endfunctionend に省略できなかったりして面白い。
    • 結局全部フルで書いた。
  • l: をちゃんと書いてたけど実は暗黙的に設定されるっぽい。
  • vimscript の文法、だいぶ知れたのでこれからは .vimrc サクサク書けそう。
  • US キーボード、' が一瞬で打てるのが良いですね。でも数字と記号を入れ替えていると 0 に記号が割り当てられてしまって不便だったりする。

まとめ

これで今まで通り記号が簡単に入力できるようになりました。 「paste モードではないが記号のマップは削除したキーマップ」なども設定できて快適です。

Vim 最高!みんなも今すぐプラグイン書こう!!

*1:perl っぽいですね(?)

*2:雑に eval すると大体動くとは思う

*3:実は go とか使うとよかったりするのかな

zsh の色付き vcs_info を一行上の右端に出す

書いておかないとあとで分からなくなるのでメモ。

zsh 便利ですよね。vcs_info も便利ですよね。

vcs_info は名前の通り VCS の情報を出してくれる君です。

zshのターミナルにリポジトリの情報を表示してみる · けんごのお屋敷

本題

最近 zsh のプロンプトを以下のように変更しました。

f:id:nonylene:20170308024523p:plain

f:id:nonylene:20170308034716p:plain

ポイントは左にあった vcs_info が右に移動していることです。

RPROMPT 問題

vcs_info を右に移動するには vcs_info を単純に RPROMPT に移動させれば良いのでは?と思いますが、 RPROMPT はコマンド入力と同じ場所に表示されるため、パスやコマンドが長いと消えてしまいます。

f:id:nonylene:20170308025835p:plain

なら上に表示すれば良いのではと思って PROMPTRPOMPT を二行にしてみると、今度は表示されなくなってしまいます。

RPOMPTPROMPT が複数行に渡っても最終行と同じ行に表示され、改行ができません。

f:id:nonylene:20170308025857p:plain

一行上の右端に表示するには、 precmd 内でプロンプト復帰前に printf コマンドを用いて表示する必要がありました。

printf 問題

色がない場合、 printf で右端に表示するには以下のように行なえば良いです。

# プロンプト復帰前に呼ばれる
precmd() {
  # $prompt_right -> 右端に表示する文字列
  # $prompt_left -> 左端に表示する文字列
  printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left}" "$prompt_right"
}

%*s ( * で幅を指定する) で長さに "$((${COLUMNS}-${#prompt_left}" (全体の横の長さから $prompt_left の長さを引いたもの) を指定することによって、右寄せを実現しています。

しかし、これを色付きで行うと崩れてしまいます。

f:id:nonylene:20170308031636p:plain

なぜなら、ANSI エスケープシーケンスを含めた時、 $prompt_right 等にエスケープシーケンスが入って文字の長さが長くなり、 printf の幅空けや ${#prompt_left} による文字長の取得が狂うためです…!!!!😇

この時、ちゃんとシーケンスの長さを考慮してあげるとうまく表示できるようになります。

# 長さは適当に 10 にしている
printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left} + 10" "$prompt_right"

vcs_info の action 問題

vcs_info には actionformats という機能があり、コンフリクト解決やリベース時などにその action を表示することが出来ます。

自分は、acrion の中身は目立つように赤色にしています。なので action があると、シーケンスの文字数が増えて再び描画が崩れてしまいます。

f:id:nonylene:20170308032729p:plain

action があるかどうかは precmd 内部では分かりません。どうしようかなと調べていると、 vcs_info にはフック関数を設定できることが分かりました。

zsh.sourceforge.net

これを使って、フック関数の中で action があるかどうかを見れば、あとは変数にセットすることで precmd 内でも分かります。

最終的に以下の様になりました。

# tput でエスケープシーケンスを出力する
tput_normal=$(tput sgr0)
tput_yellow=$(tput setaf 3)
tput_red=$(tput setaf 1)
tput_cyan=$(tput setaf 6)

# vcs_info の設定
zstyle ':vcs_info:*' enable git hg
zstyle ':vcs_info:*:*' formats "[${tput_yellow}%r: %b${tput_normal}]"
zstyle ':vcs_info:*:*' actionformats "[${tput_yellow}%r: %b${tput_normal} (${tput_red}%a${tput_normal})]"

# hook 関数の設定
# メッセージが設定される直前
zstyle ':vcs_info:*+set-message:*' hooks vcs_info_hook
# VCS が存在しないとき
zstyle ':vcs_info:*+no-vcs:*' hooks no_vcs_hook

# VCS がないときのエスケープシーケンス長
novcs_color_len=$((${#tput_cyan} + ${#tput_normal}))
# VCS があるが action がないとき
noaction_color_len=$((${novcs_color_len} + ${#tput_yellow} + ${#tput_normal}))
# VCS があり、action もあるとき
action_color_len=$((${noaction_color_len} + ${#tput_red} + ${#tput_normal}))

# $prompt_color_len にシーケンス長を設定する
# VCS があるときの hook
function +vi-vcs_info_hook() {
  # action があるかどうかは hook_com[action] を見れば良い
  if [ -z ${hook_com[action]} ]; then
    prompt_color_len=$noaction_color_len
  else
    prompt_color_len=$action_color_len
  fi
}

# VCS がないときの hook
function +vi-no_vcs_hook() {
  prompt_color_len=$novcs_color_len
}

precmd() {
  vcs_info
  # 左寄せする文字列
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"

  # 各情報を表示
  printf "\n%s%*s\n" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}"
}

# このプロンプトは printf 後に表示される (パスの部分)
PROMPT='%~ %(!. !root! #.>) '

途中で -1 しているのは、右寄せの最後に一文字空けるためです (wrap っぽく判別されるのを防ぐ)。

これで大体いい感じになりました。

f:id:nonylene:20170308034641p:plain

リサイズ問題

ウィンドウのサイズを変更した時、 RPROMPT では良い感じに右寄せ文字が移動してくれるのですが precmdprintf した文字列は再描画されないので残念ながらこのままではズレてしまいます。

f:id:nonylene:20170308135044p:plain

ここで、 TRAPWINCH を用います。

TRAPWINCH はサイズが変更された時に呼ばれる関数です。しかし、単純にここで printf すると、プロンプトの下に追加されてしまうので思った通りになりません。

f:id:nonylene:20170308135638p:plain

そこで、 printf していたのを PROMPT に変数として組み込み、TRAPWINCH 内で zle reset-prompt することによって PROMPT ごと再描画するようにします。

# $prompt_header を生成する
function generate_promopt_header() {
  prompt_header=$(printf "\n%s%*s" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}")
}

precmd() {
  vcs_info
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"
  # printf をやめて $prompt_header を生成するのみとする
  generate_promopt_header
}

TRAPWINCH() {
  # 長さが変わったので $prompt_header を再生成   
  generate_promopt_header
  # プロンプト再描画
  zle reset-prompt
}

# $prompt_header を改行前に含める
PROMPT='${prompt_header}
%~ %(!. !root! #.>) '

こうして、幅が変わっても無事に再描画されるようになりました。

f:id:nonylene:20170308140603g:plain

残る問題

実はまだ問題が残っていて、ブランチ名等が日本語等のときにカラム幅が合わなくなり、はみ出してしまいます。

f:id:nonylene:20170308140749p:plain

wc -c 等を使ってもバイト数だから上手くいかないし、どうしたものか…。

まあ日本語で git ブランチや git 用のディレクトリ作ることはそうそう無さそうなので今回は放置。

全体的な zshrc はこちらです。(大したことやってない)

dotfiles/.zshrc at master · nonylene/dotfiles · GitHub

Shell の pipe と redirection を実装する

新年なので Shell の pipe と redirection を実装した。

前回の記事はこちら

nonylene.hatenablog.jp

前回実装してみて、さすがに pipe と redirection ぐらいは実装しておいてもいいのでは?と思って実装してみた。

pipe

github.com

パイプについての説明は

パイプでつなぐ - PukiWiki

が分かりやすくて、ここを見ると大体いい感じに実装できた。

一つ困ったのは、試しに

$ ls | ls

とした時に

ls: write error: Broken pipe

が確率的に発生していた。試しに C で同じような実装をすると特に発生せず、速度の問題かと思って C で sleep しても再現しなくて困っていた(まあこんなコマンド打つことないけど…)。

結局原因は Python がデフォルトでは SIGPIPE を無視することで、これによって後段の ls が終了しても前段が正しく終了せずに ls が実行されてしまっていた。確率的に起きていたのは SIGPIPEexecvp が実行される前か後かの違いによるものだと思う。

PythonSIGPIPE を無視しないようにすると直ったので良かった。

signal.signal(signal.SIGPIPE,signal.SIG_DFL)

redirection

リダイレクションも pipe と原理は同じで、ファイルディスクリプタdup するだけでシュッと実装できた。

github.com

段々気合でパースしている部分が大変になってきている…。

面白かったのは

$ ls > a | cat

とした時に zsh では出力がなされたけど bash / dash では出力されなかった。

これは、 zshmultios という機能によって zsh が redirection と pipe どちらにも出力しているからだった。

zsh.sourceforge.net

unsetopt multios すると、bash / dash と同じ挙動になった。なるほど〜

Mackerel Agent for Android

この記事は Mackerel アドベントカレンダー 21 日目の記事です。

qiita.com

Mackerel について

Mackerel とはサーバーを監視するサービスで、Slack などとの連携が便利だったりグラフが美しかったりと良いサービスです。

Mackerel にデータを送るためのサーバー監視ソフトウェアとしては mackerel-agent というものがあり、これをインストールするとサーバー上でメトリックを定期的に取得し、送信してくれます。

github.com

本題

今回はその Android 版を作ってみました。

Mackerel Agent for Android です。

f:id:nonylene:20161221015614p:plain

昨日から PlayStore に公開しています!

f:id:nonylene:20161222000441p:plain:w150

play.google.com

GitHub はこちらです。

github.com

対応バージョンは Android 4.4 以上です。

機能

1分毎にメトリックを集め、5分毎にサーバーに送信します。Mackrel には 1 ホストとして登録するようにしています。

残念ながら mackerel-plugin は使えませんが、公式の mackerel-agent が現在送信しているメトリックと spec は全てカバーしています。

動機

実装すると proc が学べるし良いかなと思ったからです。あとバッテリー切れそうな時に通知してくれると便利かもと思いました(がまだ Android 関係のカスタムメトリックは実装していない…)。

実装等

AndroidLinux なので概ね mackrel-agent の Linux 版と同じ処理でメトリックを取得できます。

実装は Kotlin で行いました。アプリの特性上 proc ファイルをたくさん読むのですが、Kotlin だとファイル関係がかなり楽なので良かったです。

// Interface の Spec を読む例
File("/proc/net/dev").readLines()
        .map { it.split(":").map(String::trim) }
        .filter { it.size == 2 }
        // remove loopback
        .filter { it[0] != "lo" }
        .mapNotNull {
            val values = it[1].split(Regex("\\s+")).map(String::toDouble)
                // all zero -> remove data
            if (values.all { it == 0.0 }) return@mapNotNull null
            InterfaceStat(it[0].trim(),
                    values[0], values[1], values[2], values[3], values[4], values[5], values[6],
                    values[7], values[8], values[9], values[10], values[11], values[12], values[13],
                    values[14], values[15], time
            )
        }

ログや取得した値のキャッシュは Realm を使っています。proc を用いて差分を出すには数秒間待つなどの必要がありますが、そのあたりは RxJava を用いていい感じにしました。*1

Android の辛いところ

コマンド

mackerel-agent ではいくつかコマンドを打って結果をパースしている物があります。

dfuname がそれに当たるのですが、これを Android で打とうとするとバージョン問題に直面します。

Android 5.0 未満では df は一切オプション受け付けない上に独自の形式で出力され、uname においては存在すらいしていません。

結局 df では以下のようにバージョンで分けるようにして、uname は proc から取得できる値を取得することにしました。*2

MackerelAgentAndroid/FileSystem.kt at 48a67bdd9387ec519800612e847ea28bc4bacf84 · nonylene/MackerelAgentAndroid · GitHub

Android 6.0 以降では toybox のお陰でコマンド環境は向上し、大抵の必要なコマンドは使えるようになりました。

詳しくは

nonylene.hatenablog.jp

に書いています。

定期実行

Android で定期的にタスクを実行するにはいくつかの方法があります。このアプリはフォアグラウンドサービスとして常駐するようにしていますが、代表的なものは AlarmManager でしょう。

しかし、今回は Forground Service を使いました。なので通知バーに常駐しています。

これは Android 6.0 からのバッテリー節約のための新機能である Doze / AppStandby によるのものです。

developer.android.com

doze で1分間毎に実行するのが崩れる*3のは構わないのですが、問題は App Standby です。

AppStandby は、充電していない状態で一定の条件とネットワークの使用が 一日に一回 に制限されるというものです。

この "一定の条件" とは

  • The user explicitly launches the app.

  • The app has a process currently in the foreground (either as an activity or foreground service, or in use by another activity or foreground service).

  • The app generates a notification that users see on the lock screen or in the notification tray.

Optimizing for Doze and App Standby | Android Developers

となっています。

裏で毎分取得して API に送信するようなアプリはわざわざ立ち上げることも無いでしょうし、通知も出すことも無いので、バックグラウンドで走らせているとこの条件に当てはまるのは必須です。せめて端末が点いているときは送信して欲しいので、あまり推奨はされていませんが Forground Service を用いています。

Forground Service を使うと電池持ちが悪くなるように思えますが、 Forground Service で定期実行してもスリープ中は動作は遅れているので影響はほとんどありませんでした。

また、稀に CPU の使用率が -100 % になったりするみたいです。ちゃんと見ていませんが、これは CPU 使用量などの値が定期的にリセットされることによるものだと思います。結局マイナスの場合は例外を投げることにしました。

まとめ

proc を読んで Kotlin で mackerel-agent を実装していくのは楽しかったです。送信したデータを見るのも、アプリを使っている時間にメモリや CPU が使われているのが分かったりして面白いです。ぜひ入れてみてください。PR などもご気軽にどうぞ!

*1:ちなみに RxJava を Kotlin から使うと時々 SAM が辛くなったりする… https://youtrack.jetbrains.com/issue/KT-14754

*2:NDK を入れて C から System Call を呼べば取得できる気がしますが、今回は行っていません。

*3:システムがしばらく未使用状態となり Doze に入ると AlarmManager の定期実行が遅延して、許可された時間のみに行われるようになる

軽い Shell を作った

↓の記事を読んで自分でもサクッと作れそうなので作ってみた。

brennan.io

名前は nsh (nonylene shell の略)。記事を参考にコードを書いていたら大体同じようなものになってしまった。

github.com

記事では C で書いているけど、nsh では Python3 で書いてみた *1。結局パースで6割ぐらい占めていて、コマンドを実行するのはこの部分だけ。

def execute_line(args):
    pid = os.fork()
    if pid == 0:
        os.execvp(args[0], args)
    elif pid < 0:
        print("error with fork!")
    else:
        while True:
            wpid, status = os.waitpid(pid, os.WUNTRACED)
            if os.WIFEXITED(status) or os.WIFSIGNALED(status): break

それなりに使えるようになったので満足。

f:id:nonylene:20161209003000p:plain

fork して execvp している以外は通常の Python の処理なので、特段困ったこともなく一時間ぐらいで作れたので良かった(小並感)。

*1:今考えたらシステムコール打ちたい時に困りそうだけど、軽く作って見る分には os.fork や os.execvp を使う程度だったので大丈夫だった。