みずぴー日記

人間の再起動ボタンはハワイのビーチにある

🔬redux-saga

redux-sagaの動きを調べた。

redux-sagaは redux-sagaで非同期処理と戦うで説明されているように、非同期処理などを直列プログラムのような形式(直接形式; direct style) で書くためのライブラリである。 そのためにタスクを導入し、その切り替えを制御している。

複数のタスクを協調制御するという点で、コルーチンや軽量スレッド、fiberなどに類似していると感じた。

🔎対象

redux-saga v0.15.3を対象とする。ただし一部コードは説明のためにエラー処理や終了処理を省略する。

また counter-vanilla を元にした以下のプログラムの動きを追う。

// counter.js
//////////////////////////////////////////////////////////////////////////
//  Reducerの定義
// INCREMENTが来たら +1 する reducer
function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0
  }
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    default:
      return state
  }
}

//////////////////////////////////////////////////////////////////////////
//  Sagaの定義
const effects = ReduxSaga.effects
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

// INCREMENT_ASYNC の1秒後にINCREMENTを発生させる。
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC')
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'})
  }
}

//////////////////////////////////////////////////////////////////////////
//  redux-sagaの初期化
const createSagaMiddleware = ReduxSaga.default
const sagaMiddleware = createSagaMiddleware()
var store = Redux.createStore(
    counter,
    Redux.applyMiddleware(sagaMiddleware))

//////////////////////////////////////////////////////////////////////////
//  タスクの初期化・実行
sagaMiddleware.run(counterSaga)

//////////////////////////////////////////////////////////////////////////
//  イベントハンドラ
document
  .getElementById('incrementAsync')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT_ASYNC' })
  })

これは Increment async ボタンを押すと、1秒後にカウンタがインクリメントされるプログラムである。

f:id:mzp:20170507225036g:plain

🚀redux-sagaの初期化

reducer等を定義したのち、以下のようにredux-sagaの初期化を行なう。

// counter.js
const sagaMiddleware = createSagaMiddleware()
Redux.createStore(
    ...,
    Redux.applyMiddleware(sagaMiddleware))

createSagaMiddleware

createSagaMiddleware は以下のように定義されている。

// src/internal/middleware.js
export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
  // 渡されたオプションが妥当であることを確認する
  if(logger && !is.func(logger)) {
    throw new Error('`options.logger` passed to the Saga middleware is not a function!')
  }
  // sagaMiddlewareを定義する
  function sagaMiddleware({ getState, dispatch }) {  /* snip */ }

  // 定義した関数を返す
  return sagaMiddleware
}

引数が妥当であることを確認をした上で、内部で定義した sagaMiddlewareを返す。

sagaMiddleware.runの初期化

sagaMiddlewareapplyMiddleware の内部で呼び出される。 この関数は以下のような定義されている。

// src/internal/middleware.js
function sagaMiddleware({ getState, dispatch }) {
  // sagaMiddleware.run を初期化する
  sagaMiddleware.run = runSaga.bind(null, {
    context,
    dispatch,
    /* snip */
  })

  return next => action => { /* snip */ }
}

sagaMiddleware.runrunSaga を代入し、sagaの実行をできるようにする。 この際、 Function.prototype.bind を使って dispatch などのReduxとやりとりするために必要な関数が runSaga に渡されるようにしている。

Reduxのミドルウェアとして動く next => action => .... については、イベントハンドラの動きを追う際に見る。

🏃タスクの作成・実行

// counter.js
sagaMiddleware.run(counterSaga)

sagaMiddleware.run によって counterSaga の実行が開始される。

タスクの生成

sagaMiddleware.run には runSaga が代入されている。 これは以下のような定義となっている。

// src/internal/runSaga.js
export function runSaga(
  storeInterface,
  saga,
  ...args
) {
  // ジェネレータを呼び出す
  let iterator = saga(...args)

  // タスクを作成する
  const task = proc(
    iterator,
    /* snip */
  )

  return task
}

saga(...args)counterSaga を呼び出している。 これはジェネレータなので、ここではイテレータが返るだけで関数本体は実行されない。

ここで作ったイテレータprocに渡し、タスクを生成する。 proc は以下のようなコードになっている。

// src/internal/proc.js
export default function proc(iterator, /* snip */) {
  // タスクを作る
  const task = newTask(parentEffectId, name, iterator, cont)

  // タスクを実行するnext を呼ぶ
  next()

  // タスクを返す
  return task

  // タスクを実行する関数
  function next(arg, isErr) { /* snip*/ }
}

newTask によって、タスクを管理するオブジェクトを生成している。 その後、タスクを実行する nextを呼び出したのち、タスクを返している。

タスクの実行

next は以下のようなコードで定義される。

// src/internal/proc.js
function next(arg, isErr) {
  // イテレータを進め、次のyieldまでを実行する
  let result = iterator.next(arg)

  // 返ってきた値に応じて処理をする
  runEffect(result.value, parentEffectId, '', next)
}

iterator.next(arg)イテレータを進め、その返り値をrunEffect に渡している。

runEffect での処理が完了したのち、タスクの実行を再開できるようにするため、 runEffect には自分自身である next を渡している。

📤アクションを待つ

counterSaga は以下のように定義されているので、イテレータが進めたれた際に yield effects.take('INCREMENT_ASYNC') まで実行される。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- ここまで実行される
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'})
  }
}

effects.take('INCREMENT_ASYNC') の返り値は以下のようなオブジェクトになっており、これがそのままiterator.next() の返り値になる。

{
  "@@redux-saga/IO": true,
  "TAKE": { "pattern": "INCREMENT_ASYNC" }
}

このオブジェクトが runEffect に渡されると以下のような分岐を経て、 runTakeEffect に渡される。

// src/internal/io.js
// どの種別のエフェクトなのかを判定するための関数群を定義する
const TAKE = 'TAKE'
const createAsEffectType = type => effect => effect && effect[IO] && effect[type]
export const asEffect = {
  take : createAsEffectType(TAKE)
}

// src/internal/proc.js
function runEffect(effect, parentEffectId, label = '', cb) {
  let data
  // effectの種類に応じて、専用の関数を呼ぶ
  return (
    // Non declarative effect
      is.promise(effect)                      ? resolvePromise(effect, cb)
    : is.helper(effect)                       ? runForkEffect(wrapHelper(effect), effectId, cb)
    : is.iterator(effect)                     ? resolveIterator(effect, effectId, name, cb)
     // declarative effects
    : is.array(effect)                        ? runParallelEffect(effect, effectId, cb)
    : (data = asEffect.take(effect))          ? runTakeEffect(data, cb)
    : (data = asEffect.put(effect))           ? runPutEffect(data, cb)
    : (data = asEffect.all(effect))           ? runAllEffect(data, effectId, cb)
    : (data = asEffect.race(effect))          ? runRaceEffect(data, effectId, cb)
    : (data = asEffect.call(effect))          ? runCallEffect(data, effectId, cb)
    : (data = asEffect.cps(effect))           ? runCPSEffect(data, cb)
    : (data = asEffect.fork(effect))          ? runForkEffect(data, effectId, cb)
    : (data = asEffect.join(effect))          ? runJoinEffect(data, cb)
    : (data = asEffect.cancel(effect))        ? runCancelEffect(data, cb)
    : (data = asEffect.select(effect))        ? runSelectEffect(data, cb)
    : (data = asEffect.actionChannel(effect)) ? runChannelEffect(data, cb)
    : (data = asEffect.flush(effect))         ? runFlushEffect(data, cb)
    : (data = asEffect.cancelled(effect))     ? runCancelledEffect(data, cb)
    : (data = asEffect.getContext(effect))    ? runGetContextEffect(data, cb)
    : (data = asEffect.setContext(effect))    ? runSetContextEffect(data, cb)
    : /* anything else returned as is        */              cb(effect)
  )
}

チャンネルへの登録

runTakeEffect は以下のような定義となっている。

// src/internal/proc.js
function runTakeEffect({channel, pattern, maybe}, cb) {
  channel = channel || stdChannel
  channel.take(cb, matcher(pattern))
}

// src/internal/channel.js
function take(cb, matcher) {
  cb[MATCH] = matcher
  takers.push(cb)
}

runTakeEffect では、タスク間の通信に使われるチャンネルに対して take を呼び、 チャンネルの takers 配列に cb を追加している。 この cbnext であるため、あとでこれを呼び出せばcounterSaga の実行が再開できる。

ここまでで sagaMiddleware.run の実行は完了し、redux-sagaの初期化が完了する。

👀イベントハンドラ

イベントハンドラを見ていく。

// counter.js
document
  .getElementById('incrementAsync')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT_ASYNC' })
  })

Increment async ボタンがクリックされると、Reduxのディスパッチャに INCREMENT_ASYNC アクションが渡される。

アクションの配信

先程は省略した sagaMiddleware は以下のように定義されている。

// src/internal/middleware.js
function sagaMiddleware({ getState, dispatch }) {
  const sagaEmitter = emitter()
  // ....
  return next => action => {
    // 次のミドルウェアにアクションを転送する
    const result = next(action)

    // アクションを配信する
    sagaEmitter.emit(action)
    return result
  }
}

次のミドルウェアにそのままアクションを転送することで、reducerを起動する。 その後、アクションを sagaEmitter.emit に渡す。

emitter

emitter は以下のように定義されており、 emit されると対応する subscribers が起動する。

// src/internal/channel.js
export function emitter() {
  const subscribers = []

  function subscribe(sub) {
    subscribers.push(sub)
    return () => remove(subscribers, sub)
  }

  function emit(item) {
    const arr = subscribers.slice()
    for (var i = 0, len =  arr.length; i < len; i++) {
      arr[i](item)
    }
  }

  return { subscribe,  emit }
}

チャンネル

チャンネルの take で利用していた stdChannel は以下のように定義されている。

// src/internal/proc.js
export default function proc(
  iterator,
  subscribe = () => noop,
  /* snip */
) {
  // procの引数として渡されたsubscribeを用いてチャンネルを作成する
  const stdChannel = _stdChannel(subscribe)
  ....
}

// src/internal/channel.js
export function _stdChannel(subscribe) {
  // eventChannel を用いてチャンネルを作成する
  const chan = eventChannel(cb => /* snip */)

  return {
    ...chan,
    take(cb, matcher) { /* snip */  }
  }
}

export function eventChannel(subscribe, buffer = buffers.none(), matcher) {
  const chan = channel(buffer)

  // 何かがemitされた場合は、それをチャンネルにputする
  subscribe(input => {
    chan.put(input)
  })

  return {
    take: chan.take,
    flush: chan.flush
  }
}

eventChannel で入力をそのままチャンネルに put する関数を登録している。 そのため、ディスパッチャに渡されたアクションが、チャンネルへと put される。

チャンネルへのput

チャンネルの put は以下のように定義されている。

// src/internal/channel.js
function put(input) {
  // takers配列が空の場合はバッファに追加する
  if (!takers.length) {
    return buffer.put(input)
  }

  // takers配列に関数が登録されている場合は、それに入力を渡す
  for (var i = 0; i < takers.length; i++) {
    const cb = takers[i]
    if(!cb[MATCH] || cb[MATCH](input)) {
      takers.splice(i, 1)
      return cb(input)
    }
  }
}

takers 配列に格納されている関数に入力を渡している。 今回は next が登録されているため、counterSaga の実行が再開される。

つまり、redux-sagaのミドルウェアからの put (emit)と、counterSagatake がチャンネルを挟んで対になって動作する。

f:id:mzp:20170508000833p:plain

🤝 プロミスの実行

counterSaga は 以下のように定義されているので、実行が再開されると effects.call(delay, 1000) まで実行される。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- さっきはここまで実行した
    yield effects.call(delay, 1000) // <- ここまで実行される
    yield effects.put({type: 'INCREMENT'})
  }
}

take の場合と同様に、この返り値は runEffect 内の分岐を経て、 runCallEffect に渡される。

// src/internal/proc.js (再掲)
function next(arg, isErr) {
  // イテレータを進め、次のyieldまでを実行する
  result = iterator.next(arg)

  // 返ってきた値に応じて処理をする
  runEffect(result.value, parentEffectId, '', next)
}

// src/internal/proc.js (再掲)
function runEffect(effect, parentEffectId, label = '', cb) {
  let data
  // effectの種類に応じて、専用の関数を呼ぶ
  return (
    // Non declarative effect
      is.promise(effect)                      ? resolvePromise(effect, cb)
    : is.helper(effect)                       ? runForkEffect(wrapHelper(effect), effectId, cb)
    : is.iterator(effect)                     ? resolveIterator(effect, effectId, name, cb)
     // declarative effects
    : is.array(effect)                        ? runParallelEffect(effect, effectId, cb)
    : (data = asEffect.take(effect))          ? runTakeEffect(data, cb)
    : (data = asEffect.put(effect))           ? runPutEffect(data, cb)
    : (data = asEffect.all(effect))           ? runAllEffect(data, effectId, cb)
    : (data = asEffect.race(effect))          ? runRaceEffect(data, effectId, cb)
    : (data = asEffect.call(effect))          ? runCallEffect(data, effectId, cb)
    : (data = asEffect.cps(effect))           ? runCPSEffect(data, cb)
    : (data = asEffect.fork(effect))          ? runForkEffect(data, effectId, cb)
    : (data = asEffect.join(effect))          ? runJoinEffect(data, cb)
    : (data = asEffect.cancel(effect))        ? runCancelEffect(data, cb)
    : (data = asEffect.select(effect))        ? runSelectEffect(data, cb)
    : (data = asEffect.actionChannel(effect)) ? runChannelEffect(data, cb)
    : (data = asEffect.flush(effect))         ? runFlushEffect(data, cb)
    : (data = asEffect.cancelled(effect))     ? runCancelledEffect(data, cb)
    : (data = asEffect.getContext(effect))    ? runGetContextEffect(data, cb)
    : (data = asEffect.setContext(effect))    ? runSetContextEffect(data, cb)
    : /* anything else returned as is        */              cb(effect)
  )
}

Promise.prototype.thenへの登録

runCallEffectは以下のように定義されている。

// src/internal/proc.js
function runCallEffect({context, fn, args}, effectId, cb) {
  // callの引数に渡された関数を起動する。  
  let result = fn.apply(context, args)

  // 返り値としてプロミスが返ってくるので、resolvePromiseに渡す
  return resolvePromise(result, cb)
}

function resolvePromise(promise, cb) {
 // Promise.prototype.then にコールバック関数を登録する
  promise.then(
    cb,
    error => cb(error, true)
  )
}

call エフェクトに渡された delayrunCallEffect 内で呼び出される。 その返り値となるプロミスは、resolvePromise に渡される。 resolvePromise 内では、Promise.prototype.thencb を登録する。

この cbcounterSaganext であるので、プロミスの実行が完了したのち counterSaga の実行が再開される。

🔈アクションのディスパッチ

プロミスの実行が完了したのち、nextによって effects.put({type: 'INCREMENT'}) まで実行が進む。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC')
    yield effects.call(delay, 1000) // <- さっきはここまで実行した
    yield effects.put({type: 'INCREMENT'}) // <- ここまで実行される
  }
}

take エフェクトや put エフェクトの場合と同様に、 runEffect を通じて runPutEffect が呼び出される。

Reduxへのディスパッチ

runPutEffect は以下のようになっている。

// src/internal/proc.js
function runPutEffect({action, resolve}, cb) {
  // Reduxのdispatchに引数を渡す
  let result = dispatch(action);

  // コールバック関数にその結果を渡す
  return cb(result)
}

引数に渡された {type: 'INCREMENT'} をそのままReduxのディスパッチャに渡す。 これにより counter reducer が動き、カウンタの値がインクリメントされる。

その後、cb に代入された next を呼び、counterSaga の実行を継続する。 counterSagaは以下のように定義され、effects.take('INCREMENT_ASYNC') までループし、これまでと同様の処理が続いていく。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- ここまで戻る
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'}) // <- さっきはここまで実行した
  }
}

✅まとめ

簡単なシーケンス図にまとめると以下のようになる。 直列的に実行されるように書かれている counterSaga の処理が何度も中断され、条件が満たされるたびに実行が再開されている。

f:id:mzp:20170507223441p:plain

このようにredux-sagaではタスクの切り替えを制御することで、特定のアクションが来るのを待ったり、プロミスの完了を待つなどの処理を、直接形式で書けるようにしている。

🌐Webサーバ

JavaScriptの動作確認をするときなどに、簡単なWebサーバを使いたいことがある。

これまではWebrickで書いたWebサーバを使っていたが、Rustで書き直して単一バイナリで動作するようにした。

📦インストール

https://github.com/mzp/tiny-web-serverからダウンロードできる。また以下のコマンドでもインストールできる。

curl -L -o tiny-web-server https://github.com/mzp/tiny-web-server/releases/download/1.0.0/tiny-web-server-x86_64-apple-darwin
chmod a+x tiny-web-server
./tiny-web-server

📤簡単なWebサーバ

同一ネットワークの別マシンにファイルを受け渡すときに、わざわざDropboxNASを経由すると面倒なのである。 また、 JavaScriptの動作を確認したいときも、ローカルのファイルではうまく動作しないことがある。

こういった場合、ローカルのファイルを公開するだけのWebサーバを使うと便利である。

これまではWebrickで書いた以下ようなサーバを使っていた。

#!/usr/bin/env ruby
require 'webrick'
require 'erb'
include WEBrick

s = HTTPServer.new(:Port=>8055,:DocumentRoot=> Dir::pwd,  :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"])
trap("INT"){ s.shutdown }
s.start

また、Pythonなら python -m SimpleHTTPServer で同様のことができる。

🦀Rust

Rustの勉強がしたかったので、Rustで書いてみた。 単一バイナリで動いたほうがなにかと便利かもしれないと思っている。

macOS用のバイナリは🔀Rust クロスコンパイルで述べた方法で作っている。

❤️所感

  • 所有権が難しい。特にクロージャと組み合わせると、より難しい。 わりと慣れてきた感じはするが最初はまったく分かなかった。
  • OptionとかResultとかあるし関数型言語っぽく書けるのかな、と思ったけど、そこまででもなかった。

🔀Rust クロスコンパイル

Docker for Mac内のRustでmacOS向けのプログラムを書けるようにした。

⭐️要約

以下のDockerfireでクロスコンパイルのできるイメージを作成できる。

FROM multiarch/crossbuild

# reinstall osx cross compiler to link with SDK 10.7
ENV DARWIN_OSX_VERSION_MIN="10.7"
RUN rm -rf /usr/osxcross \
 && mkdir -p "/tmp/osxcross"                                                                                   \
 && cd "/tmp/osxcross"                                                                                         \
 && curl -sLo osxcross.tar.gz "https://codeload.github.com/${OSXCROSS_REPO}/tar.gz/${OSXCROSS_REVISION}"  \
 && tar --strip=1 -xzf osxcross.tar.gz                                                                         \
 && rm -f osxcross.tar.gz                                                                                      \
 && curl -sLo tarballs/MacOSX${DARWIN_SDK_VERSION}.sdk.tar.xz                                                  \
             "${DARWIN_SDK_URL}"                \
 && yes "" | SDK_VERSION="${DARWIN_SDK_VERSION}" OSX_VERSION_MIN="${DARWIN_OSX_VERSION_MIN}" ./build.sh                               \
 && mv target /usr/osxcross                                                                                    \
 && mv tools /usr/osxcross/                                                                                    \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/omp                                                    \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/osxcross-macports                                      \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/osxcross-mp                                            \
 && rm -rf /tmp/osxcross                                                                                       \
 && rm -rf "/usr/osxcross/SDK/MacOSX${DARWIN_SDK_VERSION}.sdk/usr/share/man"

# install toolchain
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y

ENV PATH=/root/.cargo/bin:$PATH

# install target
RUN rustup target add x86_64-apple-darwin

WORKDIR /work

カレントディレクトリに hello.rs を配置した上で、以下のコマンドを実行すると、macOS向けのバイナリが生成される。

docker build -t rust-cross-compile .
docker run -v $PWD:/work -it -e CROSS_TRIPLE=x86_64-apple-darwin rust-cross-compile rustc --target=x86_64-apple-darwin hello.rs

📝調べたこと

上記のDockerfileを書くまでに調べたことをメモしておく。

標準ライブラリ

macOS向けにビルドされた標準ライブラリをインストールする。 これは rustup から行なえる。

$ rustup target add x86_64-apple-darwin
info: downloading component 'rust-std' for 'x86_64-apple-darwin'
info: installing component 'rust-std' for 'x86_64-apple-darwin'

リンカ

リンカがMach-O形式を理解しないので、ビルドできずエラーになる。

$ rustc --target=x86_64-apple-darwin hello.rs
error: linking with `cc` failed: exit code: 1
  |
  (snip)
  = note: hello.0.o: file not recognized: File format not recognized
          collect2: error: ld returned 1 exit status


error: aborting due to previous error

multiarch/crossbuildが各ターゲットのツールチェインを含んでいるのでこれを使うようにする。 このイメージには crossbuild というツールチェインを切り替えるシェルスクリプトが含まれているので、これを使うようにする。

CROSS_TRIPLE=x86_64-apple-darwin crossbuild rustc --target=x86_64-apple-darwin hello.rs

macOS向けの設定

標準ライブラリは OSX 10.7 向けにビルドされているが、multiarch/crossbuild に含まれるリンカは 10.6向けのものなので、うまくビルドできない。

CROSS_TRIPLE=x86_64-apple-darwin crossbuild rustc --target=x86_64-apple-darwin hello.rs
error: linking with `cc` failed: exit code: 1
  |
  (snip)
          ld: warning: object file (...) was built for newer OSX version (10.7) than being linked (10.6)
          ld: targeted OS version does not support use of thread local variables in __ZN4core6result13unwrap_failed17h374a94f6b7a942a9E for architecture x86_64
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

error: aborting due to previous error

そのため、macOS向けのツールチェインを再ビルドする。

export DARWIN_OSX_VERSION_MIN="10.7"
rm -rf /usr/osxcross \
 && mkdir -p "/tmp/osxcross"                                                                                   \
 && cd "/tmp/osxcross"                                                                                         \
 && curl -sLo osxcross.tar.gz "https://codeload.github.com/${OSXCROSS_REPO}/tar.gz/${OSXCROSS_REVISION}"  \
 && tar --strip=1 -xzf osxcross.tar.gz                                                                         \
 && rm -f osxcross.tar.gz                                                                                      \
 && curl -sLo tarballs/MacOSX${DARWIN_SDK_VERSION}.sdk.tar.xz                                                  \
             "${DARWIN_SDK_URL}"                \
 && yes "" | SDK_VERSION="${DARWIN_SDK_VERSION}" OSX_VERSION_MIN="${DARWIN_OSX_VERSION_MIN}" ./build.sh                               \
 && mv target /usr/osxcross                                                                                    \
 && mv tools /usr/osxcross/                                                                                    \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/omp                                                    \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/osxcross-macports                                      \
 && ln -sf ../tools/osxcross-macports /usr/osxcross/bin/osxcross-mp                                            \
 && rm -rf /tmp/osxcross                                                                                       \
 && rm -rf "/usr/osxcross/SDK/MacOSX${DARWIN_SDK_VERSION}.sdk/usr/share/man"

ビルド

これでビルドできるる。

$ CROSS_TRIPLE=x86_64-apple-darwin crossbuild rustc --target=x86_64-apple-darwin hello.rs
$ file hello
hello: Mach-O 64-bit x86_64 executable

複数列Slack

Slack のデスクトップアプリは、1つのチャンネルしか表示できないため一覧性が悪い。 そこで、複数のチャンネルを一度に表示できるアプリを作った。

f:id:mzp:20170430175855p:plain

📦ダウンロード

https://github.com/mzp/SlackStack

😫チャンネル切り替え作業の増加

Slack のデスクトップアプリは、チャンネルを1つしか表示できない。

そのため、参加するチャンネルに比例し、切り替え作業が増えていく。 さらに切り替え作業中に未読が増え、延々とチャンネルを切り替え続けることになる。

また同様の理由でメッセージを見落すことも増え、チャットのレスポンスも悪化していく。

📑Slack☆Stack

そこで一画面で複数チャンネルの内容を確認するためのアプリを作成した。 オフィスでは縦置きのモニタを使っているので、横方向にも縦方向にも重ねれるようになっている。

f:id:mzp:20170430181429p:plain

🔧開発の様子

ブラウザを並べる

f:id:mzp:20170430182842p:plain

Slackを開いたブラウザを複数並べて、複数のチャンネルを見れるようにした。 Magnet – Window manager for Macでウインドウを整列した上で、Slack: Hide sidebar when window is narrow | Userstyles.orgでチャンネルリストを非表示にした。

Slackアプリ

WebViewを貼りつけただけのアプリを作ったら、それなりに動くようになった。 Slackはすごい。

複数行化

そのまま横に並べて複数行化した。 また、読み込み時にカスタムCSSを読み込ませ、チャンネルリストを非表示にしている。

チャンネルの追加、削除に対応

f:id:mzp:20170430185017p:plain

メニューを実装し、チャンネルの追加・削除をできるようにした。

NSStackView

NSStackViewを使い、何個でも横に並べれるようにした。

行の追加

NSStackView自体をNSStackViewで重ねるようにし、縦方向にも重ねれるようにした。

fastlane

fastlane/gym at master · fastlane/fastlane · GitHubを使って、ビルド・署名するようにした。 LoveLiverのGymfileを参考にしている。

アイコン

StackのアイコンYosemite風のアイコンが簡単に作れるツール作った - Qiitaを組合せて、アイコンを作った。

f:id:mzp:20170430190045p:plain

配布用zip

Github releases にアップロードするためにzipファイルを作成したら、なぜか署名が破損した。 Finderから圧縮するようにしたら破損しなくなった。

😀 for zsh

github等で使える絵文字コード(:sushi: みたいなやつ)を入力するzaw のsourcesを作った。

f:id:mzp:20170401115250p:plain

コミットメッセージの先頭に絵文字をいれるようにしているので、それを入力するときに便利。

f:id:mzp:20170401115943g:plain

📦インストール

antigen でインストールする。

antigen bundle mzp/zaw-emoji

🛠絵文字コードの取得

文字コードと絵文字の対応はgemojiで生成している。

https://github.com/uasi/skk-emoji-jisyoを参考にした。

🐬Docker for Mac without qcow2

Docker for MacのファイルIOが遅い。VirtualBox上で動いているDockerと比較しても遅い。

これにはいくつかの原因があるが、その1つとしてディスクイメージのフォーマットとしてqcow2を用いていることがあげられる。

qcow2は書き込み時に必要な容量を確保するcopy on write方式のディスクイメージだが、これを事前に必要な容量をすべて確保するrawファーマットに差し替えたところ、パフォーマンスが改善した。

使い方

既存のDockerイメージは消えます

https://github.com/mzp/docker.img

curl https://raw.githubusercontent.com/mzp/Docker.img/master/install.sh | bash

ベンチマーク

fioを用いてベンチマークを行なったところ、Docker Machine(VirtualBox)と同程度のパフォーマンスはでるようになった。*1

読み込み

Average(MiB/s) Min(MiB/s) Max(MiB/s)
native 361 361 361
docker machine 105 105 105
qcow2 46 46 46
raw 147 147 147

書き込み

Average(MiB/s) Min(MiB/s) Max(MiB/s)
native 487 487 487
docker machine 51 51 51
qcow2 29 29 29
raw 45 45 45

やっていること

Docker for Machyperkitを使い、macOS上でLinuxを動かし、その上でDockerを動かしている。

hyperkitはrawフォーマットのイメージに対応しているので、Docker for Macが起動する際にhyperkitに渡す引数を変更することで、ディスクイメージをqcow2からrawに差し替えている。

ただ起動時に渡す引数を設定で変更することができないので、hyperkitのバイナリをシェルスクリプトに差し替えて、無理矢理変更している。

*1:実行したコマンド等は https://github.com/mzp/docker.img に書いた

ぺろぺろ - Github pull request bot framework -

名古屋Ruby会議03で発表した。

発表資料

関連記事

原稿

導入

自己紹介

f:id:mzp:20170305002426p:plain

こんにちは、mzpです。

大須はたまにくるので、この大須演芸場も気になってはいたんですが、なかなか入る機会がありませんでした。 まさかこっち側に立つのが最初だとは思っていませんでした。

よろしくお願いします。

会社紹介

f:id:mzp:20170305002429p:plain

仕事では、Misocaという会社でMisocaというWebサービスを作っています。この名古屋Ruby会議のスポンサーでもあるそうです。すごいですね。

Github

f:id:mzp:20170305002445p:plain

会社としてのMisocaはおいといて、サービスとしてのMisocaはGithubのプライベートレポジトリ上で開発を進めています。そのためGithubが開発の中心になります。

プルリクエスト

f:id:mzp:20170305002501p:plain

さらに言えばプルリクエストが中心になります。そのためプルリクエストにまつわるさまざまなルールや運用が生まれます。例えば「コンフリクトに気付いたら声をかけてあげようね」ですとか「githubでコメントしたらSlackでも教えてあげよう」などなどなどです。
 最初はこれでもよいのですが、だんだんと面倒になっていきますよね。

bot

f:id:mzp:20170305002521p:plain

そこでbotを作り、こういった運用を自動化しました。

このbotは機能を固定したものでなく、gemによって拡張できるbot frameworkとして作りました。 今日はそのbot frameworkを紹介したいと思います。

使い方

まずは簡単に使い方を話します。

Deploy to heroku

f:id:mzp:20170305002603p:plain

レポジトリに deploy to herokuボタンがあるので、押せばherokuにデプロイできます。

Webhookの設定

f:id:mzp:20170305002431p:plain

あとはGithubでWebhookの設定をすれば動きます。

カスタマイズ

f:id:mzp:20170305002438p:plain

デプロイされたgitレポジトリをcloneしてきてGemfileを更新すれば挙動を拡張できます。 例えば、コンフリクトしているプルリクエストにラベルを自動でつけるようにしたい場合は、Gemfileに gem ‘prpr-conflict_label’ と追記してpushすればOKです。

既存プラグインの紹介

次は各gemでできることを紹介していきます。

prpr-checklist

f:id:mzp:20170305002503p:plain

各プルリクエストに自動でチェックリストを投稿するgemです。

機能を更新したけどヘルプを更新するのを忘れた、とか、バリデーションルールを追加したら既存データと矛盾するようになってしまった、みたいな単純な失敗をしてしまうことありますよね。 そういうときは古来よりチェックリストを使うとよいと言われているので、それです。

チェックリストの内容

f:id:mzp:20170305002524p:plain

チェックリストの内容は、レポジトリ上にあるCHECKLIST.mdという名前のファイルを使うようにしているので、変更も容易ですし、バージョン管理もできます。

余談: これを作ったときにはなかったんですが、今のGithubにはプルリクエスト テンプレートがあるので、そっちつかってもいいかもしれませんね。

prpr-mention_comment

f:id:mzp:20170305002549p:plain

コメント中に「@xxx 」といった文字列が含まれていた場合は、そのままSlackに転送するgemです。

Githubの通知をSlackに流している方も多いと思うんですが、GithubコメントでメンションしてもSlackではメンションにはならないんですよね。 これだとなかなか気づけませんし、気付いてもらえません。

Slackの通知は気付いてくれる人が多いので、これで解決です。

アカウント名の対応

f:id:mzp:20170305002606p:plain

工夫ポイントとしては、Slackのユーザ名とGithubのユーザ名が違う人もいるので、ユーザ名の変換もやっていることです。 これはレポジトリのルートに以下のようなファイルを置くことでカスタマイズできるようになっています。

prpr-gemfile

f:id:mzp:20170305002440p:plain

gemGemfile.lockのdiffを確認して、メジャーバージョンアップとマイナーバージョンアップをしている箇所にコメントをいれるgemです。

定期的にbundle updateをしてプルリクエストを送るbotを飼っているんですが、毎回Gemfile.lockの差分を見ながら「ふむふむこれはbugfixだけだし、いれていいでしょ」「う、こっちはメジャーバージョンがあがってる。ちゃんと調べなきゃ」というのをやるのは面倒なので自動化しています。

prpr-conflict_label

f:id:mzp:20170305002443p:plain

何本もプルリクエストが並行して動いてる状況だと、ちょいちょいコンフリクトが発生します。ただ、Githubのプルリクエスト一覧ではどれがコンフリクトしているかがわからず、個別のプルリクエスト画面をひらいて初めてコンフリクトしていることがわかります。

マージイベントが発生するたびに全プルリクエストを確認し、コンフリクトラベルのつけはずしをするようにしましたgemです。

これで一覧をみるだけでどれがコンフリクトしているかが分かるようになります。

まとめ

f:id:mzp:20170305002450p:plain

以上が、標準的なprprのgemです。 たぶんだいたいのシチュエーションで便利だと思います。 ボクも自分のプライベートプロジェクトで使っていたりします。

Misoca開発フローとの統合

これらとは別にMisocaでの開発フローにあわせて作ったgemもあります。 次はこういったgemについて紹介していきたいと思います。

開発フロー

f:id:mzp:20170305002526p:plain

まずは、我々の開発スタイルについて簡単に説明します。

  1. 各開発者がプルリクエストを作る。このときはまだWIP(work in progress; 作業中)というラベルをつけておき、まだレビューしなくてもいいよ、ということを明示します。
  2. プルリクエストが完成したら、レビューを依頼する。 指摘がついたら対応する。
  3. 最低1人、できれば2人がOKを出したあとでマージする。
  4. いくつかマージされたプルリクエストがたまってきたらデプロイする。

その他の特徴

f:id:mzp:20170305002553p:plain

補足情報としては、

  • レビュー担当者などは特に固定せず、互いにプルリクエストレビューを行なう
  • 基本的にはSlackを使ってやりとりする

といった特徴もあります。

prpr-review_label

f:id:mzp:20170305002608p:plain

レビュー依頼部分を支援するgemです。 このgemはREVIEWというラベルがついた場合は、Slackに「レビュー待ちにしました」という通知を流します。

この通知にはラベルをつけた人の名前とアイコンを使うようにしているので、誰が依頼をしたかがすぐ分かるようになっています。

prpr-lgtm

f:id:mzp:20170305002452p:plain

次は「マージする」という部分を支援するgemです。最初は雰囲気でやっていたのですが、

  • プルリクエスト一覧では何人がOKをだしたかがわからない
  • プルリクエストを出した人が、二人にOKされたことに気づくのに時間がかかる
 という問題がありました。

そこで、 * OKの数をラベルとして表現する * 二人がOKをした場合は、プルリクエストにassignされている人にメンションを飛ばす というbotを作りました。

一覧をみるだけで何人がOKをだしているかがわかる、二人がOKを出いた瞬間に気づけるようになり、テンポよくマージしていけるようになりました。

自己

f:id:mzp:20170305002455p:plain

余談ですが、このgemを作る過程でうっかり暴走させてしまい、同僚に大量のメンションを飛してしまい、たいへん申し分けない感じになりました。ごめん。 これは自分のコメントに反応してさらにコメントをつけてしまい、それに反応して…という図です。

prpr-merged

f:id:mzp:20170305002458p:plain

「マージする」という部分を支援するgemはもう一つあります。

「マージする」という行為は影響が大きいので、できるだけみんなに把握しておいてほしいです。 そのため、マージが発生するとslackに通知されるgemを作りました。

もちろんslackのgithub連携機能でも同じ情報は流れてきますが、よりマージを目立たせるという効果があります。

デプロイ

f:id:mzp:20170305002508p:plain

最後はデプロイの支援です。

Misocaのデプロイはチャット経由でできるようになっています。

  1. チャットで「@bot デプロイしたい」と発言する
  2. botがデプロイ用の内容をまとめたプルリクエストを作る。 これはマージ先がmasterではなくリリース用ブランチになっています。
  3. デプロイ用のプルリクエストをマージすると、デプロイプロセスがはじまる
  4. デプロイされる

という流れです。

このうち、3の「デプロイ用のプルリクエストをマージすると、デプロイプロセスがはじまる」の部分はprprがやっています。

prpr-code_deploy

f:id:mzp:20170305002529p:plain

Misocaのデプロイ作業はAWSのCodedeployを使っています。 codedeployにコミットIDを送ると、デプロイしてくれます。

そこで、デプロイ用のブランチにマージされた場合は、AWS codedeployにそのコミットIDを送り、デプロイプロセスがはじまるようにしています。

まとめ

f:id:mzp:20170305002555p:plain

以上がMisoca開発におけるprprの使い方です。

あとはこのprprを作ったときの話をしていきたいと思います。

先行事例

作るときに参考ににしたものや、あとから見つけたやつなどがあるのでそれを紹介します。

クックパッド

f:id:mzp:20170305002511p:plain

チェックリストの投稿などはクックパッド開発ブログで紹介されています。 ただこれは自分たちで作っていこうな、という趣旨の記事なので、汎用のbotがあるわけではありません。

trailing space bot

f:id:mzp:20170305002513p:plain

Github上で動作するbotというのもいくつかあります。 たとえば、末尾のスペース(trainling space)を検知して修正するプルリクエストを作成するbotもいます。

これはそいつが送ってくるプルリクエストです。

しかしこれは挙動が固定されているため自分たちにあわせて機能を追加・削除していくのは困難です。

拡張可能なbot

f:id:mzp:20170305002516p:plain

プラグインで挙動を拡張できるbotは多数存在しており、開発関係だとhubotやrubotyなどが有名な気がします。

ただこれはチャット経由でなにかする、というものがほとんどで、プルリクエストに反応して何かをするものは見付けれませんでした。

設計

目標

f:id:mzp:20170305002531p:plain

自由に拡張できるgithubbotフレームワークはないことがわかったので、自分で作ることにしました。 作成時に標榜した目標は以下の通りです。

gemにより挙動を拡張できる。 コア部分と、自分たちの開発を便利にするための機能を切り離す。

botの設定変更は誰でもできるようにする。 これは先ほど紹介したチェックリスト投稿が分かりやすいと思うんですが、チェックリストの内容変更は管理者だけができる、ということになると、チェックリストへの追加作業のハードルがあがってしまいます。そこで、レポジトリ上のファイルが設定ファイルになるようにし、レポジトリにコッミトできる人間なら誰でも設定変更できるようにしました。 設定変更もgitで履歴管理されるようになるのもメリットです。

herokuで動く。 自分でちゃんとデプロイするの大変ですしね。

すばやく作る。 とりあえず日々の運用を楽にするのが先決なので、わりとさっさと作りたかったです。

構成

f:id:mzp:20170305002558p:plain

構成としては以下のようになっています。

  • どのイベントに反応するかを決めるHandler
  • 挙動を決めるAction
  • 環境変数やレポジトリ上のファイルから設定を読み込むsettings
  • Slack等に通知するpublisher

といった部分で構成されています。 またイベントを受け取る部分は通常のwebhookの他にテスト用のCLIもあります。

プラグインがやるのがhandler/actionあたりです。

例: Handler

f:id:mzp:20170305002613p:plain

マージ通知をするgemを例に、もうちょっとだけ具体的に説明します。

マージだけに反応するればいいので、handlerは以下のようになっておりプルリクエストのcloseに反応するようになっています。

例: Action(½)

f:id:mzp:20170305002533p:plain

Actionはこのようになっております。

  • 本当にマージされたかどうかの判定
  • publisher経由で通知する

例: Action(2/2)

f:id:mzp:20170305002536p:plain

送信するメッセージは、このように環境変数から読むようになっています。

やらなかったこと

f:id:mzp:20170305002538p:plain

prprは最初のバージョンを早めに出して、プルリクエストにまつわる運用を楽にしたかったので、割り切って作った部分もいくつかあります。

WebUIを作らない。 設定画面等をつけようかとも思ったんですが、認証とかを考えると面倒そうだったのと、Webhook受け取るだけでとりあえず動くだろ、ということでWebUIをつけるのはやめました。

Bitbucketのことは忘れる。 最初はGithubとbitbucketの両対応をしようかと思ったんですが、抽象レイヤーを作るのが面倒そうだったのと、bitbucketを使う予定がないので忘れることにしました。

命名

f:id:mzp:20170305002540p:plain

次は名前の話ですね。

コードを書きはじめる前に名前を決める派なので、けっこう初期からこの名前でした。 プルリクエスト=PRに反応するbotつくりたいけどなにがいいかなー?って相談しました。 よくみると夜中の12時くらいに決めてるので、まあそういうことだよなって気持ちになりました。

ヲタクっぽくて気にいっています。

よかったところ

f:id:mzp:20170305002544p:plain

あとは感想です。

  • gem化したのでどこでからがプラグインかが明確になって、クラス設計等がしやくなった
  • CLIでテストできるようにしたのでデバッグが楽
  • 外部のAPIを叩く部分をすべてプラグインにしたので、本体のspecでモックを作るのが簡単だった。

苦労したところ

f:id:mzp:20170305002601p:plain

  • githubの新機能(Approve/Project/…)などの機能がなかなかAPIにおりてこない。 ReviewのApproveのAPIがくるまで一ヶ月ほど待ちました。
  • PreviewAPIはoctokitにはいってこないので、わりと無理矢理呼ぶことになる。 こんな感じです。
  • gemに分割したのでgemspecを書いたり、レポジトリを大量に作るのが面倒だった。

まとめ

f:id:mzp:20170305002615p:plain

まとめます。

  • プルリクエストに関する運用を自動化するためのbotフレームワークを作った
  • Misocaの開発フローにあわせるためのgemがいくつかある
  • 拡張しやすいようにいくつかのコンポーネントにわけて設計している。