発話PUSHサーバ

HTTPリクエストを受け取ってGoogle Homeに発話要求を送信するバックグラウンドプロセスである。
使用例を以下に示す。
Endpoints:
    http://192.168.11.39:8091/google-home-notifier
    https://XXXXXXXX.ap.ngrok.io/google-home-notifier
GET example:
curl -X GET https://XXXXXXXX.ap.ngrok.io/google-home-notifier?text=Hello+Google+Home
POST example:
curl -X POST -d "text=Hello Google Home" https://XXXXXXXX.ap.ngrok.io/google-home-notifier
リスト1.発話PUSHサーバの利用例


ここで、EndpointにIP指定とホスト名指定の2通りがある。通常、発話PUSHサーバはWiFiネットワーク内(したがってプライベートIP空間)で動くので、インターネット側からの発話依頼に応えられない。

そこで、インターネット側からのリクエストも受け付けられるようにngrokを利用する。上記の"https://XXXXXXXX.ap.ngrok.io/"はngrokによって払い出されたURLである。ここでURLの"XXXXXXXX"の部分は、利用しているngrokのアカウントにログインし、Online Tunnel Endpoints(メニューから[Endpoints]→[Status]を選択)を表示すれば確認できる(下図)。

図1.ngrokのOnline Tunnel Endpoints


このngrokが払い出すURLを使えばインターネット側からでも発話PUSHサーバに発話要求を送ることができる。

ところが、このngrokは、発話PUSHサーバを起動するたびに 払い出すURLを変える。したがって、それに伴って、呼び出し側のプログラムも修正しなければならない。これは甚だ不便である。そこで、新しいURLが払い出される都度、その新しいURLを呼び出し側に通知する仕組みを組み込む。

発話PUSHサーバにリクエストを送るGAS WebAPIは、WebAPI (A2)のみである。したがって、WebAPI (A2)にngrokのURLの変更を受け付ける機能を追加する。変更通知を受け取ったGASは、Googleスプレッドシートファイル"pill-reminder"の「設定」シートのセル(7行2列)にURLを書き込む。

WebAPI (A2) のPOSTメッセージを受け取る関数は次のようになる。

/**
 * IFTTTからのリクエストに応じてGoogleカレンダーを服用済みに更新する
 * @param  {Object}   e     POSTメソッドで送られてくるデータ
 *  delay: 10 (開始を10分間遅らせる)
 *  timing: [朝|昼|夜|就寝前]
 *  ngrok_url: https://XXXXXXXX.ap.ngrok.io/google-home-notifier
 *
 * https://script.google.com/macros/s/AKfy............................nIbMw/exec
 */
function doPost(e) {
  debug(JSON.stringify(e, null, ' '));
  var params = JSON.parse(e.postData.getDataAsString());  // ※
  
  debug('params=\n' + JSON.stringify(params, null, ' '));
  if (params.timing) {
    //カレンダーのタイトルと色を変える
    var timing = params.timing;  // => [朝|昼|夜|就寝前]が取れる
    NotifyNotTaking(timing);
  } else if (params.delay) {
    //開始時刻を遅らせる
    var delay = parseInt(params.delay);  // => 遅らせる時間[分]
    // どうやって遅らせる対象のイベントを見つけるか?
    delayStartTime(delay);
  } else if (params.ngrok_url) {
    //ngrokの払い出したurlをGoogleスプレッドシートに書き込む
    setNgrokUrl(params.ngrok_url);
  }
  
  var out = ContentService.createTextOutput();
  
  //Mime TypeをJSONに設定
  out.setMimeType(ContentService.MimeType.JSON);
  
  //JSONテキストをセットする
  result = {
    status: 'OK'
  };
  out.setContent(JSON.stringify(result));
  
  return out;
}
リスト2.WebAPI (A2) POSTメッセージを受け取る関数

なお、リクエストする側は、下記のようなJSONフォーマットのデータをPOSTする。
{
  "ngrok_url": "https://XXXXXXXX.ap.ngrok.io/google-home-notifier"
}
リスト3.WebAPI (A2) に送るPOSTデータ

ngrokの払い出したurlをGoogleスプレッドシートに書き込む関数setNgrokUrlを以下に示す。
/**
 * ngrokのurlをGoogleスプレッドシートに書き込む
 * Googleスプレッドシート名:pill-reminder
 * セル:7行2列
 * @param  {String}   url     ngrokが払い出したurl
 */
function setNgrokUrl(url) {
  var spreadsheet = SpreadsheetApp.openById('1TRT..........................TbbY');
  var sheet = spreadsheet.getSheetByName('設定');
  sheet.getRange("B7").setValue(url);
}
リスト4.関数setNgrokUrl()

また、Googleスプレッドシートからngrokが払い出したurlを取得する関数getNgrokUrlを以下に示す。
/**
  * ngrokのurlをGoogleスプレッドシートから読み込む
  *
  */
function getNgrokUrl() {
  var spreadsheet = SpreadsheetApp.openById('1TRT..........................TbbY');
  var sheet = spreadsheet.getSheetByName('設定');
  return sheet.getRange("B7").getValue();
}
リスト5.関数getNgrokUrl()

ベアボーンサーバ上で動く発話PUSHサーバ(example.js)の修正


ベアボーンサーバ上で動く発話PUSHサーバ(example.js)は、起動時、リスト3に示すJSONフォーマットのデータをWebAPI (A2) へ送信する必要がある。そこで、ngrok url払い出し処理部分を以下のように修正した。
app.listen(serverPort, function () {
  putLog('Endpoints:');
  putLog('    http://' + ip + ':' + serverPort + '/google-home-notifier');
  
  connectNgrok(serverPort).then(url => {
    var ngrok_url = url + '/google-home-notifier';
    putLog('    ' + ngrok_url);
    putLog('GET example:');
    putLog('curl -X GET ' + ngrok_url + '?text=Hello+Google+Home');
    putLog('POST example:');
    putLog('curl -X POST -d "text=Hello Google Home" ' + ngrok_url);
    sendNgrokUrl(ngrok_url);
  });
})

/** 
 * ngrokを非同期で起動
 */
async function connectNgrok(port) {
 try {
  let url = await ngrok.connect({
   addr: port,
   region: 'ap', 
   configPath: '~/.ngrok2/ngrok.yml',
   authtoken: token
  });
  return url;
 } catch (err) {
  putLog(err.name + ': ' + err.message);
 }
}

/**
 * ngrokが払い出したurlをWebAPI(A2)を使ってGoogleスプレッドシートへ書き込む
 * Googleスプレッドシート: semi2018kumwのpill-reminder
 *
 */
function sendNgrokUrl(ngrok_url) {
 var WebAPI_A2_URL = "https://script.google.com/macros/s/AKfy...............................nIbMw/exec";
 webclient.post({
   url: WebAPI_A2_URL,
   headers: {
     "content-type": "application/json"
   },
   body: JSON.stringify({"ngrok_url": ngrok_url})
 }, function (error, response, body){
   if(!error) {
     putLog("****** sendNgrokUrl 成功 ******");
   } else {
     putLog("****** sendNgrokUrl エラー ******");
     putLog('error:' + error);
     putLog('response:' + response);
     putLog('body:' + body);
   }
 });
}
リスト6.ngrok url 払い出し処理の修正

app.listen関数の最後で引数に払い出されたurlを指定してsendNgrokUrl関数を呼び出す。この関数はWebAPI (A2)を使って払い出されたurlをGoogleスプレッドシートへ書き込む。

問題点


修正したプログラムは機能した。すなわち、発話PUSHプログラムを起動するとngrokで払い出されたurlがGoogleスプレッドシートに書き込まれた。
ところが問題が発生した。それは
$ forever start example.js
を使って発話PUSHプログラムを起動したときである。
初回は無事ngrokからurlの払い出しを受けるものの、強制的にexample.jsをkillすると、確かにexample.jsは再び起動されるが、そのときngrok.connectでエラーが発生する。
Error: connect ECONNREFUSED 127.0.0.1:4040
そのため、Googleスプレッドシートには
undefined/google-home-notifier
が設定され、利用できなくなる。

これは、経験的にngrokのダッシュボードからURLを削除してから再び発話PUSHサーバを起動するとうまくいくことがわかっている。おそらく払い出されているurlを解放するまでに多少のタイムラグあるためだと考えられる。

ngrok管理画面


ngrokには管理画面があり、ここから現在払い出されているurlを確認できる。
図2.ngrok管理画面(ダッシュボード)

この画面を出すには、 ログインして左のメニューから[Status]を選ぶ。すると、現在利用可能な払い出しurlを確認することができる。

発話PUSHサーバ(example.js)の自動起動


システム起動時に発話PUSHサーバ(example.js)を自動起動するには、/etc/rc.localに下記の1行を追加すればよい。
forever start /home/mtanaka/pill-reminder/example.js
リスト7./etc/rc.local

foreverコマンドはNode.jsで作成されたアプリをデーモン化するコマンドである。/etc/rc.localで起動したforeverコマンドは管理者権限で実行されることになるので、foreverコマンドでログの確認をする際は、管理者権限でコマンドを実行しなければならない。

mtanaka@semi2014vpn:~$ sudo su
root@semi2014vpn:/home/mtanaka# forever list
info:    Forever processes running
data:        uid  command             script                                 forever pid  id logfile                 uptime
data:    [0] X31a /usr/local/bin/node /home/mtanaka/pill-reminder/example.js 1364    1398    /root/.forever/X31a.log 72:6:20:38.561999999918044

SSHのngrokによるトンネル化


遠隔地からベアボーンサーバを管理するため、ngrokを使ってsshに外部からアクセスできるようにする。そのために、/etc/rc.localに下記の一行を追加する。
su - mtanaka -c '/home/mtanaka/ngrok tcp 22'
リスト8.ngrokによるsshのトンネリング

ngrokのダッシュボードでURLが確認できるので、それを使ってTeratermで接続すればよい。

0 件のコメント:

コメントを投稿