時間のかかるプログラムのプロセス終了をSlackで通知してくれるようにする

# 概要
時間のかかるプログラム(ディープなモデルの学習など) のプロセスの終了を Slack で通知してくれるようにしたので、そのメモ。

特徴としては、

1. プログラム実行環境にて、追跡するプログラムのプロセスIDを引数とするシェルスクリプトを実行
2. Slack にて、追跡開始の確認、およびプログラムの実行コマンドの表示
3. Slack にて、プログラムの終了通知、および追跡を終了するプロセスIDの表示

を行います。

# 使用ツール & 環境

* 使用ツール
    * シェルスクリプト
    * Slack
    * [Incoming Webhook](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) (Slack App)
   
* 環境
    * Ubuntu 18.04

シェルスクリプトをほぼ初めて触ったので、冗長なコーディングがあればご指摘お願いします。

# 導入手順
### 1. Slack のチャンネルに Incoming Webhook を連携させる
[Incoming Webhook](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) へ飛んで、自分の属するワークスペースのチャンネルに連携させてください。

![ED6BFF1E-9A49-4D47-A1E4-E8EF6CEF7BFC.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544429/4283f6dd-7f9c-9aa4-a460-7b83fc3054d1.jpeg)

各自お好みで設定していただければと思います。ここで `Webhook URL` が、この後の作業で必要となります。

### 2. プロセスの追跡を行うシェルスクリプトの作成
僕は、以下のディレクトリ構成を作成しました。わかりやすければどこでも良いと思います。

```
~/
└ Documents/
      └ slack/
          └ (ここに作成するよ)
```

まず、 `~/Documents/slack/` に以下のコマンドでシェルスクリプトを作成します。

```shell
$ touch watch # 名前もなんでも良いと思います。
```
次に、エディタで `watch` を開きます。そこへ僕は以下のシェルスクリプトを記述しました。

```shell
$ vi watch
```

```shell:watch
#!/bin/bash

set -eu

URL="" ### ここにWebhook URLを記述！ ###
USERNAME="あおい姐さん"

PID=$1
if [ ! $PID ]; then
  echo "ERROR: PID required." >&2
  exit 1
fi

# 実行開始時のプロセス
START_MSG="`ps ho args $PID`"
ret=$?
if [ $ret -ne 0 ]; then
  echo "ERROR: Process not found." >&2
  exit 1
else
  ary=(`echo $START_MSG`)
  for i in `seq 1 ${#ary[@]}`
  do
  set +u
    if [ "$i" -eq 1 ]
    then
        PRETXT="${ary[$i-1]##*/} を実行したよ"
    else
        TEXT="$TEXT ${ary[$i-1]}"
    fi
  done
fi
set -u

TITLE="実行コマンド"
TEXT="\`\$ ${ary[0]##*/}$TEXT\`"
BEGIN_DATA="payload={\"username\": \"$USERNAME\", \"text\": \"追跡開始！ (PID： \`$PID\` )\", \"attachments\": [{\"fallback\": \"実行コマンド確認\",\"color\": \"#003399\",\"pretext\": \"$PRETXT\" ,\"title\": \"$TITLE\",\"text\": \"$TEXT\"}]}"

curl -s -X POST --data-urlencode "$BEGIN_DATA" ${URL} >/dev/null


# プロセスの終了時
END_MSG="${ary[0]##*/} が終了したよ\n追跡終了！ (PID： \`$PID\` )"
: "start watch ${PID}"
{
  while true
  do
    if ! ps -p ${PID} >/dev/null ;then
      curl -s -X POST --data-urlencode "payload={\"username\": \"$USERNAME\", \"text\": \"${END_MSG}\"}" ${URL} >/dev/null
      exit
    fi
    sleep 1m
  done
} &
```

こちらのコードを参考にさせていただきました。本当にありがとうございます。
>
参考 : https://gist.github.com/ohsuga/41a459792748e8c2afdb81d0b261c3de

### ３．テスト
ここまでできたら、一度テストしてみましょう。

まず、適当にPythonで30秒カウントするソースコードを作成します。同時にプロセスIDも出力してくれるように`os.getpid()`をprintするようにします。

```shell
$ vi test.py
```

```python:test.py
import os
import sys
import time

def print_args(pid, args):
    print(os.getpid(), args)

if __name__ == "__main__":
    args = sys.argv
    print_args(os.getpid(), args)

    for i in range(0, 30, 1):
        time.sleep(1)
        print(i, "sec")
```

そしてテストのコマンドが以下になります。コマンドライン引数のテストも兼ねて色々付け足しておきましょう。
ここで、プロセスIDはプロセスごとで異なるので、臨機応変に対応お願いします。以下の場合ですと`17027`です。

```shell
$ python test.py  1 2 3 hello --args-conf
17027 ['test.py', '1', '2', '3', 'hello', '--args-conf']
0 sec
1 sec
2 sec
3 sec
...
```

30秒以内にすかさず以下のコマンドを実行しましょう。

```shell
$ bash watch 17027
```

そうすると Slack の Incoming Webhook を連携したチャンネルに以下のような投稿がされると思います。
![Screenshot from 2021-01-20 23-12-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544429/c97fe6dd-94a8-d903-3f89-63b44e25f569.png)
そして30秒が経過すると。。。

```shell
26 sec
27 sec
28 sec
29 sec
$ 
```

タイムラグがあると思いますが、以下のような投稿がされると思います。
![Screenshot from 2021-01-20 23-17-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544429/72cd683e-ff6a-fc09-2623-97158002b23e.png)
### 4. PATHを通す
せっかくコマンドを作成したので、PC上のどこからでもアクセスできるようにPATHを通してしまいましょう。

まずは以下のコマンドで`~/.bash_profile`にシェルスクリプトのPATHを記述します。

```shell
$ echo 'export "PATH=$HOME/Documents/slack:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
```

エイリアスも設定して、`bash` を打たずに実行できるようにしましょう。僕は `aoi` というコマンドで実行できるようにしました。

```shell
$ echo "alias aoi='bash watch'" >> ~/.bash_aliases
$ source  ~/.bash_aliases
```

これでコマンドラインに`aoi`と打ってみて、以下のようなエラーが出ていればPATHが通っています。

```shell
$ aoi
/home/user/Documents/slack/watch: line 11: $1: unbound variable
```

# 実行例
PyTorchのチュートリアルである[MNISTの分類タスク](https://github.com/pytorch/examples/tree/master/mnist)をやってみましょう。こちらは`torch`と`torchvision`を必要とするのでご注意ください。

コードを持ってきたら、まずは以下のコマンドでmainのコードを走らせましょう。

```shell
$ python main.py --no-cuda
Train Epoch: 1 [0/60000 (0%)]   Loss: 2.305401
Train Epoch: 1 [640/60000 (1%)] Loss: 1.359780
Train Epoch: 1 [1280/60000 (2%)]        Loss: 0.830692
Train Epoch: 1 [1920/60000 (3%)]        Loss: 0.620273
Train Epoch: 1 [2560/60000 (4%)]        Loss: 0.354148
Train Epoch: 1 [3200/60000 (5%)]        Loss: 0.461558
Train Epoch: 1 [3840/60000 (6%)]        Loss: 0.278612
...
```

そして`aoi`を実行するためにプロセスIDを確認しましょう。以下のコマンドで確認できると思います。

```shell
$ ps x | grep "python main.py"
11313 pts/0    Rl+    0:10 /home/user/.pyenv/versions/3.6.7/bin/python main.py --no-cuda
11387 pts/5    S+     0:00 grep --color=auto python main.py
```

おそらくプロセスIDは`11313`ですね。それでは、`aoi`を実行しましょう！

```shell
$ aoi 11313
```

すると。。。
![Screenshot from 2021-01-20 22-22-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544429/f669f001-bce9-df10-82b1-c8e2ef070c84.png)
こんな感じで学習の開始と終了を通知してくれるようになりました。

# おわりに
まだまだ発展途上の段階の実装だと思うので、導入の際は注意してください m(_ _)m

今後の課題としては

* プロセスIDの確認の手間を省きたい
* Slack にて、学習のログの表示やエラーによる停止の通知など拡張したい

したいなと思ってます。

もしよろしければ [GitHubリポジトリ](https://github.com/junprog/slack) へのContribution絶賛お待ちしております！