DRYな備忘録

Don't Repeat Yourself.

atom command not found

Problem

# MacOS
% atom .
zsh: command not found: atom
% which atom
atom not found

Anything can’t exist without Atom

Research

Solution

% which atom
atom not found
% /Applications/Atom.app/Contents/Resources/app/atom.sh
# and close the window just opened
% which atom
/usr/local/bin/atom
% atom .

It works.

【Go言語】意地でもsliceをswitch-caseで比較したい

invalid case []byte literal in switch (can only compare slice foo to nil)

switch response[:4] {
case []byte{0,0,0,0}, []byte{1,0,0,0}, []byte{2,0,0,0}:
    fmt.Println("do something")
}
// これ動かないやつ

可変長であるsliceはnilとの比較だけが許されているようだ。固定長ならどうか?

やったこと

  1. 固定長arrayをつくる
  2. sliceとしてcopyにかける
  3. 固定長arrayと比較する
var header [4]byte
copy(header[:], response[:4])
switch header {
case [4]byte{0,0,0,0}, [4]byte{1,0,0,0,}, [4]byte{2,0,0,0}:
    fmt.Println("do something")
}

The Go Playground

DRYな備忘録として

EC2インスタンスにsshが接続できない原因のまとめ

目次(随時追加)

  1. InternetGatewayがVPCに未追加
  2. Pemファイルのパーミッションが不適切
  3. sshを試みるUserが違う

当該SubnetのRouteTableに当該InternetGatewayが追加されていない: Operation timed out

問題

  • sshコマンドを打ってもOperationTimeoutになる
    • TCP的に到達していない感じ

原因

解決

  1. EC2インスタンス一覧
  2. 当該インスタンスSubnet IDをメモる
  3. VPC→Subnets→当該SubnetのRoute TableのリンクをクリックしてRoute Table設定へ行く
  4. 当該RouteTableのRoutesでEdit
  5. (便宜上とりあえず)Destination0.0.0.0/0として当該VPCにattachされているInternetGatewayを追加

ダウンロードしたKeyPairファイル(.pem)のアクセス権限: Permission denied (publickey).

問題

  • sshがネットワーク的に到達しているが以下のようなエラーが出る
% ssh -i /tmp/my-project.pem ec2-user@X.X.X.X
Permissions 0644 for '/tmp/my-project.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/tmp/my-project.pem": bad permissions
Permission denied (publickey).

原因

  • 最後の一文だけ見て焦るけど、おちついて全文読みましょう
  • pemファイルのファイルアクセス権限の問題なので、chmodの話

解決

% chmod 600 /tmp/my-project.pem

ローカルのデフォルトユーザでsshを試みている: Permission denied (publickey).

問題

Host my-test
    HostName 123.123.123.123
    IdentityFile ~/.ssh/tmp/my-test.pem

として、sshを試みるも、

% ssh my-test
Permission denied (publickey).

となる。

原因

  • 自動生成されるidentityは、ec2-user名義のpemであるので、Userを指定する必要がある。

解決

% ssh ec2-user@my-test

あるいは、~/.ssh/configにおいて、

Host my-test
    HostName 123.123.123.123
+    User ec2-user
    IdentityFile ~/.ssh/tmp/my-test.pem

とする。

雑感

  • 何事も、慣れだ

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

(メモ)Go言語のinvalid recursive typeエラー、再帰データ型の実装とイテレーションについて

jsonでいうとこんな感じの

{
    "id": 48,
    "name": "foo",
    "entry": {
        "id": 36,
        "name": "bar",
        "entry": {
            "id": 24,
            "name": "baz",
            "entry": {
                "id": 12,
                "name": "qux"
            }
        }
    }
}

問題

コンパイル時のinvalid recursive typeエラーは、ポインタ型にすれば解決 invalid recursive type in a struct in go - Stack Overflow

実装

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

const data = `{
  "id": 48,
  "name": "foo",
  "entry": {
      "id": 36,
      "name": "bar",
      "entry": {
          "id": 24,
          "name": "baz",
          "entry": {
              "id": 12,
              "name": "qux"
          }
      }
  }
}`

type Entry struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Child *Entry `json:"entry"`
}

func main() {

    root := new(Entry)
    if err := json.NewDecoder(strings.NewReader(data)).Decode(root); err != nil {
        panic(err)
    }

    for entry := root; entry.Child != nil; entry = entry.Child {
        fmt.Printf("%s has a child: %+v\n", entry.Name, entry.Child)
    }
}

https://play.golang.org/p/vBNfDuhLHB

参考

雑感

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

(メモ)docker/machineのテストが落ちる問題

github.com

bash-4.4$ DRIVER=virtualbox make test-integration test/integration/core/core-commands.bats
test/integration/run-bats.sh test/integration/core/core-commands.bats
INFO: Run the tests with B2D_CACHE=1 to avoid downloading the boot2docker iso each time.
=> test/integration/core/core-commands.bats
 ✓ virtualbox: machine should not exist
 ✓ virtualbox: appears with ls
 ✓ virtualbox: has status 'started' appearing in ls
 ✓ virtualbox: create with same name again fails
 ✓ virtualbox: run busybox container
 ✓ virtualbox: url
 ✓ virtualbox: ip
 ✓ virtualbox: ssh
 ✓ virtualbox: version
 ✓ virtualbox: docker commands with the socket should work
 ✓ virtualbox: stop
 ✓ virtualbox: machine should show stopped after stop
 ✓ virtualbox: url should show an error when machine is stopped
 ✓ virtualbox: env should show an error when machine is stopped
 ✓ virtualbox: version should show an error when machine is stopped
 ✗ virtualbox: machine should not allow upgrade when stopped
   (in test file test/integration/core/core-commands.bats, line 109)
     `[[ "$status" -eq 1 ]]` failed
   Starting machine so machine can be upgraded... Starting "bats-virtualbox-test-shared-1498198301"... (bats-virtualbox-test-shared-1498198301) Check network to re-create if needed... (bats-virtualbox-test-shared-1498198301) Waiting for an IP... Machine "bats-virtualbox-test-shared-1498198301" was started. Waiting for SSH to be available... Detecting the provisioner... Waiting for SSH to be available... Detecting the provisioner... Upgrading docker... Stopping machine to do the upgrade... Upgrading machine "bats-virtualbox-test-shared-1498198301"... Copying /tmp/machine-bats-test-virtualbox/cache/boot2docker.iso to /tmp/machine-bats-test-virtualbox/machines/bats-virtualbox-test-shared-1498198301/boot2docker.iso... Starting machine back up... (bats-virtualbox-test-shared-1498198301) Check network to re-create if needed... (bats-virtualbox-test-shared-1498198301) Waiting for an IP... Restarting docker...
 ✗ virtualbox: start
   (in test file test/integration/core/core-commands.bats, line 115)
     `[ "$status" -eq 0  ]` failed
   Starting "bats-virtualbox-test-shared-1498198301"... Machine "bats-virtualbox-test-shared-1498198301" is already running.
 ✓ virtualbox: machine should show running after start
 ✓ virtualbox: kill
 ✓ virtualbox: machine should show stopped after kill
 ✓ virtualbox: restart
 ✓ virtualbox: machine should show running after restart
 ✓ virtualbox: status

23 tests, 2 failures

make: *** [test-integration] Error 1
bash-4.4$

Code Reading

まず、(in test file test/integration/core/core-commands.bats, line 109) とあるので、その行へ行く

@test "$DRIVER: machine should not allow upgrade when stopped" {
  run machine upgrade $NAME
  echo ${output}
  [[ "$status" -eq 1 ]]
}

run machine upgradeがstatus 1で終わるべきだが、status 0で成功している。

次に、run machine upgradeの内容としてcommands/upgrade.goを見る

package commands

import "github.com/docker/machine/libmachine"

func cmdUpgrade(c CommandLine, api libmachine.API) error {
    return runAction("upgrade", c, api)
}

commands/commands.go

runAction抜粋

   if errs := runActionForeachMachine(actionName, hosts); len(errs) > 0 {
        return consolidateErrs(errs)
    }

runActionForeachMachine抜粋

   for _, machine := range machines {
        numConcurrentActions++
        go machineCommand(actionName, machine, errorChan)
    }

machineCommand

// machineCommand maps the command name to the corresponding machine command.
// We run commands concurrently and communicate back an error if there was one.
func machineCommand(actionName string, host *host.Host, errorChan chan<- error) {
    // TODO: These actions should have their own type.
    commands := map[string](func() error){
        "configureAuth": host.ConfigureAuth,
        "start":         host.Start,
        "stop":          host.Stop,
        "restart":       host.Restart,
        "kill":          host.Kill,
        "upgrade":       host.Upgrade,
        "ip":            printIP(host),
        "provision":     host.Provision,
    }

    log.Debugf("command=%s machine=%s", actionName, host.Name)

    errorChan <- commands[actionName]()
}

host.Upgradeが呼ばれている

libmachine/host/host.go

Upgrade冒頭抜粋

func (h *Host) Upgrade() error {
    machineState, err := h.Driver.GetState()
    if err != nil {
        return err
    }

    if machineState != state.Running {
        log.Info("Starting machine so machine can be upgraded...")
        if err := h.Start(); err != nil {
            return err
        }
    }

テストのエラー文言にもあったように、明らかにUpgradeするためにStartを試みている。この実装をした日時と最後にテストが編集された日時が欲しい。blameしてみる。

f:id:otiai10:20170623163828p:plain

このコミットは、Start machine if needed for upgrade · docker/machine@e55dfb2 · GitHub

2017/02/23。対して、test/integration/core/core-commands.batsは、History for test/integration/core/core-commands.bats - docker/machine · GitHub この時点で2016/01/08なので、明らかにテストのほうが追従していない。

試しに、host.Upgradeの中で、machineが動いて無くてもStartしないように変更を加えると、

func (h *Host) Upgrade() error {
-  machineState, err := h.Driver.GetState()
-  if err != nil {
-      return err
-  }
-
-  if machineState != state.Running {
-      log.Info("Starting machine so machine can be upgraded...")
-      if err := h.Start(); err != nil {
-          return err
-      }
-  }

    provisioner, err := provision.DetectProvisioner(h.Driver)
    if err != nil {
        return err
    }

testそのものは

bash-4.4$ DRIVER=virtualbox make test-integration test/integration/core/core-commands.bats
test/integration/run-bats.sh test/integration/core/core-commands.bats
INFO: Run the tests with B2D_CACHE=1 to avoid downloading the boot2docker iso each time.
=> test/integration/core/core-commands.bats
 ✓ virtualbox: machine should not exist
 ✓ virtualbox: appears with ls
 ✓ virtualbox: has status 'started' appearing in ls
 ✓ virtualbox: create with same name again fails
 ✓ virtualbox: run busybox container
 ✓ virtualbox: url
 ✓ virtualbox: ip
 ✓ virtualbox: ssh
 ✓ virtualbox: version
 ✓ virtualbox: docker commands with the socket should work
 ✓ virtualbox: stop
 ✓ virtualbox: machine should show stopped after stop
 ✓ virtualbox: url should show an error when machine is stopped
 ✓ virtualbox: env should show an error when machine is stopped
 ✓ virtualbox: version should show an error when machine is stopped
 ✓ virtualbox: machine should not allow upgrade when stopped
 ✓ virtualbox: start
 ✓ virtualbox: machine should show running after start
 ✓ virtualbox: kill
 ✓ virtualbox: machine should show stopped after kill
 ✓ virtualbox: restart
 ✓ virtualbox: machine should show running after restart
 ✓ virtualbox: status

23 tests, 0 failures

make: Nothing to be done for `test/integration/core/core-commands.bats`.
bash-4.4$

通る。

結論

以下のどちらか

  1. host.Upgradeの仕様を戻す
  2. core-commands.batsのシナリオをちゃんと更新する

まあ2かな。

DRY


追記 2017/06/23 17:40

issue立てた

github.com

追記 2017/08/22

pull-req送って、7月にマージされてた

github.com

Firebase Cloud Messaging(FCM)でより簡単にWebブラウザにPush通知を送るサンプル

前回、下記のエントリでProgressive Web Appにおけるそこそこ生のWeb-Push-Protocolについておおまかな仕組みを触れました

otiai10.hatenablog.com

ので、今回はFirebase Cloud Messagingを使って、もっと手軽にWebブラウザにPush通知を送るのをやってみた備忘録です。

公式ドキュメント

必要なもの

  • GCM Sender ID
    • ブラウザ(デバイス)側で、Subscriptionを発行するときに、このアプリケーションの所属を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPublicKeyをFirebaseがラップしている要素だと思えばよい
  • FCM Server Key
    • 自前サーバ側で、FCMを叩くときに、アプリケーションの所属と妥当性を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPrivateKeyをFirebaseがラップしている要素だと思えばよい

どっちもFirebase consoleで手に入る。

  1. Firebase consoleに行き、[プロジェクトを追加]する(図)
  2. [ウェブアプリにFirebaseを追加]を選択(3つ並んだ◯の右の赤いやつ)
  3. messagingSenderIdというのがあるので、これがGCM_SENDER_IDです
  4. つづいて、左上、[Overview]の隣の設定アイコンから[プロジェクトの設定]を選択
  5. 上部のタブのうち[クラウドメッセージング]を選択 6.サーバーキーというのがあるので、これがFCM_SERVER_KEYです

f:id:otiai10:20170622010233p:plain

やること

  • ブラウザのタブで動くmain.js
    • firebaseアプリケーションの初期化
    • messagingモジュールの初期化
      • 自動で/firebase-messaging-sw.jsをServiceWorkerとしてregisterしようとする
    • Notificationの許可と、DeviceToken取得できたら自前サーバにそれを送る
  • バックグラウンドで動き続けるfirebase-messaging-sw.js
    • ブラウザタブがactiveでないときにPush通知が来たときのハンドラを設定できる
  • 自前のサーバserver.js
    • ブラウザから送りつけられてきた各ユーザのDeviceTokenの保存
    • 何らかのきっかけでFCMのエンドポイントを叩く
      • payloadのtoというフィールドでPush通知を投げたい相手のDeviceTokenを指定する
    • もちろんjsである必要はない全く無い

うるせえ動くコード見せろ

ウッス

github.com

% git clone git@github.com:otiai10/firebase-messaging-sample.git
% cd firebase-messaging-sample
% GCM_SENDER_ID={上記の} \
FCM_SERVER_KEY={上記の(けっこう長い)} \
node server.js

こんな感じでうごく

f:id:otiai10:20170622022753p:plain

注意点・ハマったポイント

manifest.jsonのgcm_sender_idは"103953800507"

  • 問題: ブラウザのコンソールで「Messaging: Please change your web app manifest’s ‘gcm_sender_id’ value to ‘103953800507’ to use Firebase messaging. (messaging/incorrect-gcm-sender-id).」と叱られる
  • 原因: これは、実際にPush通知を送る主体はFirebaseのサーバなので、FirebaseのアプリケーションIDを登録する必要があるため
  • 解決: ハードコードで103953800507を指定する manifest.json

setBackgroundMessageHandlerで設定したハンドラが呼ばれてない

  • 問題: firebase-messaging-sw.jsにおいて、setBackgroundMessageHandlerを使ってハンドラを登録しているのにもかかわらず、ぜんぜん呼ばれてる気配が無い
  • 原因:
    • その1: ブラウザのタブがactiveな時は必ず、ブラウザタブのmain.jsにおけるmessaging.onMessageが呼ばれるので、ServiceWorkerには入らない
    • その2: これが上記で「癖がある」って言ったやつなんですけど、自前サーバ側でFirebase叩くときに、そのペイロード中にnotificationというフィールドのデータが入っていると、そのnotificationで指定したtitle,body,iconでNotificationが生成され、ServiceWorkerには入らない。ふしぎだね。
  • 解決: まずブラウザのタブを閉じる。そんで、サーバからのFCMへのリクエストのトップにnotificationというフィールドを含めない

雑感

  • 暗号化その他もろもろFirebaseがやってくれるので、やっぱり素でWeb-Push-Protocolに乗るより断然お手軽だった
  • 癖があるところとか、ほげほげKEYが多すぎて混乱するところとかはあるけども
  • 酒が弱くなってる。よいことだ。

DRYな備忘録として

Using Google App Engine

Using Google App Engine

webブラウザにPush通知送るサーバとjsのサンプル

このドキュメントは

以下の2つのドキュメントをよりプリミティブに理解するためのDRYな備忘録です。

  1. Adding Push Notifications to a Web App  |  Web  |  Google Developers
  2. The Web Push Protocol  |  Web  |  Google Developers

f:id:otiai10:20170619113329p:plain

背景

かつて サーバからブラウザにプッシュ通知を送りたい(非WebSocket、非ロングポーリング) - DRYな備忘録 これ書いたけど、改めてProgressive Web Appのドキュメント行ったらアホみたいに分かりづらくなってて不条理を感じたので書きます。

サーバからPush通知がブラウザに対して送れて、ブラウザのjsが閉じててもServiceWorkerが生きてるからNotificationが出る、っていうやつです。最近だとウェブのGmailとかFacebook Messanger若干うざいやつとかでよく見る気がします。

この公式ドキュメント、パッとサンプル落としてきてハイ動くでしょ!っていうタイプなのか最小限の要素を理解させつつ動かすタイプなのか、完全にどっち付かずのドキュメントになっていて、若干わかりにくいです。

今回は、WebPushProtocolにおける暗号化されたbodyを送る、というところを除き、クライアントのコードとサーバのコードを書いたので、そのログを備忘録として晒します。

概念の確認

サービスとしては、3つです

  1. ブラウザ
    • ネイティブアプリ開発における「デバイス」だと理解して問題ないです
    • ブラウザではふたつのことをします
      • ServiceWorkerを登録し、登録したServiceWorkerにプッシュをsubscribeさせ、PushSubscriptionモデルを取得します
        • これが、ApplePushNotificationにおけるDevice Token的なものです
      • ServiceWorker内部では、タブのJavaScriptが死んでもbackgroundで動き続けるイベントリスナーを登録します
        • pushが来たとき、というイベントリスナーもここで定義します
  2. 自前サーバ
    • ブラウザで登録・取得されたPushSubscriptionモデルのデータを、自作アプリケーションにおけるユーザなどと紐付けて保存しておく必要があります
    • 何らかのトリガーで(たとえばユーザによる他ユーザへの呼びかけなど)下記の「Pushサービス」に「このsubscription endpointへこの内容でプッシュ通知送ってちょんまげ」というリクエストをする役割があります
  3. Pushサービス
    • 各デバイスに対してPushを実際に送るサービスです
    • Web-Push-Protocolのインターフェースを満たすサービスである必要があります
      • Chromeではfcm.googleapis.com
      • Firefoxではupdates.push.services.mozilla.com
      • が、それぞれPushサービスとして採用されます
    • 僕らは自前サーバからこのエンドポイント叩くだけなのでその違いを意識する必要は無い ← ここだいじ!

うるせえ動くコード見せろ

ウッス

github.com

こんな感じに動きます

f:id:otiai10:20170619200505p:plain

実際の処理

  1. ブラウザに登録されるServiceWorker
    • ソースコード: service-worker.js
    • やること
      • 自分自身に何かイベントがあったらそれをハンドリングする
        • install, activate, push
    • 必要なもの
      • 無い。ぶっちゃけ登録されるServiceWorkerがやるべき責務は小さい。
  2. ウェブページのmain
    • ソースコード: main.js
    • やること
      • ServiceWorkerをブラウザにregisterする
      • そしたらregistration.pushManager.subscribe()というのを呼ぶ
      • 上記で得られたsubscriptionモデルを自作サーバへ送りつける
        • このsubscriptionというのが、Pushを提供するGCMサーバのエンドポイントと、このデバイス(ブラウザ)のDeviceToken的なものを持っている
    • 必要なもの
      • ApplicationServerPublicKeyが必要
        • サーバのみが知りうるApplicationServerPrivateKeyに対をなすもの
  3. Pushをトリガーする自前サーバ
    • ソースコード: server.js(もちろんjsで書く必要は無い)
    • やること
      • ウェブページのjsから送られてきたsubscriptionの保存
      • 任意のトリガーでそのsubscriptionへPushを送る
        • 厳密にはPushを"送る"のはgcmなりMozilaPushServiceなので、「Pushサービスが提供したエンドポイントを叩く」のほうが正確
        • また、Pushサービスを叩くときのリクエストはWeb-Push-Protocolで定められた方法で暗号化されている必要があるが、今回はこれはweb-pushライブラリにすべて任せている
    • 必要なもの
      • ApplicationServerPrivateKeyが必要

注意点・ハマったところ

ServiceWorkerの更新ができない

  • 問題: ページをリロードしても編集したServiceWorkerが適用されていない
  • 原因: ServiceWorkerのライフサイクルは「ブラウザ自体」と「ウェブページのドメイン」によって定められるのであって、「ウェブページのタブ(いわゆるいつものブラウザのjsランタイム)」ではないので、ページをリロードしても更新されない
  • 解決: getRegistraionからのunregisterを呼ぶ。あるいはchrome://serviceworker-internals/に行き該当ウェブページに登録されているServiceWorkerを[Unregister]ボタンで削除するなりする必要がある

Subscriptionの更新ができない

InvalidStateError: Registration failed - A subscription with a different applicationServerKey (or gcm_sender_id) already exists; to change the applicationServerKey, unsubscribe then resubscribe.
  • 問題: registration.pushManager.subscribe()を呼ぶと上記のようなエラーが出ることがある。
  • 原因: エラーメッセージでは「別のapplicationServerKeyを使ったsubscriptionがすでにあるので、applicationServerKeyを変えてunsubscribeしてからもっかいsubscribeしてね」と言っている。基本的にはApplicationServerPublicKeyApplicationServerPrivateKeyの組み合わせは、1つのプロジェクトについてただ1つだけなので、このようなエラーは起きないと思うが、開発中とかだと起きるかもしれませんねハイ。上記のサンプルでは、npmのpost-installで1度だけVAPIDを作るようにしてます
  • 解決: エラーメッセージにおとなしく従う

ApplicationServerKeyとは

InvalidAccessError: Failed to execute 'subscribe' on 'PushManager': The provided applicationServerKey is not valid.
Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
  • 問題: subscribeするときに、上記のようなエラーが出ることがある。どうやらapplicationServerKeyがよくないようだが、そもそもApplicationServerKeyって何なのよ。
  • 原因: 今回いちばんハマったところなんですが、ApplicationServerKeyっていうのは、WebPushProtocolの文脈でのただの言い方で、その他いろいろな文脈とは関係ない、ただの、定められた方法で生成されたPublicKeyとPrivateKeyのペアというだけです。ここにGCM_API_KEYとかFCM_SERVER_KEYとか突っ込んで困る、というケースが多そう。まあ僕もなんですけど。RSAとかも無関係、名前似てるけど。
  • 解決: ほんで、この「Keyのペア」は、今回はweb-pushライブラリのgenerateVAPIDKeysに全部まかせています。PushサーバとしてGCMやMozilaPushServiceを叩くことこそあれど、今回のサンプルはGCM, FCM, MPSを関知しない、Web-Push-Protocolを用いたアプリケーションの構成のサンプルという位置づけです。

雑感

DRYな備忘録として

Building Progressive Web Apps: Bringing the Power of Native to the Browser

Building Progressive Web Apps: Bringing the Power of Native to the Browser

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-