再帰呼び出しになるかどうか

次のエラーで少しハマったので調べました。
Uncaught (in promise) RangeError: Maximum call stack size exceeded

どうやら再帰呼び出しによるスタックオーバーフローのようです。
実行していたコードは以下の通り。

let drawPattern = true;
function messageDraw(player_name) {
  document.getElementById("log_name").textContent = "Name : " + player_name
  if (drawPattern) {
    document.getElementById("message").textContent = "( ・ω・)";
  } else {
    document.getElementById("message").textContent = " (・ω・ )";
  }
  drawPattern = !drawPattern;
  setTimeout(messageDraw(player_name), 500);
}

なるほど、messageDrawの呼び出しが問題のようです。
このコードを次のように修正するとスタックオーバーフローは発生しなくなりました。

let drawPattern = true;
let player_name = "hoge"
function messageDraw() {
  document.getElementById("log_name").textContent = "Name : " + player_name
  if (drawPattern) {
    document.getElementById("message").textContent = "( ・ω・)";
  } else {
    document.getElementById("message").textContent = " (・ω・ )";
  }
  drawPattern = !drawPattern;
  setTimeout(messageDraw, 500);
}

ふむふむ、なぜ?
関数を引数に与える形式で記述すると再帰呼び出しでは無くなるようですが、理屈が分からない。 おそらくスコープ内の変数が無いので、setTimeout()タイムアウトmessageDraw()を呼び出した時点で前のスタックが解消されるのだと想像します。

Javascriptの同期処理についてメモっておく

サーバからユーザ情報を取得して表示を更新する処理を作るに当たって、httpの完了を待つ処理が必要でした。
#3 クライアントでプレイヤー情報を参照する · bluemon0919/lobby@8f4b0f2 · GitHub

こちらの記事を参考に作りました。

var u1_name;  // これを更新する
var u2_name;

function getUserInformation() {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', "/players/" + u2, true);
        xhr.responseType = 'json';
        xhr.onload = function() {
            var res_data = this.response;
            u1_name = res_data[u1];
            u2_name = res_data[u2];
            resolve();  // 処理の終わりを通知する
        };
        xhr.send();
    });
    return p;
}

function init() {
    ...
    getUserInformation().then(() => update());  // getUserInformationが終わったらupdateを行う
}

上の例では使っていませんが、 resolveメソッドに結果を渡し、それをthenメソッドの中で受け取ることができます。
次の例では文字列"done"を渡してconsole.logで出力します。

function getUserInformation() {
    const p = new Promise((resolve, reject) => {
        ...
        resolve("done");
    }
}

function init() {
    ...
    getUserInformation().then((result) => {
        console.log(result);
        update();
    });  // getUserInformationが終わったらupdateを行う
}

dockerの基本コマンド

はじめに

dockerを勉強したかったので、tutorialから手をつけてみました。

#LearnDocker | Docker

Dockerイメージの理解を目指すチュートリアル - Qiita

出てきたコマンドを忘れそうだったので、 備忘録的にdockerの基本コマンドをまとめてみた記事です。

docker pull

docker hubからイメージやリポジトリを取得するためのコマンド。
name spaceやtagは省略でき、name spaceを省略した場合はdocker公式のname space=libraryになり、 tagを省略した場合はtag=latestになる。

docker pull {<name spece>"/"}<repository>{":"<tag>}

$ docker pull hello-world
or
$ docker pull library/hello-world:latest

docker images

docker イメージ一覧を表示する。

$ docker images

docker inspect

dockerイメージの詳細を表示するコマンド。
name spaceやtagは省略でき、name spaceを省略した場合はdocker公式のname space=libraryになり、 tagを省略した場合はtag=latestになる。

docker inspect {<name spece>}<repository>{":"<tag>}

$ docker inspect hello-world:latest

docker rmi

dockerイメージを削除するコマンド。

docker rmi <repository>

$ docker rmi hello-world

docker run

dockerイメージからdockerコンテナを起動するコマンド。

docker run <ContainerID>|<ContainerName>{":"<tag>}

$ docker run hello-world

docker ps

dockerコンテナの状態を確認するコマンド。
option指定はこちらを参照。

docker ps [option]

$ docker ps -a

docker rm

dockerコンテナを削除するコマンド。
option指定はこちらを参照。

docker rm [option] <Container ID>|<Container Name>

$ docker rm hello-world

docker build

dockerイメージをビルドするコマンド。 パスに指定したディレクトリにあるDockerfileとコンテキストを利用して新しいDockerイメージを構築する。 コンテキストはDockerイメージ構築に必要なファイル全般のことを指す。
-tContainerName:tag形式指定のオプションで、ビルド時にタグを追加することもできる。

docker build -t <ContainerName>{":"<tag>}

$ docker build -t myhello .
or
$ docker build -t myhello:v2 .

終わり

もう少し勉強したらdockerのライフサイクルをまとめてみようと思います。

docker for macでttyにアクセスできない

macの場合dockerがVM上で動作しているので、/var/lib/docker/に直接アクセスできない。

$ docker inspect hello-world:latest
...
"GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/merged",
                "UpperDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/diff",
                "WorkDir": "/var/lib/docker/overlay2/a561cd4994ad3cc18999f56d565a9c2d703f3bcfb17bb30374874dd6da80e88b/work"
            },
            "Name": "overlay2"
},

次のコマンドでttyにアクセスできるらしいのだが、ファイルが無いというエラーが出てしまった。

$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty
...
No such file or directory

確かにttyというファイルは無い。バージョンによる違いだろうか。

$ ls -l ~/Library/Containers/com.docker.docker/Data/vms/0
total 144
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000002.000005f4
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000002.00001003
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000003.000005f5
srwxr-xr-x  1 kota  staff      0  1  4 10:00 00000003.00000948
srwxr-xr-x  1 kota  staff      0  1  4 10:00 connect
-rw-r--r--  1 kota  staff  65536  1  4 10:00 console-ring
drwxr-xr-x@ 3 kota  staff     96  1  4 10:00 data
lrwxr-xr-x  1 kota  staff     17  1  4 10:00 guest.000005f5 -> 00000003.000005f5
lrwxr-xr-x  1 kota  staff     17  1  4 10:00 guest.00000948 -> 00000003.00000948
-rw-r--r--  1 kota  staff   2375  1  4 10:00 hyperkit.json
-rw-r--r--  1 kota  staff      4  1  4 10:00 hyperkit.pid
drwxr-xr-x  2 kota  staff     64  1  4 10:00 log

使っているバージョンは次の通り。何か情報あったら教えてください。

$ docker version
Client: Docker Engine - Community
 Cloud integration: 1.0.4
 Version:           20.10.0
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        7287ab3
 Built:             Tue Dec  8 18:55:43 2020
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.0
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       eeddea2
  Built:            Tue Dec  8 18:58:04 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

今年ハマったものを振り返ろう

お題「#買って良かった2020
ですが、一部買ったわけでは無いもの(サービス)を含みます。
それでは今年ハマったものベスト5をざくっと紹介します。

1位:Ghost of Tsushima

過去購入したゲームの中で一番ハマったゲームかもしれません。
とにかく美しい対馬の自然を表現した舞台を冒険できるだけでも買う価値ありだと思います。
周りにゲームをする人がいないので感動を共有できないのが残念。 PS4のトロフィーで初めて100%を達成しました。

www.playstation.com

2位:HHKBキーボード

左右特に右手が慢性的に腱鞘炎なので、とにかくタッチの軽いキーボードが欲しくて購入。 会社で使っているノートPCのキーボードが超絶打ち難いこともあって重宝しています。 日本語配列の白を使っていますが、黒もかっこよくていいですよね。 家と会社とそれぞれ買えればいいですが、ちょっとそこまでは出せないので週末になると会社から持って帰ってます。

www.pfu.fujitsu.com

3位:YAMAP

登山系SNSのYAMAPは今年の10月くらいに知ってから使っています。
スマホGPSやカメラを使って登山の記録をつけたり、登山計画を作ったり保険に入ったりと、これ一つで大抵のことができるのではないでしょうか。 登山以外にハイキングや散歩に使っている人もいて、私もその一人です。 本格的な登山をしたことが無いですが、このサービスを知ってから近所の山にハイキングに何度か行くようになりました。 記録するだけでも楽しいですよ。

yamap.com

4位:ソーダストリーム

今年は家で晩酌する機会が増えましたね。
私は普段から家でハイボール飲むんですが、今まではスーパーに行っては炭酸水を買う生活をしていたんです。でもソーダストリームさえあればもう重たい炭酸水を運ばなくてもいいんですよ! なにより、夜中に炭酸水がなくなってガッカリすることがなくなったのが精神的にも良かったと思います。

www.sodastream.jp

5位:YouTube

今までYouTubeってあまり使ってなかったのですが、テレワーク中の休憩時間によく見るようになりました。 その分テレビ番組を見る機会が減ったかな。あと、勉強会やイベントもオンライン化してYouTubeライブが使われたりするので、そういった方面でも利用することが増えています。

Go Modulesについて

先日、Go Modulesの操作ミスが重なってリポジトリを1つ失うことになりました。悲しいことが2度と起きないようにGo Modulesの勉強をしました。
この記事はThe Go Blog using go module からピックアップした内容です。

モジュールを作成する

モジュール名はgo mod initコマンドを使って付けます。このコマンドを実行するとgo.modファイルが生成されます。

$ go mod init <module name> 

githubリポジトリを持つ場合は次のような命名規則になると思います。

<module name> = github.com/<account name>/<repository name>

現在自分のモジュールが依存しているパッケージのバージョンはgo.modで確認できます。

go.modを読む

$ cat go.mod
module example.com/hello

go 1.15

require (
    golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c  // indirect
    rsc.io/quote v1.5.2
    rsc.io/sampler v1.3.0  // indirect
)

rsc.io/quote v1.5.2
これは自分のモジュールが依存しているモジュールです。
v1.5.2はモジュールのバージョンを表しています。セマンティックバージョニングのルールに基づいてバージョンをつけます。

rsc.io/sampler v1.3.0 // indirect
indirectコメントが付いている場合は、依存関係にあるモジュールから間接的に依存しているモジュールです。

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
v0.0.0-yyyymmddhhmmss-abcdefabcdefのようになっているのは擬似バージョンです。
コミットタグの付いていないバージョンを使う場合、このようなバージョン構文になります。

また、go.modをみなくてもgo list -mコマンドで依存関係を確認することができます。
ただし、indirectコメントは見ることができません。

$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0

依存パッケージのバージョン確認とバージョン変更

依存しているパッケージ(rsc.io/sampler)のバージョン一覧を知りたい場合は、次のコマンドで確認できます。

$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99

依存しているパッケージのバージョンを変えたい場合は次のコマンドで変更します。 こうすることで、go.modのバージョンを書き換えることができます。

$ go get rsc.io/sampler@v1.3.1
go: downloading rsc.io/sampler v1.3.1

$ cat go.mod
module example.com/hello

go 1.15

require (
    golang.org/x/text v0.3.4 // indirect
    rsc.io/quote v1.5.2
    rsc.io/sampler v1.3.1 // indirect
)

依存パッケージのメジャーバージョンアップ

メジャーバージョンをあげる場合はv2, v3 ... のように異なるパスを割り当てます。

rsc.io/quote/v3 v3.1.0

メジャーバージョンアップするとv0, v1とは別のモジュールとして扱われますので、依存しているパッケージをメジャーバージョンアップするには、必要なパッケージをインポートすればOKです。

package hello

import "rsc.io/quote/v3"

依存関係のクリーンナップ

go testなどのビルドコマンドは不要になったパッケージ情報をロードしないので、使用していないパッケージがgo.modに残り続けてしまいます。
クリーンナップするには明示的にgo mod tidyを実行する必要があります。

$ go mod tidy

モジュールを公開する

git tagでタグをつけます。
最初に紹介した通り、セマンティックバージョニングのルールに基づいたバージョンです。
タグをつける前にはgo mod tidyとテストの成功を確認しておくことを忘れずに。

$ go mod tidy
$ go test ./...
ok      example.com/hello       0.015s
$ git add go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: changes for v0.1.0"
$ git tag v0.1.0
$ git push origin v0.1.0

モジュールをメジャーバージョンアップする

v1→v2にバージョンアップする場合、v2ディレクトリを作成して必要なファイルをv2フォルダにコピーします。 go.modも同じくコピーし、モジュールパスにサフィックスを追加します。 ※v2以降はv0, v1と別のモジュールとして扱われることに注意。

$ mkdir v2
$ cp *.go v2/
$ cp go.mod v2/go.mod
$ go mod edit -module <module name/v2> v2/go.mod

Goでゲームのロビー機能のようなモノを作る〜宿題の提出1

はじめに

Go2 Advent Calendar 2020 9日目の記事の宿題の提出です。

この記事では、ゲームのマッチングを行うロビー機能をGoで作って紹介しました。
記事の時点ではWebSocketを管理するhubをpoolに戻す処理が未対応で宿題となっていました。
今回はこの部分を作ってきました。

github.com/bluemon0919/lobby

サーバに/huboutというハンドラを追加しています。 クライアントがアクセスするとhubからプレイヤー情報を削除します。

func main() {
    flag.Parse()
    http.HandleFunc("/", serveFront)
    http.HandleFunc("/lobby", serveLobby)
    http.HandleFunc("/play", servePlay)
    http.HandleFunc("/login", serveLoginHandler)
    http.HandleFunc("/ws", serveWebsocket)
    http.HandleFunc("/hubout", huboutHandler) // 追加
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

huboutHandler()はCookieから自身のセッションIDを取得し、セッションIDをHubManagerから削除します。

func huboutHandler(w http.ResponseWriter, r *http.Request) {
    manager := sessions.NewManager()
    session, err := manager.Get(r, cookieName)
    if err != nil {
        fmt.Println(err)
        return
    }
    hubManger := websocket.NewManager()
    hubManger.Destroy(session.ID) // WebSocketのHubManagerから削除する
}

HubManagerが管理しているデータは次の通り。
Destroy()はこれらのデータから対象となるキーを削除していきます。

type Manager struct {
    database map[string]*Hub
    pool     []*Hub
    count    map[*Hub]int
    users    map[*Hub][]string
}

プレイヤーを削除するごとにm.countをデクリメントすると、クロスタイミングでhubを横取りされる可能性があるので プレイヤー全員を削除したタイミングでm.countも削除するようにしています。 最後に、使っていたhubをpoolに戻します。これで次のプレイヤーが新たにhubを使えるようになりました。

func (m *Manager) Destroy(key string) {
    if hub, exist := m.database[key]; exist {
        delete(m.database, key)
        users := m.Users(hub)
        for u := 0; u < len(users); u++ {
            if users[u] == key {
                m.users[hub] = remove(m.users[hub], u)
                users = m.users[hub]
            }
        }
        if 0 == len(users) {
            // ペアが解放されたらhubをpoolに戻す
            delete(m.count, hub)
            m.pool = append(m.pool, hub)
        }
    }
}