Rubyスクリプトのサービス化(systemd + rvm + ユーザ権限で実行)

# はじめに

[Raspberry Pi 3でソフトUARTを利用](https://qiita.com/suruseas/items/168f2b1250d9e5c8f0d6) で作ったGPSログ用のスクリプトが思いのほか不安定なので、例外でプログラムが終了しても再起動するように`systemd` を使ってみます。

# 前提

- rvm環境
- ユーザ権限でrubyスクリプトをサービス化する
- `systemd` を利用する

# Goal

- GPSログ用スクリプトが不慮のエラーで落ちても再起動すること

# rvmの公式ドキュメントを見る

ググって公式の以下のページができたのですが、ざっくり説明すぎてよくわからなかったので手探りでやってみました。

https://rvm.io/deployment/init-d

なお、本手順はcronなどでも使えるはずです（試してないけど:sweat_drops:）

# 手順

基本的には以下の手順となります。

- rvmのwrapperを作る
- systemdのUnit定義ファイルを作る
    - ExecStartでwrapperを介してスクリプトを実行する


# rvmのwrapperを作る

まずはrmvのパスを確認します。

```bash
pi@raspberrypi:~ $ echo $rvm_path
/home/pi/.rvm
```

そのパス配下の `wrappers` に新しいGemセット？を作成するので、念のために先に見ておきます。

```
pi@raspberrypi:~ $ ll ~/.rvm/wrappers/
total 0
lrwxrwxrwx 1 pi pi 33 Jul 18 16:22 default -> /home/pi/.rvm/wrappers/ruby-2.7.1
lrwxrwxrwx 1 pi pi 38 Jul 18 16:22 ruby-2.7.1 -> /home/pi/.rvm/gems/ruby-2.7.1/wrappers
lrwxrwxrwx 1 pi pi 45 Jul 18 16:22 ruby-2.7.1@global -> /home/pi/.rvm/gems/ruby-2.7.1@global/wrappers
```

以下のコマンドでGemセットを作成します。今回は `gps` という名前で作成してみました。公式の記載に合わせたのですがなぜ `gps` とアプリ名の指定が2個必要なのかわからず…

```bash
pi@raspberrypi:~ $ rvm alias create gps ruby-2.7.1@gps
Gemset 'gps' does not exist, 'rvm ruby-2.7.1 do rvm gemset create gps' first, or append '--create'.
Creating alias gps for ruby-2.7.1.....
```

案の定エラーが出ますがきちんと以下のように作成できたのでよしとしましょう:sweat_smile:

```bash
pi@raspberrypi:~ $ ll ~/.rvm/wrappers/
total 0
lrwxrwxrwx 1 pi pi 33 Jul 18 16:22 default -> /home/pi/.rvm/wrappers/ruby-2.7.1
lrwxrwxrwx 1 pi pi 33 Jul 19 20:01 gps -> /home/pi/.rvm/wrappers/ruby-2.7.1
lrwxrwxrwx 1 pi pi 38 Jul 18 16:22 ruby-2.7.1 -> /home/pi/.rvm/gems/ruby-2.7.1/wrappers
lrwxrwxrwx 1 pi pi 45 Jul 18 16:22 ruby-2.7.1@global -> /home/pi/.rvm/gems/ruby-2.7.1@global/wrappers
```

# systemdのUnit定義ファイルを作る

あとは通常通り作成するだけです。今回の内容は以下の通り。 `WorkingDirectory` でスクリプトのあるディレクトリを指定し、 `ExecStart` でwrapperを介してbundleコマンドを実行しています。
`Restart=always` が大切:relaxed:

```
pi@raspberrypi:~ $ cat /etc/systemd/system/gps.service
[Unit]
Description=Gps Service
After=network.target

[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/project/gps
ExecStart=/home/pi/.rvm/wrappers/gps/bundle exe ruby main.rb
Restart=always

[Install]
WantedBy=multi-user.target
```

# スクリプトを少し改造する

やっつけ作業でloggerと例外で落ちる処理を入れてみました。

```ruby
require 'serialport'
require 'nmea_plus'
require 'logger'

logger = Logger.new('gps.log')

logger.info '起動したよ'

puts `sudo rmmod soft_uart.ko`
sleep 0.1
puts `cd ~/project/soft_uart && sudo insmod soft_uart.ko`

sp = SerialPort.new('/dev/ttySOFT0', 9600, 8, 1, 0) # see: https://rubydoc.info/gems/serialport/SerialPort#set_modem_params-instance_method

trap 'SIGINT' do
  sp.close if sp
  exit
end

count = 0

source_decorder = NMEAPlus::SourceDecoder.new(sp)
source_decorder.each_complete_message do |message|
  # カウントアップ
  count +=1

  # see: https://github.com/ianfixes/nmea_plus/blob/master/lib/nmea_plus/message/nmea/rmc.rb
  if 'GPRMC' == message.data_type
    logger.info message.utc_time
    logger.info message.active? # false: データ無効
    logger.info message.latitude
    logger.info message.longitude
    logger.info message.faa_mode # A: 単独測位(精度3m程度), D: 相対測位(精度0.4m程度) 
  end

  if count >= 10
    sp.close # とりあえず閉じてみる
    logger.error '落とすよ'
    raise '強引なエラー' 
  end
end

```

# 確認してみる

まずはsystemd自体をリロードしてからサービスを起動します。

```
sudo systemctl daemon-reload
sudo systemctl start gps.service
```

ログは…いい感じですね！
これで不安定なスクリプトがちょっとは強い子になりました。

```bash
pi@raspberrypi:~ $ tail -f ./project/gps/gps.log
I, [2020-07-19T20:59:05.521956 #6437]  INFO -- : 起動したよ
I, [2020-07-19T20:59:06.651867 #6437]  INFO -- : 2020-07-19 11:59:06 +0000
I, [2020-07-19T20:59:06.652419 #6437]  INFO -- : true
I, [2020-07-19T20:59:06.652937 #6437]  INFO -- : 33.725631666666665
I, [2020-07-19T20:59:06.653450 #6437]  INFO -- : 131.64383833333332
I, [2020-07-19T20:59:06.654003 #6437]  INFO -- : A
I, [2020-07-19T20:59:07.652485 #6437]  INFO -- : 2020-07-19 11:59:07 +0000
I, [2020-07-19T20:59:07.653032 #6437]  INFO -- : true
I, [2020-07-19T20:59:07.653515 #6437]  INFO -- : 33.725631666666665
I, [2020-07-19T20:59:07.654019 #6437]  INFO -- : 131.64384
I, [2020-07-19T20:59:07.654624 #6437]  INFO -- : A
E, [2020-07-19T20:59:07.694013 #6437] ERROR -- : 落とすよ
I, [2020-07-19T20:59:12.530348 #6460]  INFO -- : 起動したよ
I, [2020-07-19T20:59:13.660674 #6460]  INFO -- : 2020-07-19 11:59:13 +0000
I, [2020-07-19T20:59:13.661174 #6460]  INFO -- : true
I, [2020-07-19T20:59:13.661502 #6460]  INFO -- : 33.72563
I, [2020-07-19T20:59:13.661792 #6460]  INFO -- : 131.64383666666666
I, [2020-07-19T20:59:13.662027 #6460]  INFO -- : A
I, [2020-07-19T20:59:14.660078 #6460]  INFO -- : 2020-07-19 11:59:14 +0000
I, [2020-07-19T20:59:14.660613 #6460]  INFO -- : true
I, [2020-07-19T20:59:14.660962 #6460]  INFO -- : 33.72562833333333
I, [2020-07-19T20:59:14.661288 #6460]  INFO -- : 131.64383666666666
I, [2020-07-19T20:59:14.661528 #6460]  INFO -- : A
E, [2020-07-19T20:59:14.662166 #6460] ERROR -- : 落とすよ
```

# 感想

- スクリプトのサービス化はrbenvとrvmどっちが楽なんだろう？
- 公式のドキュメントがざっくり過ぎてわからないとかあるんだな…