ハトネコエ Web がくしゅうちょう

プログラミングとかAndroid

Wi-Fiの接続先が変化した際にシェルスクリプトを走らせる(Mac OSX)

すごく稀なユースケースだと思うんですが、
例えば研究室の無線LANに接続しているときだけプロキシ設定を有効にし、それ以外のときは無効にしたい場合を考えます。

通常であれば、研究室のWi-Fiに繋いだら

export http_proxy="http://www-proxy.waseda.jp:8080"
export HTTPS_PROXY="http://www-proxy.waseda.jp:8080"

環境変数の設定をして、家に戻ったら

unset http_proxy HTTPS_PROXY

環境変数をリセットさせる。

このようなことを何度も繰り返します。
でも、間違いなくめんどうです!

そこで、接続先の無線LANが変化した(異なるSSIDのネットワークに接続した)際に、プロキシ設定を更新することを考えます。

1. シェルの hook を考える

通常、特定のシェルスクリプトを何度も走らせたい場合、cronを用いることが一般的だと思いますが、
今回は、特定のイベントがおこなわれた際に発火する hook を使ってみます。

シェルの hook については、こちらのサイトが詳しく説明してくれていました。
大変ありがたかったです。

シェルでコマンドの実行前後をフックする - Hibariya ( http://note.hibariya.org/articles/20170219/shell-postexec.html )

hook の種類は少ないですが、
そこは if 文でがんばるとして、
preexec という、シェルに入力したコマンドが実行される直前に発火されるというこの hook を使ってみましょう。

2. 接続中の SSID を取得する

ここのサイトで知りました。

https://stackoverflow.com/questions/4481005/get-wireless-ssid-through-shell-script-on-mac-os-x

/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | awk '/ SSID/ {print substr($0, index($0, $2))}'

これを実行すると、たしかに現在接続中のSSID名称が出てきます。すごい。

SSID取得の方法がわかったので、この値を環境変数として記録しておいて、
次回呼び出したときに、前回のSSIDと変わっているのかを確認すれば良さそうです。

SSIDが変更されたかの条件分岐を最初におこなうことで、
コマンド実行ごとに発火されるこの hook によるシェルへの負荷を、できるだけ少なくしてあげることが出来ます。

3. できあがるスクリプト

上の 1. , 2. を参考にすると、例えば以下のようなスクリプトが出来上がります。

zshbashに対応させました。(bashの方は検証が足りていないので、もしかすると上手く動かないかも…)

# Check if SSID is changed, and update proxies
function check_ssid() {
  local -r LABO_PROXY="http://www-proxy.waseda.jp:8080"
  
  local previous_ssid="$CURRENT_SSID"
  local current_ssid=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | awk '/ SSID/ {print substr($0, index($0, $2))}')

  export CURRENT_SSID="$current_ssid"

  if [ "$previous_ssid" != "$current_ssid" ] ; then
    case "$current_ssid" in
      "ssid_of_labo") # SSID of your labo's network
        export http_proxy="$LABO_PROXY"
        export HTTPS_PROXY="$LABO_PROXY"
        ;;
      *)
        unset http_proxy HTTPS_PROXY
        ;;
    esac
  fi
}

function add_shell_hook() {
  local shell_pid=$$

  if ps -p $shell_pid | grep -qs "zsh" ; then
    autoload -Uz add-zsh-hook
    add-zsh-hook preexec check_ssid
  elif ps -p $shell_pid | grep -qs "bash" ; then
    local -r BASH_PREEXEC_PATH="${HOME}/.bash-preexec.sh"
    if [ ! -e $BASH_PREEXEC_PATH ]; then
      curl -s https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o $BASH_PREEXEC_PATH
    fi

    source $BASH_PREEXEC_PATH
    preexec() { check_ssid; }
  else
    echo "Error: This script supports only zsh and bash." 1>&2
  fi
}

add_shell_hook

これを ~/.zshrc (もしくは ~/.bashrc )に直書きするか、
もしくはシェルスクリプトに記述して、 ~/.zshrc ( ~/.bashrc ) から source コマンドで呼び出すようにすれば、hook が登録され、
SSID が変わるごとにプロキシ設定が更新されるようになります。

もちろん、 LABO_PROXY"ssid_of_labo" の値は適当に変えてくださいね。

4. 注意点

私のハマったところとして、
このシェルスクリプト内で export CURRENT_SSID="$current_ssid" と export している箇所がありますが、
単純なシェルスクリプトとして実行させていたときは CURRENT_SSID環境変数として保存されず悩んでいました。

【追記あり】シェルスクリプト内でexportした環境変数は実行後でも使えるのか - by shigemk2 ( https://www.shigemk2.com/entry/linux_shell_export_path )

これによると、

シェルスクリプトを実行する際には、実行するための新たな環境ができるので、環境変数はプロンプトの環境に引き継がれません

ということでとても合点がいきました。
スクリプトを使って現在の shell に環境変数を読み込ませるためには、
.zshrc に直接書くなり、 source コマンドで呼び出すなりしないといけないのですね。

5. まとめ

以上、shell の hook 機能と、
SSIDを取得できることを利用したプチハックでした。

これの延長線上で、いろいろ便利なことができそうな気がします。

以下に、より細かい設定を加えたバージョンの gist を貼っておきます。ご参考にどうぞ。

DiscordのbotをRubyで簡単に作成したのでコード晒します

同僚と松屋で食べてたら盛り上がって、 Discord の部屋を立てました。
Slackと違ってアプリ連携できないよな〜と思ってたら bot は作れると知ったので「よっしゃ! 作るしかない!」Ruby で作りました。

1. 完成形

こんなものができました。

f:id:nekonenene:20180719041905p:plain

f:id:nekonenene:20180719042000p:plain

2. 必要なもの

  • Ruby (今回は v2.4.2 および v2.5.1 で動作確認しました)
  • discordrb (Discord bot 用のRubyライブラリ)
  • VPSなどのサーバー、もしくは常時稼働させているご自宅のPC
  • それなりの権限を持っているDiscordサーバー

VPSに関しては、GCPの無料枠でGCEを使うのがいいかもしれません。
botなのでメモリもディスク容量も必要ありませんので。

参考 : GCP(Google Cloud Platform)での無料GCE(Google Compute Engine)インスタンス作成

3. 各プログラミング言語におけるライブラリ

Discord の bot は便利なことにいろんな言語で作れます。
ライブラリが充実しているので、得意な言語で書くことが出来ます。

今回は Ruby での作り方を書きますが、各プログラミング言語におけるライブラリと参考になる記事を紹介します。
以下で書いたもの以外にも、PHP, Rust, Swift, Haskell などのライブラリもあるようです。(参考 : https://discordapi.com/unofficial/libs.html

3-1. Python

気のせいかもしれないですが、Pythonbot を作っている記事を多く見かけます。
言語処理などライブラリも充実しているので、あれこれ作るには向いているのでしょうね。

ライブラリ
記事

3-2. Node.js (JavaScript)

Pythonに次いで記事が多いように感じました。
Glitchで無料で動かす手法、とても良さそうです。

ライブラリ
記事

3-3. Java

上手くやればKotlinでも書けるそうなのでチャレンジしてみてはいかがでしょう。

ライブラリ
記事

3-4. C#

C# はWebのイメージがないのでライブラリが2つもあることに驚きました。
よく考えるとゲーム制作者とC#はフレンズさんなので、Discordに関わる人が書きやすい言語である可能性がありますね。

ライブラリ
記事

3-5. Go (golang)

最近人気のgolangことGo言語ですが、なぜか日本語記事が少ないです。
ただ、こういう文字列処理を多く扱うWebアプリケーションはGo言語より他の高級言語が向いているとも言えるでしょうね。

ライブラリ
記事

4. コード公開

作ったコードがこちらです!
解説しやすいよう、実際よりかはいくらか削ってあります。(特に munou_message メソッドは長いのでガッツリ削りました)

また、API Keyなどの秘密情報は環境変数としました。
実際に使う際は、 export HOGEHOGE_KEY='secret_value_is_here' のようにコンソールに打って値を設定するなり、
コードに値を直接記入するなり、 dotenv を活用するなりしてください。

5. 解説

はじめに断っておくと、例外処理はけっこう甘いです。
例外を吐くと bot が何も返さないという結果が起こるだけで、プロセスが落ちたりするわけではないのでゆるくやってます。
ですが、本来はメソッドの仕様上 nil が返ってきてはいけない箇所があったりします。

28〜30行目: CommandBot クラスの作成

@bot = Discordrb::Commands::CommandBot.new(client_id: BOT_CLIENT_ID, token: BOT_TOKEN, prefix: "/")

BOT_CLIENT_ID, BOT_TOKEN に基づいて CommandBot クラスを作成します。
この client id および bot token の値についてはこちらの記事を参考にしてみてください。
今回の制作にあたってとても参考になったサイト様です。

DiscordのBotを作ってみよう! | 東京工業大学デジタル創作同好会traP

ここで、 client id と bot token の組み合わせが誤っていると、 @bot.run のときの接続で
「Authentication failed.」のエラーがコンソールに出力されます。

今回 "/" を指定している prefix は、
コマンドの先頭につける文字です。スラッシュ / だとDiscord側で用意されているコマンドとかぶるおそれがあるので、
!: などを使用するのが安心かもしれません。

68〜76行目: お天気案内( message, command メソッド)

### 天気 ###
@bot.message(contains: /天気は?$/) do |event|
  return if event.message.mentions.count > 0
  event.respond(weather_message)
end

@bot.command :weather do |event|
  event.respond(weather_message)
end

下の @bot.command から説明します。

先に説明した prefix の話はこの command メソッドが関連します。
prefix では "/" を指定しておりましたので、ユーザーがチャット欄に /weather と打つと反応してくれます。

f:id:nekonenene:20180719033130p:plain

つまり、prefixを ! としているのであれば、ユーザーは /weather でなく !weather と打たなければ bot が反応してくれないわけです。

次に @bot.message の方を説明します。

message メソッドは、ユーザーが何の気なしに発言したメッセージに勝手に反応します。
contains: /天気は?$/正規表現で拾うようにしています。

なお、この場合は contains: /天気は?$/ というように正規表現せずとも end_with: "天気は?" と定義しても同じになります。
オプションについて詳しくはこちらをご覧ください。 https://www.rubydoc.info/gems/discordrb/Discordrb/EventContainer#message-instance_method

これだけだと 、あとで説明するメンションにおいても、「天気は?」が含まれるメッセージに反応するようにしていますので、 「@kizuna-bot 明日の天気は?」という質問に 2回botが反応してしまう現象が発生してしまいます。ですので、

return if event.message.mentions.count > 0

を入れて、誰かへのメンションであるメッセージに関しては無視するようにしています。

本当はここで nil を返すのは良くなくて、
本来は Discordrb::Events::MessageEvent などの Event クラスが返り値となるべき箇所ですので、
https://github.com/meew0/discordrb/blob/8b93549e9bf45accd0c49401830ef3af87364071/lib/discordrb/bot.rb#L1227 でエラーが起こり discordrb 側で例外処理されています。

42〜55行目: メンションへの反応( mention メソッド)

### メンションに反応 ###
@bot.mention do |event|
  mention_users = event.message.mentions
  message = event.content

  # 不要な文字列を除去
  message.delete!("\s")
  mention_users.each{ |user|
    message.slice!("<@#{user.id}>")
  }

  reply = munou_message(message: message, event: event)
  event.respond(reply) unless reply.nil?
end

mention メソッドはこのbotに対するリプライを誰かが飛ばしたときに反応するものです。

あとの munou_message メソッドで扱いやすいよう、
「@kizuna-bot 明日の天気は?」のメンション部分を削って「明日の天気は?」を message 変数に代入するよう調整しています。

メンションは、Discord内の処理としては <@ユーザーID> という文字列の形式をとっていますので、その形を slice! メソッドを使用することで削り取っています。
なお、「#general」などのチャンネルリンクも同様で <#チャンネルID> という文字列を内部的には保持しています。

整形したら、 message と event を渡してあげて、
248行目以降の munou_message メソッドで、返すメッセージを選んでくれています。

この munou_message メソッドの語源は「人工無脳」からです。
決められたメッセージに反応するbotを俗に人工無脳と呼びます。
(Siriは人工知能なんかじゃなく人工無脳なんじゃないか、と言われたりもしますね)

294〜295行目など: そしてスタートだ!

kizuna_bot = KizunaBot.new
kizuna_bot.start

KizunaBotクラスを作り start メソッドを呼び出しています。

クラスが作られた時点で initialize メソッドが走り CommandBot クラスが作成され、
start メソッドにより、コンソールにbotの invite URL を表示させたあと、
settings メソッドで先ほどのコマンドやメンションなどの際のbotの反応を定義させ、
最後に run メソッドを呼び出すことで WebSocket による通信を開始しています。
参考 : https://github.com/meew0/discordrb/blob/8b93549e9bf45accd0c49401830ef3af87364071/lib/discordrb/gateway.rb#L504-L529

6. コンソールからの実行

この kizuna_bot_for_blog.rb の実行方法について説明します。

RubyをインストールしてあるPCで

gem install discordrb

と打ち、依存ライブラリをまずインストールします。

kizuna_bot_for_blog.rb をダウンロードして適当な場所に置いたあと、

BOT_CLIENT_ID = ENV["BOT_CLIENT_ID"].freeze
BOT_TOKEN = ENV["BOT_TOKEN"].freeze

の部分に従って、環境変数の設定をするか、もしくはこの部分のコードを直接書き換えます。

コンソールで環境変数の設定をするなら

export BOT_CLIENT_ID="1234567890"; export BOT_TOKEN="secret.token"

のように打ち込みます。
RSS2JSON_API_KEY, RECRUIT_API_KEY については、各サイトでAPI Keyを取得してくる必要があるのでここでは説明を省きます)

最後に、コンソールで kizuna_bot_for_blog.rb があるディレクトリにて

ruby kizuna_bot_for_blog.rb

と実行するだけです。けっこうシンプルです。

7. Heroku での実行

クレジットカードを持っているなら 2. で書いたとおり、GCPの無料枠でGCEを使うのが一番気軽な選択肢かと思いますが、 「24時間起動していたいけどクレジットカードも持ってないしパソコンを立ち上げっぱなしもつらい〜」という方は Heroku を利用する手段があります。

無料プランだと月に550時間(クレジットカード登録をすると1000時間になる)しか動かせないので、
使われる頻度の高いbotだと厳しいですが、botがあまり使われないようであれば、
Herokuは、30分使用されないと勝手にスリープ状態に入り、スリープ期間中は使用時間としてカウントされませんので、1ヶ月でなんとか550時間を超えないで済むかもしれません。

この550時間は、動かしているプロセスの合計時間としての制限ですので、
複数の bot を動かしたい場合は簡単に制限を超えてしまうので注意しましょう。

Heroku で実行するには Gemfile , Gemfile.lockProcfile が必要になります。
説明すると長くなりますので、詳細はいろいろググってみてください。
(ぱっとググってみた感じだと、あまり網羅的な記事が見つかりませんでした……)

github.com

今回は私のほうで Heroku で実行できるようなファイルセットにしたものを用意しました。

README に使い方を記していますので、使ってみたい方はお読みください。

8. おしまい

というわけで、大変長くなりましたが、Ruby で Discord の bot を作る方法でした。
初期セットアップの部分の記事はいくらか見ましたので(お世話になりました、ありがとうございます)、
この記事ではもう一歩踏み込んだ実用としての部分のコードをお見せしました。

Ruby は便利メソッドが大変多いので、botを手軽に作りたいときに選ぶプログラミング言語としてはなかなか向いているのではないでしょうか。

ところで今回の KizunaBot 、Kizuna という名前で Kizuna AI ちゃんの方を連想されたかたが多そうですが、
VOICEROID であり VOCALOID でもある 紲星あかり (きずなあかり)ちゃんの名前からとっていました。

かわいいですよね。

Kizuna AI + ミライアカリ みたいな名前してるので、人と話してると VTuber の名前だと勘違いされがちです。

発売が最近なのでまだ知名度ないですが、素直な(声の)子なのでがんばってほしいです。

VOICEROID2 紲星あかり|ダウンロード版

VOICEROID2 紲星あかり|ダウンロード版

かわいかろう?

いや、でも VOICEROID で言うと私はきりたんの方が欲しいんですよ。
あのダウナーで、かつリアルなしゃべり。きりたそ〜欲しい〜。

VOICEROID+ 東北きりたん EX ダウンロード版|ダウンロード版

VOICEROID+ 東北きりたん EX ダウンロード版|ダウンロード版

以上です。

エンジニアマネージャーに必要なことってなんだろう?

最近、ありがたいことにチームリーダーをやらせてもらっているのですが、
経験が無いのでどうすればいいのか手探りでやっています。

今後どうしていけばいいのか、
今の時点で得られていることを頭の整理としてまとめておきたいと思います。

インプットを続ける

エンジニアに大切なことは技術力の向上ですが、
マネージャーはそれにプラスしてマネージング力の向上を続けていかなければなりません。

そのためには人事、経理、組織論について、本やネット記事を読むだとか、
職場の他のマネージャー、他社のマネージャーに話を伺いに行き、手法を学び続ける必要があります。

そうしないと自己流が出来上がってしまい、「自分は上手くやれてるはずなのに」と、しだいに頭の固い人間になってしまいます。
上手くやれてるつもりになるのが一番危ないです。

どうすればチームがダメになるのかを知る

いくつか手段があると思います。

  1. 経験する
  2. 聞く・読む
  3. チームメンバーの性格を把握する

1 はつまり自分や自分の所属するグループが崩壊をすることです。
自分の経験なので記憶に強く残りますから、同じ失敗はしにくくなります。
問題点はそんなに何回もチームの崩壊を経験できないことです(笑)

ということで、2 の、人から聞いたり記事を読むことになるわけです。
他社の「こんな状況からこう正常化させた」という記事から、逆説的に、ダメな状況がどういうものかを学べます。

3 は上記とは別軸の話です。
一般論としての 1, 2 ではなく、現在のチームの特性としての 3 です。
個人のやる気を下げる要因がなんなのか知ることは、チーム崩壊を起こさないために大事なことです。
何がイヤなことであるか直接聞いたり、現在の職場で気になりつつあることを聞いたり、前の職場を辞めた原因はなんなのか聞いたり
1 on 1 や普段の会話で、個人個人のNGポイントを集めておきましょう。

上司が何を欲しているか知る・予見する

これはマネージャーに限らず大切なことですが、
上司が何を欲しているか予見して動くことは非常に大切です。

マネージャーの上司はマネージャーです。
チームマネージャーはメンバーの行動や未来を考えますが、その上司のマネージャーは
「チームが何をしているか」「チームがこれからどうしたいか」などを気にしています。

チームの四半期目標だったり、1 on 1 の結果報告だったり、チームが達成したことの週次報告(月次報告)だったり、
チームの状況を知るために必要なことは聞かれる前に上げていくくらいにしましょう。

あなたがチームリーダーとして上司からの信頼を得られることは、
チームが好きに動きやすくなれることにもつながります。大事なことです。

自分が頭いいと思わない

自分の頭がいいと思ってしまうと、あらゆることを自分でやろうとしてしまいます。
それはスケジュール的に自分の首を絞めてしまうばかりでなく、
結局のところ自分が気付いていないだけで他の人のほうが上手くやれることだったりして、
周りの反感を知らず知らずに買っていたりします。

周りの人間を信じましょう。
その人は失敗するかもしれませんが、失敗から学ぶこともあります。
子どもが怪我をしないように公園で遊ばせない親がいい親とは言えません。
(ただし大怪我に気付けるよう、公園のベンチから様子を確認してはおきましょうね?)

メンバーの成果は褒め称える

何かしてもらったら「ありがとう」と言うべきですし、
良い成果を上げたのなら「ナイス!」と伝えるべきです。

信じて任せて感謝する
聞いた話だとこれがけっこう難しいらしいです。

信じられて任せられて感謝されたら、誰もが気持ちよく働けるでしょう。

メンバーの市場価値を高める

エンジニアはひとつの会社には留まりません。常に新しい環境を求める生き物です。
そのとき――チームメンバーの転職――は必ずやってきます。

それは他の会社に嫁(婿)に出すようなものだと私は思っています。
ですので、親の立場となるマネージャーがそのときまでにすることは、
「どこに嫁(婿)に出しても恥ずかしくない人間にする」ことだと私は思っています。

そのために必要なことは、会社でいろいろなことを経験させること、そしてそのおこなったことを説明できる能力を身に付けさせることです。
面接で絶対に聞かれることは「前職でおこなってきたこと」です。
ここの説得力は面接の結果を左右します。

  1. いろいろな業務を経験させる
  2. その業務で何をしたのか、何を工夫したのか、チームの振り返り会や 1 on 1 で振り返らせる
  3. プレゼンテーション能力を向上させる(社内勉強会の開催など)
  4. 社外へのアウトプット(勉強会での登壇やブログなど)を勧める

これらのことを継続的におこなうと、メンバーの市場価値は上がっていくのではないでしょうか?
このことによって会社の利益が出るとかはないでしょうが、続けていけば何かいいことがある……はず? ……たぶん。

夢を持つ

自分がすごく苦手なところなんですが、夢を持つのは大切だと思います。
チームメンバーに将来どのようになりたいのか聞いてキャリアプランを設計すべき人が、将来どうなりたいのかを持っていないってのは意味がわかりませんし、
そんな人にはメンバーも正直に夢を語ってはくれないでしょう。

夢って、難しいですね……うーん、自分は将来何をしたいんだろう。
『夢をかなえよう』って曲の主人公並みに夢について考えちゃうぞ。

これは現在の自分が特に出来ていない課題です……。

1 on 1 で何を話せばいいのか?

今までのことの総まとめになります。

  • 最近会社で気がかりなこと(嫌なこと)はないかの確認
  • 直近の業務で何をしたのかの振り返り
  • 次にどういう業務をやってみたいかヒアリング
  • 会社に欲しいもの、導入したいツール
  • 短期的・長期的な個人目標

など、エンジニアマネージャーとしてチームを回していく上で必要な情報の収集をおこないます
基本は気軽に雑談をすればいいと思いますが、
こういった、チームを回していく上で必要な情報を得られていないのなら、
1 on 1 の機会を利用して積極的に情報収集をしていくべきでしょう。

おわりに

以上が、現時点で考えるエンジニアマネージャーとして必要なことです。

考えたり経験したり、読んだり聞いたりして情報を集めてはいますが、
いかんせんまだリーダー経験としては2ヶ月なので、圧倒的に経験不足!!

足りないところや間違いなどがあり、数ヶ月経ったら更新したくなりそうですが、
今の時点で一度、得たことを振り返っておきたかったのでまとめました。

まだマネージャー職という自覚がなくて、
「メンバーのタスク状況の確認とか文章書いたりでコードあまり書いてないし、重たい実装タスクはメンバーに任せてるし、私って仕事してないのでは?!」
と思う日々が続いています。

同業の人に相談したら、「いや、コードはあまり書かなくなるものだよ、それでいいんだよ」と言われましたが、
突然リーダーになってしまいコードを書くことが仕事という考えから抜けられない私はまだ戸惑っています。
慣れるかな・・・?

最後に、最近読んでよかった本をアフィリエイトとしてぺたりとー。
自分やチームメンバーが今なにが出来て、次はなにを出来るようになればいいかを考えることができる良書でした。

Googleスプレッドシートでセルの文字列の文字列結合(IFの活用)

スプレッドシート関数の書き方わからなくて困ること多いのでメモ。

f:id:nekonenene:20180225103733p:plain

こんなふうに、右の列に "/releases" と文字列をくっつけた行が続く場合には関数で書きたい。

文字列の結合は & で書ける。
なので、C2 には =B2&"/releases" と書けばいい。

ただ、これで困るのがB列の文字列が空のとき。
隣のC列には /releases とだけ書かれてしまう。

これでは見栄えが悪いので IF 文を使う。

=IF(NOT(B2 = ""), B2&"/releases", "")

これでOKだ。

!= が使えないので NOT でなんとかしている。

=IF(B2 <> "", B2&"/releases", "")
=IF(B2 = "", "", B2&"/releases") とも書けるので、個人的な見栄えの良さで選択するといいと思います。