CentOS 7にJupyterHub 1.0を導入(SSL対応/NativeAuthenticator/DockerSpawner)

はじめに

Jupyter Notebookにユーザ管理機能を追加したJupyterHub(v1.0.0)を構築する方法をまとめます。

github.com

JupyterHubはデフォルトだとPAM認証なのでLinuxのユーザを作成(adduser/passwd)しなければなりませんし、ユーザのサインアップやパスワードの変更機能が提供されていないので実運用するには色々と難点があるんですが、あまり良い環境構築手順がなく苦労しました...。

以降ではこんなことをやります。最低限の構成としてはこれぐらいあれば良いんじゃないかと思います。

  • Apacheでリバースプロキシ
  • SSL対応
  • NativeAuthenticatorでユーザのサインアップ・承認、パスワード変更機能を追加
  • DockerSpawnerでNotebookサーバをDockerコンテナで生成

前提

rootに変更

$ sudo su -

Miniconda3のインストール

Anacondaでも良いです。

# wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# bash Miniconda3-latest-Linux-x86_64.sh

...
Do you accept the license terms? [yes|no]
[no] >>> yes

Miniconda3 will now be installed into this location:
/root/miniconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/root/miniconda3] >>> /opt/miniconda3
...
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes

# source ~/.bashrc
# rm Miniconda3-latest-Linux-x86_64.sh

Conda環境の作成

Pythonは3.7にしておきます。 anacondaに関しては、標準のSpawnerである LocalProcessSpawner を使う場合に色々な便利ライブラリが欲しかったので指定しましたが、 結局DockerSpawnerを使うのでいらないと思います。

# conda create -n py37 python=3.7 anaconda
# conda activate py37

JupyterHubのインストール

本記事執筆時点で最新バージョンは1.0.0です。

# conda install -c conda-forge jupyterhub

NativeAuthenticatorのインストール

ユーザのサインアップ・承認、パスワード変更機能を追加するため、NativeAuthenticatorを導入します。 最新版が欲しいので直接リポジトリからインストールします。

# yum install git
# cd ~
# pip install -e git+https://github.com/jupyterhub/nativeauthenticator.git@919a37460cdb46ef536985c0cb0c1109d5e0e483#egg=jupyterhub_nativeauthenticator
# cd src/jupyterhub-nativeauthenticator/nativeauthenticator/

ただどうやら上記のバージョンだとJupyterHubのv1.0.0に対応してないらしく、パスワード変更でエラーになってしまいました。 本家にIssue作成しときましたが、とりあえずコードを直修正します。

github.com

handler.ChangePasswordHandler.post のユーザ取得処理に await を追加すれば問題は解消しました。

handler.py

124     @web.authenticated
125     async def post(self):
126         #user = self.get_current_user()
127         user = await self.get_current_user()

DockerSpawnerのインストール

デフォルトのLocalProcessSpawnerだとLinuxユーザが存在しなければならず煩わしいので、 ユーザごとのNotebookサーバをDockerコンテナとして立ち上げるためDockerSpawnerを導入します。

# pip install dockerspawner

Dockerのインストール

Dockerがないのであればインストールします。

# yum install -y yum-utils device-mapper-persistent-data lvm2
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yum install -y docker-ce docker-ce-cli containerd.io
# systemctl start docker
# systemctl enable docker

Hub APIのポートを解放

Dockerコンテナからホストで動作するJupyterHubのAPIにアクセスするためFWの設定を変更します。

# firewall-cmd --add-port=8081/tcp --zone=public --permanent
# firewall-cmd --reload

設定ファイルの作成

# mkdir /etc/jupyterhub
# cd /etc/jupyterhub
# jupyterhub --generate-config

jupyterhub_config.py

# JupyterHub自体はlocalhostで起動
c.JupyterHub.ip = '127.0.0.1'

# Adminユーザを任意の名前で指定
c.Authenticator.admin_users = {'jupyter'}

# DBはSQLite3
c.JupyterHub.db_url = 'sqlite:////etc/jupyterhub/jupyterhub.sqlite'

# NativeAuthenticatorを指定
c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'

# DockerSpawner関係の設定
c.JupyterHub.hub_connect_ip = '<jupyterhub-server-ip>'
c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.hub_port = 8081
c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
c.DockerSpawner.hub_connect_ip = '10.0.1.4'

## pandas、matplotlib、seaborn、scikit-learnなどが含まれたscipy-notebookを採用
## https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#jupyter-scipy-notebook
c.DockerSpawner.image = 'jupyter/scipy-notebook:6c3390a9292e'

## ノートブックの永続化設定
notebook_dir = '/home/jovyan/work'
c.DockerSpawner.notebook_dir = notebook_dir
c.DockerSpawner.volumes = { 'jupyterhub-user-{username}': notebook_dir }

JupyterHubサービスの設定

/lib/systemd/system/jupyterhub.service

[Unit]
Description=Jupyterhub

[Service]
User=root
Environment="PATH=/opt/miniconda3/envs/py37/bin:/opt/miniconda3/condabin:/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin"
ExecStart=/opt/miniconda3/envs/py37/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
WorkingDirectory=/etc/jupyterhub

[Install]
WantedBy=multi-user.target

JupyterHubを再起動します。

# systemctl enable jupyterhub
# systemctl start jupyterhub

Apacheの設定

Using a reverse proxyを参考に設定します。

/etc/httpd/conf.d/jupyterhub.conf

<VirtualHost *:443>
  ServerAdmin <server-admin>
  ServerName <jupyterhub-server-hostname>

  # Configure SSL
  SSLEngine on
  SSLProtocol all -SSLv2 -SSLv3
  SSLCipherSuite HIGH:3DES:!aNULL:!MD5:!SEED:!IDEA
  SSLCertificateFile <ssl-certificate-file>
  SSLCertificateKeyFile <ssl-certificate-key-file>
  SSLCertificateChainFile <ssl-certificate-chain-file>

  # Use RewriteEngine to handle websocket connection upgrades
  RewriteEngine On
  RewriteCond %{HTTP:Connection} Upgrade [NC]
  RewriteCond %{HTTP:Upgrade} websocket [NC]
  RewriteRule /(.*) ws://127.0.0.1:8000/$1 [P,L]

  <Location "/">
    # Preserve Host header to avoid cross-origin problems
    ProxyPreserveHost on
    # Proxy to JupyterHub
    ProxyPass         http://127.0.0.1:8000/
    ProxyPassReverse  http://127.0.0.1:8000/
  </Location>
</VirtualHost>

SELinuxの無効化

/etc/sysconfig/selinux

SELINUX=disabled

再起動

# shutdown -r now

JupyterHubにアクセス

https://jupyterhub-server-hostname にアクセスし、ログイン画面が表示されることを確認します。

Adminユーザの作成

Adminユーザを作成するため、Signup!リンクからサインアップ画面へ遷移します。

image

ユーザ名にはjupyterhub_config.pyc.Authenticator.admin_usersに指定したjupyterを入力します。

image

Your information have been sent to the admin と表示されますが、Adminとして設定したjupyterについては自身がAdminなのですぐさまログインできるようになります。

image

Adminユーザでログイン

Login!リンクからログイン画面へ遷移し、上記で入力したアカウント情報を用いてログインします。

image

一般ユーザの作成

上記の手順と同様にサインアップ画面から任意のユーザ名、パスワードでユーザを作成します。
ただし、この時点で一般ユーザはログインすることができず、Adminユーザによる承認が必要となります。

image

image

image

Adminユーザでログインし、/hub/authorizeにアクセスし、新規作成されたユーザを承認することでアカウントが有効になります。

image

image

image

image

パスワードの変更

パスワードの変更は/hub/change-passwordから実施できます。

image

ユーザの管理

AdminユーザはControl Panel>Adminからユーザの管理(Notebookサーバの停止、ユーザの削除など)を行うことができます。 ただし、Add Usersでユーザを作成してもパスワードが設定されないのでログインができません。使用しないことをおすすめします。

image

Jupyterノートブックの保存先

各ユーザのJupyterサーバはDockerコンテナとして立ち上がります。

# docker ps
CONTAINER ID        IMAGE                                 COMMAND                  CREATED             STATUS              PORTS                       NAMES
350c3f757230        jupyter/scipy-notebook:6c3390a9292e   "tini -g -- start-no…"   About an hour ago   Up About an hour    127.0.0.1:32911->8888/tcp   jupyter-test
bb36d90c7890        jupyter/scipy-notebook:6c3390a9292e   "tini -g -- start-no…"   2 hours ago         Up 2 hours          127.0.0.1:32910->8888/tcp   jupyter-jupyter

Jupyterノートブックは各コンテナ上の/home/jovyan/work配下に保存されます。

# docker exec -it jupyter-jupyter bash
$ cd work/
$ pwd
/home/jovyan/work
$ ls
foo.ipynb

DataTables(+Django)でAjax通信

はじめに

DataTables 1.10.12 (https://datatables.net/)でAjax通信を行う方法をまとめます。
以降のサンプルプログラムではDjangoを使用しています。

DataTablesの設置

まず、次のテーブルをWebページに設置します。
デザインにはBootstrapを使用しています。

f:id:hiroki-sawano:20181121002412p:plain

table 要素の idmytable とし、ヘッダのみ作成しておきます。

templates/myapp/datatable_sample.html

<table id="mytable" class="table table-striped table-bordered table-hover dataTable no-footer dtr-inline">
    <thead>
        <tr>
            <th>Column1</th>
            <th>Column2</th><th>Column10</th>
        </tr>
    </thead>
</table>

続けて、DataTableの初期化を行います。
$(document).ready() で各種設定を行います。

templates/myapp/datatable_sample.html

$('#mytable').DataTable({
  scrollX: true,
  ordering: false,
  displayLength: 25,
  deferRender: true,
  ajax: {
    "processing": true,
    "url": "{% url 'myapp:action' %}",
    "data": function( d ) {
      d.param1 = $('#id_param1').val();
      d.param2 = $('#id_param2').val();
      d.param3 = $('#id_param3').val();
    },
    "dataSrc": ""
  },
  columns: [
    { "data": "item1",
      "render": function (data, type, full, meta) {
        return '<a href="/link/to/another/page?param=' + full.item1 + '">' + full.item1 + '</a>';
      }
    },
    { "data": "item2" },
    { "data": "item3" },
    { "data": "item4" },
    { "data": "item5" },
    { "data": "item6" },
    { "data": "item7" },
    { "data": "item8" },
    { "data": "item9" },
    { "data": "item10" }
  ]
});

ajax オプション

url ではリクエスト先のURLを指定します(例ではDjangoテンプレートの記法で記述)。
data ではGET時のパラメータを設定しています。

columns オプション

DataTableの各列に表示する値を設定します。
基本的な使い方として{ "data": "key" }を列の数だけ用意しておき、 keyJSONデータのキーを指定します。 例えば [{"key": 100}] というデータがサーバから返却されたとき、 対象の列には100が設定されます。
単純に値を出力するだけでなく、例えばその値を用いてハイパーリンクを構成したり、 画像へのリンクを生成したい場合には、前述の例に示したように render オプションを使用します。

deferRender オプション

多量データを処理する場合には deferRender: true を設定すると性能が向上します。
このオプションを有効にすることでデータテーブルは実際に画面表示に必要な要素のみを読み込むため、 数万件のレコードを受信した場合でも、ページングの都度必要な件数のみを処理することができます。

Ajax通信

f:id:hiroki-sawano:20181121012614g:plain

クライアントサイド

myapp:action へのGETリクエストで設定するパラメータを入力するフォームを設置します。

templates/myapp/datatable_sample.html

<form id="myform" role="form">
  <div class="form-group">
    <label>Param1</label>
    <input type="text" name="column1" class="form-control" id="id_param1">
  </div>
  <div class="form-group">
    <label>Param2</label>
    <input type="text" name="column2" class="form-control" id="id_param2">
  </div>
  <div class="form-group">
    <label>Param3</label>
    <input type="text" name="column3" class="form-control" id="id_param3">
  </div>
  <button type="submit" class="btn btn-default">Submit</button>
</form>

Submit ボタン押下時のコールバックでは DataTable().ajax.reload() によってサーバとのAjax通信を開始します。

templates/myapp/datatable_sample.html

$("#myform").submit(function(event) {
  event.preventDefault();
  $('#mytable').DataTable().ajax.reload();
  return false;
});

このときデータテーブルでは上記テキストボックスに入力した値を クエリストリングに設定しています。

従ってURLは /myapp/action?param1=foo&param2=bar&param3=baz のような形式になります。

(再掲)
templates/myapp/datatable_sample.html

  ajax: {"data": function( d ) {
      d.param1 = $('#id_param1').val();
      d.param2 = $('#id_param2').val();
      d.param3 = $('#id_param3').val();
    },
    …
  },

medium.com

サーバサイド

サーバサイドの実装は今回主題ではないため、簡単に固定のJSONを返却しておきます。
ここで param1 の設定がない場合をページロード時の通信であると判断して空のJSONを返却しています。
どうやら serverSide : truedeferLoading : 0 にすると初回のAjax通信を抑止できるようですが試してません。

myapp/views.py

def action(request):
    if request.GET.get("param1"):
        response = '[{"item1":1, "item2":2, "item3":3, "item4":4, "item5":5, "item6":6, "item7":7, "item8":8, "item9":9, "item10":10}]'
        return HttpResponse(response, content_type='application/json')
    else:
        # Request on page load
        return HttpResponse('{}', content_type='application/json')

stackoverflow.com

Ajaxリロード時のプログレス表示

processing : true であるにも関わらず、 DataTable().ajax.reload()Loading... が表示されない問題が生じたため次のコードで対処しました。

templates/myapp/datatable_sample.html

$('#mytable').on('preXhr.dt', function(e, settings, data){
  $(this).DataTable().clear();
  settings.iDraw = 0;
  $(this).DataTable().draw();
});

stackoverflow.com

エラー処理

Ajax通信のエラー処理を実装する場合には、 xhr.dt イベントでHTTPステータス xhr.status を判定し、 サーバサイドから返却された xhr.responseJSON 内のエラーメッセージ等を表示、 DataTable().clear().draw() で空のデータテーブルを再描画すると良さそうです。

$('#mytable').on('xhr.dt', function ( e, settings, json, xhr ) {
  // Receive BadRequest status
  if (xhr.status == '400') {
    // Show error message
    $("#err_msg").text(xhr.responseJSON.error[0].message);
    $("#err_msg").show();
    // Redraw data table
    $('#mytable').DataTable().clear().draw();
    // Prevent error.dt from occurring
    return true;
  }
});

qiita.com

VimでNeomake + Pylavaを使用したPython3の自動静的コード解析

はじめに

Python3のコーディングをVimで行う際にPEP8規約違反などを自動的に検出する方法をまとめます。

静的検証ツール Pylava の導入

Pythonの静的解析ツール Pylava を使用します。

github.com

Pylava では pycodestylePyFlakes などのツール群が利用可能であるため強力なコードチェックが可能です。
同様のツールとして Pylama がありますが、現状Python3.7に未対応です。
そこで Pylama からフォークした Pylava を選択しました。

github.com

github.com

Pylava では例えば次のような検証ができます。

  • pycodestyle : コードのPEP8規約準拠
  • pydocstyle : docstring のPEP257規約準拠
  • pyflakes : 論理的なエラー検知(未使用のモジュールや未定義の名前など)
  • Maccabe : McCabeの複雑度

インストール

$ pip install pylava

Pylavaの設定

pylava は実行時にカレントディレクトリから設定ファイルを探します。
pylava.ini を作成することで使用するlinterや無視する規約などを指定することができます。 pydocstyle はデフォルトだと無効なので明示的に記述する必要があります。

pylava.ini

[pylava]
skip = */settings.py
linters = pycodestyle,pydocstyle,pyflakes,mccabe
ignore = D203,D213,D401,D406

個人設定では ignore には以下のエラーコードを指定しています。
PEP257では規約内で矛盾が生じている箇所があるため個人の好みやプロジェクト内のルールに従い選択が必要です。

エラーコード エラーメッセージ
D203 1 blank line required before class docstring
D213 Multi-line docstring summary should start at the second line
D401 First line should be in imperative mood
D406 Section name should end with a newline

Pylavaの動作確認

例として次のサンプルプログラムを用意します。
使用していない sys のimportが行われている等の不備に気付くかと思います。

sample.py

import sys

def plus(a, b):
    print(a + b)

plus(1,2)

sample.py は1と2を足して3を出力するだけの単純なプログラムです。

$ python sample.py 
3

このままでも実行可能ですが、 pylava を実行することで改善点が多々あることが検知できます。

$ pylava
sample.py:1:1: D100 Missing docstring in public module [pydocstyle]
sample.py:1:1: W0611 'sys' imported but unused [pyflakes]
sample.py:3:1: E302 expected 2 blank lines, found 1 [pycodestyle]
sample.py:3:1: D103 Missing docstring in public function [pydocstyle]
sample.py:6:1: E305 expected 2 blank lines after class or function definition, found 1 [pycodestyle]
sample.py:6:7: E231 missing whitespace after ',' [pycodestyle]

VimでPylavaを自動実行

Neomakeの導入

コーディング後に都度 pylava を実行するのは手間なのでVim上で自動的にエラー検知を可能にします。 pylavaの自動実行にはVimプラグイン neomake を使用します。

github.com

dein.toml

[[plugins]]
repo = 'neomake/neomake'

Neomakeの設定

Neovimの場合は .init.vimVimの場合は .vimrcNeomake から Pylava を使用するための設定を記述します。

.init.vim(.vimrc)

call neomake#config#set('ft.python.pylama.exe', 'pylava')
call neomake#configure#automake('nrwi', 100)
let g:neomake_open_list = 2
let g:neomake_python_enabled_makers = ['pylama']
let g:neomake_python_pylama_maker = {
        \ 'args': ['--format', 'parsable', '-o', '~/pylava.ini'],
        \ 'errorformat': '%f:%l:%c: [%t] %m',
        \ 'postprocess': function('neomake#makers#ft#python#PylamaEntryProcess'),
        \ 'output_stream': 'stdout',
        \ 'exe': '/usr/local/bin/pylava'
        \ }

特に注意が必要な点として、 pylava を 使用するためには次の設定が必要です。

.init.vim(.vimrc)

call neomake#config#set('ft.python.pylama.exe', 'pylava')

github.com

また前述の pylava.iniVimからも共有したいので、 args には pylava -o ~/pylava.ini を指定しています(設定ファイルのパスは適宜変更してください)。

.init.vim(.vimrc)

let g:neomake_python_pylama_maker = {
        \ 'args': ['--format', 'parsable', '-o', '~/pylava.ini'],
        ...

exe には pylava のパスを指定します。
conda などで導入してある pylava を使用したい場合にもこちらで指定が可能です。

.init.vim(.vimrc)

let g:neomake_python_pylama_maker = {
        ...
        \ 'exe': '/usr/local/bin/pylava'
        \ }

エラーの一覧はロケーションリストに出力されるため、 エラー箇所への移動を簡単にするためにマッピングを設定します。

.init.vim(.vimrc)

map <C-n> :lnext<CR>
map <C-p> :lprev<CR>

動作確認

このようにしてVimから離れることなくコードの質を高めることができます。

f:id:hiroki-sawano:20181111145649g:plain

Miniconda3でPython環境構築

はじめに

本エントリではMinicondaでPython環境を構築する手順をまとめます。

インストール

Miniconda3 Python 3.7 version(投稿時点最新Ver 4.5.11)を次の手順で導入します。

  1. 次のURLにアクセス : https://conda.io/miniconda.html
  2. 該当するOSのbashインストーラをダウンロード
  3. インストーラを実行 : $ bash Miniconda3-latest-<your_operating_system>-x86_64.sh

Conda環境の作成

$  conda create --name sample-env
Solving environment: done

## Package Plan ##

  environment location: /Users/.../miniconda3/envs/sample-env

Proceed ([y]/n)? y

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate sample-env
#
# To deactivate an active environment, use
#
#     $ conda deactivate

Conda環境のアクティベート

$ conda activate sample-env
(sample-env) $ 

環境設定ファイルの作成

conda installpip install でインストールしたパッケージを含めて環境設定を持ち運びたい場合は conda env export で環境情報をエクスポートします。

$ conda env export > environment.yml

例えば次のようなymlファイルが出力されます。

environment.yml

name: sample-env
channels:
  - conda-forge
  - ...
dependencies:
  - factory_boy=2.11.1=py_0
  - faker=0.9.2=py36_1000
  - text-unidecode=1.2=py_0
  - blas=1.0=mkl
  - bzip2=1.0.6=h1de35cc_5
  - ...
  - pip:
    - atomicwrites==1.2.1
    - attrs==18.2.0
    - beautifulsoup4==4.6.3
    - coverage==4.5.1
    - ...
prefix: /Users/.../miniconda3/envs/sample-env

environment.yml のバージョン管理

パッケージの追加・更新を行う際は environment.yml を都度更新してgitなどでバージョン管理しておきます。 ただし、次の点に注意します。

prefixの削除

conda env export を行うと前述の通りconda環境のパスが prefix に出力されます。

environment.yml

prefix: /Users/.../miniconda3/envs/sample-env

このパスは環境依存なので prefix は削除します。
後述するymlからの環境作成時にはデフォルトの ~/miniconda3/envs/sample-env が使用されるだけで特に害はないです。

stackoverflow.com

GitHubから直接インストール

GitHubから直接パッケージをインストールしたい場合には、次の通り記述します。

environment.yml

dependencies:
  - pip:
    - "--editable=git+https://github.com/project-path/project.git#egg=project"

stackoverflow.com

ymlファイルからconda環境作成

ここまでに作成した environment.yml を入力に conda env create することで簡単にconda環境を再作成できます。

$ conda env create -f environment.yml
$ source activate sample-env

C#で非同期に処理するTask間のメッセージ通信

はじめに

C#でTaskを使用した非同期処理をする場合に複数スレッド間でメッセージのやりとりをする方法をまとめます。

実行環境

事前準備

MVVM Light ToolkitのMessengerを使用するのでNuGetでインストールします。 f:id:hiroki-sawano:20180907062443p:plain

プロキシの設定が必要な場合はNuGet.Configに次の記述を追加します。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="http_proxy" value="http://~~" />
    <add key="http_proxy.user" value="user_name" />
  </packageSources>
</configuration>

サンプルプログラムの構成

ここではTaskATaskBが並列実行している中で、TaskAがとあるイベントを契機にTaskBにメッセージを送信し、TaskBはメッセージを処理し終えたことをTaskAに通知するプログラムを想定します。 f:id:hiroki-sawano:20180907073634p:plain

実装

まずはじめにProgram::Main()で2つのTaskrun()します。

class Program
{
    static void Main(string[] args)
    {
        TaskA taskA = new TaskA();
        TaskB taskB = new TaskB();

        var tokenSource = new CancellationTokenSource();
        var token = tokenSource.Token;

        var tasks = new[]
        {
            Task.Run(() => taskA.run(token), token),
            Task.Run(() => taskB.run(token), token)
        };
        Task.WaitAll(tasks);
    }
}

続けてTask側ですが、各Taskのコンストラクタではメッセージ受信時に起動したいメソッドをMessenger.Default.Registerで登録しています。
TaskAResponseMessage型のインスタンスMessenger.Default.Sendされたときに、TaskA::onReceivedMessage(ResponseMessage res)を実行します。
TaskBRequestMessage型のインスタンスMessenger.Default.Sendされたときに、TaskB::onReceivedMessage(RequestMessage req)を実行します。
Messenger.Default.SendはメッセージをMessengerに登録している全タスクにブロードキャストしてしまうので、メッセージの型を変えることでメッセージの送信先を制御しています。

class TaskA
{
    public TaskA()
    {
        Messenger.Default.Register<ResponseMessage>(this, onReceivedMessage);
    }
    public void run(CancellationToken token)
    {
        // do something
        RequestMessage req = new RequestMessage();
        req.msg = "TaskA's message.";
        Messenger.Default.Send<RequestMessage>(req);
    }
    private void onReceivedMessage(ResponseMessage res)
    {
        Console.WriteLine("TaskB sent the following message : " + res.msg);
    }
}

class TaskB
{
    public TaskB()
    {
        Messenger.Default.Register<RequestMessage>(this, onReceivedMessage);
    }

    public void run(CancellationToken token)
    {
        //do something
    }

    private void onReceivedMessage(RequestMessage req)
    {
        Console.WriteLine("TaskA sent the following message : " + req.msg);
        ResponseMessage res = new ResponseMessage();
        res.msg = "TaskB's message.";
        Messenger.Default.Send(res);
    }
}

class RequestMessage
{
    public string msg { get; set; }
}

class ResponseMessage
{
    public string msg { get; set; }
}

さいごに

これでひとまずやりたいことはできましたが、より良い方法が見つかったら追記します。

LINE Messaging APIを使用したbotの作成

はじめに

LINE Botを作成してみたので本エントリにまとめます。

Herokuをはじめる

botアプリケーションはHerokuにデプロイするため、 こちらの手順に従ってHerokuアカウントの登録、Heroku CLIのインストールと使い方を学びます。

Herokuはアプリケーションをクラウドでビルド、実行、運用できるPaaSです。 devcenter.heroku.com

LINE Developersコンソールでチャネルを作成

LINEが提供するMessaging APIを使用したbotを作成するためには、 開発者アカウントの登録とLINEプラットフォームを使用するために必要な"チャネル"を作成する必要があります。 手順はこちらの記事にまとまっています。 qiita.com

次のようにチャネルの作成ではアプリ名を'My bot'としました。 f:id:hiroki-sawano:20180504073716p:plain

Herokuにはmy-bot-201805としてsample-spring-boot-echoをデプロイしました。 f:id:hiroki-sawano:20180504073733p:plain

これでLINEからMy botにメッセージを送ると、送信したメッセージがMy botから返ってきます。 f:id:hiroki-sawano:20180504073748p:plain

サンプルbotの修正

ここまででEchoサンプルボットの動作確認ができました。
続けて少しだけサンプルアプリsample-spring-boot-echoに手を加えてみます。

まず、Herokuからmy-bot-201805リポジトリをgitでクローンします。

$ heroku git:clone -a my-bot-201805
Cloning into 'my-bot-201805'...
warning: You appear to have cloned an empty repository.

しかし、上記に示すように空のリポジトリをクローンした、と警告されます。 この事象についてはこちらのヘルプに載っていました。
どうやらsample-spring-boot-echoのREADMEで行なったように、 Herokuボタンを使用したHeroku API経由のデプロイだとこのような結果になるようです。
Why do I see a message 'You appear to have cloned an empty repository' when using `heroku git:clone`? - Heroku Help

同ヘルプページのResolutionに従ってHerokuボタンのリンクからgitリポジトリのURLを取得し、git remote addgit pullすれば良いです。

$ cd my-bot-201805/
$ git remote add origin https://github.com/line/line-bot-sdk-java
$ git pull origin master

それではbotに修正を加えます。
以下の通り、EchoApplication::handleTextMessageEvent()に"How are you?"というメッセージを受け取った場合には"I'm great!"と応答するコードを追加します。

EchoApplication.java

@EventMapping
public TextMessage handleTextMessageEvent(MessageEvent<TextMessageContent> event) {
    System.out.println("event: " + event);
    if (event.getMessage().getText().equals("How are you?")){
        return new TextMessage("I'm great!");
    } else {
        return new TextMessage(event.getMessage().getText());
    }
}

変更を反映します。

$ git add .
$ git commit -am "Make it more interactive"
$ git push heroku master

LINEからメッセージを送ると期待通りに動作することが確認できました。 f:id:hiroki-sawano:20180504081944j:plain

さいごに

bot作成は思った以上に簡単にできることがわかりました。なかなか面白いです。
実用的なbotアプリの実装に向けてこれからアイディアを捻出してみようと思います。

Vimのmakeで任意の名前のMakefileを指定する

はじめに

Vimmakeコマンドは便利ですが、任意の名前のMakefileをオプションで指定できそうもありません (カレントディレクトリのMakefileを探しにいきます)。
そのため所属するプロジェクトでMakefileの格納先ディレクトリや命名規則が規定されている場合は不便です。

そこで本エントリではこの問題の解決策をまとめます。

問題

Cobolプログラムで説明します。
次に示す例では、cobファイルとMakefileを格納しているディレクトリが異なり、 Makefileには<PROGRAM-ID>.makといった名称をつけるルールが定められています。

$ tree
.
├── make
│   └── hello.mak
└── source
    └── hello.cob

hello.cob

       IDENTIFICATION DIVISION.
       PROGRAM-ID. HELLO.
       PROCEDURE DIVISION.
       MAIN SECTION.
           DISPLAY 'HELLO WORLD!!'.
           STOP RUN.

hello.mak

hello.o    :    hello.cob
  cobc -x --free hello.cob

このような環境においてhello.cobVimで編集中にmakeしたい時、../make/hello.makを指定する方法を考えます。

対応策

ただ単にmakeしたいだけであれば、:!make -f <makefile_name>を実行すれば良いです。
しかしこの場合、Quickfixリストが機能しないため良い解決策とは言えないでしょう。

そこでmakeprgを使用します。
makeprgを使用することで:makeを実行した時に発行されるコマンドを任意に設定することができます。

次の設定を.vimrcに追加します。
ノーマルモードでF5を押下した時にCBLCOMP関数を呼び出す(1行目)
makeprgmake -f ../make/<cob_file_name>.makを組み立てて設定(4行目〜8行目)
makeコマンドを実行(9行目)

.vimrc

001 nnoremap <F5> :call CBLCOMP()<CR>
002 
003 function! CBLCOMP()
004  let &makeprg='make -f '
005  let &makeprg.=expand('%:p:h:h')
006  let &makeprg.='/make/'
007  let &makeprg.=expand('%:t:r')
008  let &makeprg.='.mak'
009  exec ':make'
010 endfunction

これで、Vim上でF5を押すだけで編集中のプログラムに対応したMakefileを指定してmakeを実行し、make結果をQuickfixリストで確認できるようになりました。

f:id:hiroki-sawano:20180501235608p:plain
makeで構文エラーを検出した場合