Sinatra + WEBrick + Comet で簡単なチャット

サンプルチャット

試しにということで最低限の骨組みだけのものを書いてみました。チャット初めて書きました。Thread::Queue も初めて使いました。

sonota88/sinatra-webrick-samplechat
https://github.com/sonota88/sinatra-webrick-samplechat

現時点( タグ=20190327 )では サーバ側が 38 行、 JavaScript が 109 行。

短いのでサーバ側だけ貼っておきます。

require 'sinatra'
require 'sinatra/reloader'

class ConnectionManager
  def initialize
    @map = {} # session id => queue
  end

  def broadcast(msg)
    @map.each_value { |queue| queue.enq(msg) }
  end

  def deq(session_id)
    unless @map.key?(session_id)
      @map[session_id] = Thread::Queue.new
    end

    @map[session_id].deq
  end
end

$conn_manager = ConnectionManager.new

get "/" do
  send_file "index.html"
end

post "/comet/open" do
  $conn_manager.deq(params[:sessionid])
end

post "/messages" do
  $conn_manager.broadcast(
    params[:sessionid] + ": " + params[:body]
  )
  "ok"
end

見ての通りですが、サンプルなので

  • エラーハンドリングは適当
  • タイムアウト、再接続などのハンドリングなし
  • 離脱・リロードで発生する幽霊接続の後始末なし
  • 永続化なし

経緯

  • プロトタイプ、自分だけ or チーム内の数人でしか使わないちょっとしたツールをシュッと作りたいという場合に、いつも Sinatra を使っている
    • べんり
  • その延長でサーバからの push が必要なものが作りたくなって WebSocket を使おうとした
  • WEBrick だとできないらしい
  • Thin とかを使うらしい
    • でもほんとにちょっとしたやつなんだけどな……
  • Sinatra(+WEBrick)だけでなんとかできない?
  • そういえば Comet(ロングポーリング)というのがありましたね

要件

上に書いたことと被りますが。 ここらへんが合わない場合はおとなしく WebSocket とかを使った方がよいはず。

  • 接続数
    • 多くないというか少ない。自分だけ or 数人程度
  • レイテンシも別に…
    • ゲームの画面を 60 FPSレンダリングしたいとかじゃない
    • ちょっともっさり程度でもいい。動けばいい
    • でも普通のポーリングよりは全然まし、体感的に違いが分かって嬉しい、という程度でOK
  • メンテナンス性が良いというか、メンテナンスフリーなのが望ましい
    • こういうごくごく小規模はツールは環境が変化して動かなくなることがツール自体を使わなくなる契機になりがち(けっこうばかにできない)なので、枯れてないものになるべく依存したくない……

実用するには

実用しようとするといろいろやらないといけないっぽくて、以下、ちょっと試してみたレベルで得た知見+適当な思いつきのメモ。ここらへんが膨らんできたらライブラリとかありものの利用を検討する。

  • @map への追加だけやっていて削除がないので保持する接続(@map の要素)が増え続ける
    • ブラウザのタブ開く・閉じる or リロード
    • 不要になったものは消さないといけない
      • Queue#num_waiting == 0 のもの
        • だけではダメかも。接続ごとに最終接続日時を持っておいてそれと合わせて判断?
      • キューに溜まり続けるパターンもありそう
        • これも適当な閾値と最終接続日時を合わせて判断?
  • タイムアウト・再接続
    • HTTP的にタイムアウトどう扱うのか詳しくない……
    • サーバ側 and/or クライアント側
      • クライアント側: タイムアウトしたら comet のループを止めてサーバにポーリングするループに入る。サーバが復活したらポーリング止めて comet のループを再開、みたいな?
    • アプリケーションの要件によっても変わる
  • enqueue が高頻度な場合
    • Thread::SizedQueue も検討(enqueue をブロックする)
    • 上記のサンプルだとキューにたくさん溜まっていても deq で 1個だけ取ってクライアント側にすぐ戻していて、接続がもったいない
      • 複数個 dequeue してまとめて返す
    • とはいえチャット程度なら心配する必要なさそう

関連

Scalatra 2.12.6 で auto reload

sinatra/reloader みたいなの。

sbt を起動して ~jetty:start でよかった。

$ sbt
(snip)
sbt:My Scalatra Web App> ~jetty:start

一応書いておくと sbt は 1.2.8。

参考

No aux-compile task in sbt console in scalatra project - Stack Overflow
https://stackoverflow.com/questions/28105107/no-aux-compile-task-in-sbt-console-in-scalatra-project

(追記 2019-04-13)

公式サイトに書いてあった(しかも「First steps」に)……。

$ sbt
~;jetty:stop;jetty:start

First steps - Scalatra
http://scalatra.org/getting-started/first-project.html

SPA メモ

SPAに関する雑なメモ。


  • 「ハンマーを持つ人にはすべてが釘に見える」になっていないか
  • 要件
  • 保守性
  • 学習コスト

2019-11-02 私たちはなぜ SPA で開発するのか / Why you choose SPA - Speaker Deck
https://speakerdeck.com/potato4d/why-you-choose-spa

  • UX要件とDX要件
    • DX要件によるSPA選定の特徴 / 必然性に乏しいことが多く、(略)

  • 「アプリケーション」なのに State がバラバラで苦しむ例

  • 速度と品質ではなく、「要件と想定する最大のアーキテクチャ規模」が重要

  • プロダクトのためためではなく、エゴを優先する「詐欺師」ではないか


2016-09-16 SPAと覚悟
https://www.slideshare.net/teppeis/spa-66093931

  • ページ遷移
  • SPAとはブラウザを超える体験を再実装する覚悟

  • SPAでどんな問題を解決したの?

  • SSRのUX(速度)も高まってる

  • 旧来のドキュメントベースの遷移モデルから脱却することをどこまで考えるか?


2016-04-05 CLIENT SIDE OF █████ FRESH! - Speaker Deck
https://speakerdeck.com/ahomu/client-side-of-fresh

  • PinP (Picture in Picture) が要件にあったから


2018-12-12 JavaScript/Vue.jsアプリのパフォーマンス改善 一休.comスマートフォンサイトを高速化するためにやったこと - ログミーTech
https://logmi.jp/tech/articles/320604

  • SPA にするより、MPA にして CDN でキャッシュを効かせる方が速いかも?

  • たとえば、画面遷移を速くするという目的であれば、SPAにするよりも、 複数のページがある普通のWebサイトにして、各ページをCDNでキャッシュしたほうがシンプルに作れる、 といった状況はあると思います。


2019-01-29 You probably don’t need a single-page application
https://journal.plausible.io/you-probably-dont-need-a-single-page-app

  • Core functionality is real-time (e.g Slack)
  • Rich UI interactions are core to the product (e.g Trello)
  • Lots of state shared between screens (e.g. Spotify)
  • ハイブリッドなアプローチ

2016-05-25 ssig33.com - なぜ SPA か
https://ssig33.com/text/%E3%81%AA%E3%81%9C%20SPA%20%E3%81%8B

顧客は SPA であることを望んでいるのか?そうではないです。技術者は SPA を作りたいのか?そうではないです。

ではなぜ SPA 的なものが出来てしまうかといえば、いちいち UI の遷移のために大量のデータをロードしているのは時間と資源の無駄だからです。


2021-03-19 スタートアップにおける言語とフレームワークの選択 / Why we chose Ruby on Rails as a startup - Speaker Deck
https://speakerdeck.com/autifyhq/why-we-chose-ruby-on-rails-as-a-startup


2021-08-12 なんでもSPAにするんじゃねぇ!という主張のその先 - console.lealog(); https://lealog.hateblo.jp/entry/2021/08/12/103111

Hydration / Marko


2019-02-18 その技術を使わない方がいい - 西尾泰和のScrapbox
https://scrapbox.io/nishio/%E3%81%9D%E3%81%AE%E6%8A%80%E8%A1%93%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84%E6%96%B9%E3%81%8C%E3%81%84%E3%81%84

※SPAとは直接関係ない

webpack --watch で差分ビルドの完了時に任意のコマンドを実行する

<他の記事の案内です>

<案内おわり>


「任意のコマンドを実行」としたけど、要するに完了にフックして通知がしたい。

単純な watch の実行:

$(npm bin)/webpack --watch --config webpack.config.js

これだけだと(差分)ビルド完了の検出がやりにくそうなので、 --info-verbosity verbose を追加する。

Setting info-verbosity to verbose will also message to console at the beginning and the end of incremental build. info-verbosity is set to info by default.

$(npm bin)/webpack --watch --config webpack.config.js \
  --info-verbosity verbose

開始時と完了時にこういうメッセージが出力される:

Compilation  starting…
Compilation  finished

あとは適当なラッパーを書いて、完了メッセージを検出してコマンドを実行すればOK。 Ubuntu だと notify-send コマンドで通知できる。 Webpack の設定でできるか調べるよりこっちの方が速かった……。まあでもこの方法なら webpack 以外でも使いまわせるし、いいよね、ということにしておく。

#!/usr/bin/env ruby

require "pty"

npm_bin = `npm bin`.chomp

watch_cmd = [
  "#{npm_bin}/webpack",
  "--watch",
  "--config webpack.config.js",
  "--color=false",
  "--info-verbosity verbose"
].join(" ")

PTY.spawn(watch_cmd) do |i, o|
  loop do
    line = i.gets
    print line
    if line.chomp == "Compilation  finished"
      timestamp = Time.now.strftime("%F %T")
      system "notify-send 'Compilation finished #{timestamp}'"
    end
  end
end

試しにやってはみたものの、今手元にあるものは規模が小さくてビルドがすぐ終わるので便利になったかよく分からない(やる前に気付こう)。


$(npm bin)/webpack --version
4.29.0

関連:

Vagrant + Ubuntu 18.04 + anyenv + Renv + グラフをSVG出力

ホストは Ubuntu 16.04(まだ 18.04 に移行してない……)。 VirtualBox のインストールについては割愛。

$ apt list --installed | grep -i virtualbox

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

unity-scope-virtualbox/xenial,xenial,now 0.1+13.10.20130723-0ubuntu1 all [インストール済み、自動]
virtualbox/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [インストール済み]
virtualbox-dkms/xenial-updates,xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 all [インストール済み]
virtualbox-qt/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [インストール済み、自動]
virtualbox-source/xenial-updates,xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 all [インストール済み、自動]

Vagrant は昔 apt でインストールしたものだが、 古かったので公式から取ってきてインストールし直した。

$ vagrant -v
Vagrant 1.8.1

$ sudo apt-get remove vagrant

$ sudo dpkg -i vagrant_2.1.5_x86_64.deb

$ vagrant -v
Vagrant 2.1.5

Vagrant 用に用意した適当なディレクトリに移動して

$ vagrant init bento/ubuntu-18.04

Vagrantfile を下記のように修正。 メモリはデフォルトの 1024 だと R のビルド時にスワップが発生したので 2048 にした。

  config.vm.provider "virtualbox" do |vb|
    # Display the VirtualBox GUI when booting the machine
    vb.gui = true

    # Customize the amount of memory on the VM:
    vb.memory = "2048"

    vb.customize [
      "modifyvm", :id,
      "--vram", "256",
      "--clipboard", "bidirectional"
    ]
  end

起動させる。

vagrant up

vagrant login: のプロンプトが出たら ユーザ vagrant, パスワード vagrant でログイン。

……でもいいが、キーボードが英語配列になるので vagrant ssh して作業した方が楽だったかも。


まず apt-get update して、次いでデスクトップ環境をインストールする。

sudo apt-get update
sudo apt-get install ubuntu-desktop

いっぱいインストールされるのでしばらく待つ(ubuntu-desktop のインストールが十数分)。

とりあえずベタに ubuntu-desktop をインストールしたが、 (軽量な方がいいとかの理由で)他のが良ければ適宜 xubuntu-desktop とか lubuntu-desktop とかにすると良さそう。

終わったらホスト側で

vagrant reload

以後はグラフィカルログインでログインする。 ユーザ、パスワードはさっきと同様に vagrant, vagrant

最初に出るセットアップ:

  • Livepatch … いったん無視する。右上の [Next] で次へ。
  • Help improve Ubuntu … お好みで。 [Next]
  • [Done]

Software Updater

[Install now] - キーボードレイアウトの設定: Japanese を選択したかったが、 いったん戻ってやり直そうとしたらなぜか選択できなくなって、 English (US) で進んでしまった。後で設定し直す。

Continue without installing GRUB? … インストールしなくても問題なかった

キーボードの設定

  • 左下の「Show Applications」から「Settings」を開く。
  • Region & Language
    • Manage Installed Languages
      • [Install]
      • [Install / Remove Languages...]
        • Japanese を選択して [Apply]
        • [Close]
    • Input Sources の [+]
      • Japanese を選択して [Add]
    • Japanese を一番上に移動させる

設定反映のため再起動。

# ホスト側で
vagrant reload

その他の設定

画面の自動ロックを無効化

Settings>Privacy>Screen Lock

R のインストール

apt でインストールされるものはバージョンが古いらしいが、 後で Renv でインストールする際の依存パッケージがだいたい入るので、 とりあえずインストールしておく。

r-baser-base-core どっちをインストールするのが適切なのか分からなかったが、 上記の記事を見ると r-base-core をインストールしているようなので、その通りにしてみる。

ターミナルは画面左上の「Activities」または左下の「Show Applications」で "terminal" と入力して出てきたものを使う。 または Alt+F2 から gnome-terminal で起動する。

sudo apt-get install r-base-core

これらがインストールされる:

The following NEW packages will be installed:
  autoconf automake autopoint autotools-dev build-essential bzip2-doc cdbs debhelper dh-autoreconf dh-strip-nondeterminism
  dh-translations dpkg-dev fakeroot g++ g++-7 gcc gcc-7 gfortran gfortran-7 gir1.2-harfbuzz-0.0 icu-devtools intltool jq
  libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libarchive-cpio-perl libasan4 libatomic1
  libblas-dev libblas3 libbz2-dev libcilkrts5 libfakeroot libfile-stripnondeterminism-perl libfile-which-perl libgcc-7-dev
  libgfortran-7-dev libgfortran4 libglib2.0-dev libglib2.0-dev-bin libgraphite2-dev libharfbuzz-dev libharfbuzz-gobject0
  libicu-dev libicu-le-hb-dev libicu-le-hb0 libiculx60 libitm1 libjpeg-dev libjpeg-turbo8-dev libjpeg8-dev libjq1
  liblapack-dev liblapack3 liblsan0 libltdl-dev liblzma-dev libmail-sendmail-perl libmpx2 libncurses5-dev libonig4
  libpcre16-3 libpcre3-dev libpcre32-3 libpcrecpp0v5 libpng-dev libpng-tools libquadmath0 libstdc++-7-dev
  libsys-hostname-long-perl libtcl8.6 libtk8.6 libtool libtsan0 libubsan0 m4 pkg-config po-debconf python3-distutils
  python3-lib2to3 python3-scour r-base-core r-base-dev r-cran-boot r-cran-class r-cran-cluster r-cran-codetools
  r-cran-foreign r-cran-kernsmooth r-cran-lattice r-cran-mass r-cran-matrix r-cran-mgcv r-cran-nlme r-cran-nnet
  r-cran-rpart r-cran-spatial r-cran-survival r-doc-html r-recommended scour

anyenv のインストール

どうせ Ruby とかも使うだろうからということで まず anyenv をインストールする。 手順はほぼ公式の README の通り。 (Ubuntu なので ~/.bashrc に追記する)

git clone https://github.com/riywo/anyenv ~/.anyenv

echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(anyenv init -)"' >> ~/.bashrc
exec $SHELL -l

Renv のインストール

$ anyenv install Renv

$ anyenv envs
Renv

$ exec $SHELL -l

R 3.5.1 のインストール

利用可能なバージョンを Renv install -l で確認して、 現時点で最新の 3.5.1 をインストールする。

$ Renv install 3.5.1
Downloading http://cran.r-project.org/src/base/R-3/R-3.5.1.tar.gz...
Installing R-3.5.1...

BUILD FAILED

Inspect or clean up the working tree at /tmp/R-build.20181016202219.8938
Results logged to /tmp/R-build.20181016202219.8938.log

Last 10 log lines:
checking whether wcstombs exists and is declared... yes
checking whether wctrans exists and is declared... yes
checking whether iswblank exists and is declared... yes
checking whether wctype exists and is declared... yes
checking whether iswctype exists and is declared... yes
checking for wctrans_t... yes
checking for mbstate_t... yes
checking for ICU... yes
checking for X... no
configure: error: --with-x=yes (default) and X11 headers/libs are not available

おそらく plot などでの描画で必要なので、 --with-x=yes のオプションはそのままにしておいて必要なパッケージの方をインストールする。

sudo apt-get install xorg-dev

これらがインストールされる:

The following NEW packages will be installed:
  libcapnp-0.6.1 libdmx-dev libdmx1 libdrm-dev libexpat1-dev libfontconfig1-dev libfontenc-dev libfreetype6-dev libfs-dev
  libfs6 libice-dev libmirclient-dev libmirclient9 libmircommon-dev libmircommon7 libmircookie-dev libmircookie2
  libmircore-dev libmircore1 libmirprotobuf3 libpciaccess-dev libpixman-1-dev libprotobuf-dev libprotobuf-lite10
  libpthread-stubs0-dev libsm-dev libx11-dev libx11-doc libxau-dev libxaw7-dev libxcb1-dev libxcomposite-dev libxcursor-dev
  libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxfont-dev libxft-dev libxi-dev libxinerama-dev libxkbcommon-dev
  libxkbfile-dev libxmu-dev libxmu-headers libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev
  libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86dga-dev libxxf86vm-dev mesa-common-dev mir-client-platform-mesa-dev
  x11proto-composite-dev x11proto-core-dev x11proto-damage-dev x11proto-dev x11proto-dri2-dev x11proto-fixes-dev
  x11proto-fonts-dev x11proto-gl-dev x11proto-input-dev x11proto-randr-dev x11proto-record-dev x11proto-render-dev
  x11proto-resource-dev x11proto-scrnsaver-dev x11proto-xext-dev x11proto-xf86bigfont-dev x11proto-xf86dga-dev
  x11proto-xf86vidmode-dev x11proto-xinerama-dev xorg-dev xorg-sgml-doctools xserver-xorg-dev xtrans-dev

再度ビルド。

$ Renv install 3.5.1
Downloading http://cran.r-project.org/src/base/R-3/R-3.5.1.tar.gz...
Installing R-3.5.1...

BUILD FAILED

Inspect or clean up the working tree at /tmp/R-build.20181016204050.19429
Results logged to /tmp/R-build.20181016204050.19429.log

Last 10 log lines:
checking for pcre/pcre.h... no
checking if PCRE version >= 8.20, < 10.0 and has UTF-8 support... yes
checking if PCRE version >= 8.32... yes
checking whether PCRE support suffices... yes
checking for pcre2-config... no
checking for curl-config... no
checking curl/curl.h usability... no
checking curl/curl.h presence... no
checking for curl/curl.h... no
configure: error: libcurl >= 7.22.0 library and headers are required with support for https
$ apt-cache search '^libcurl4.+-dev'
libcurl4-gnutls-dev - development files and documentation for libcurl (GnuTLS flavour)
libcurl4-nss-dev - development files and documentation for libcurl (NSS flavour)
libcurl4-openssl-dev - development files and documentation for libcurl (OpenSSL flavour)

この3つのうちどれが適切なのか分からなかったが、 上から順番に試そうということで libcurl4-gnutls-dev をインストールしたところ 先に進めたので、ひとまずこのまま進める。

(調べてないので分かりませんが、 内部で使っているライブラリが違うだけで libcurl の動作としてはどれでも同じという状況ではないかと想像)

$ sudo apt-get install libcurl4-gnutls-dev

$ Renv install 3.5.1
Downloading http://cran.r-project.org/src/base/R-3/R-3.5.1.tar.gz...
Installing R-3.5.1...
Installed R-3.5.1 to /home/vagrant/.anyenv/envs/Renv/versions/3.5.1

インストールできた。

グローバルに 3.5.1 を使うように設定。

$ Renv global 3.5.1

$ which R
/home/vagrant/.anyenv/envs/Renv/shims/R

$ R --version
R version 3.5.1 (2018-07-02) -- "Feather Spray"
Copyright (C) 2018 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)
(略)

試しに使ってみる。

$ R -q
> d = data.frame(
+   x = c(1, 2, 3)
+  ,y = c(12, 13, 14)
+ )
> plot(d$x, d$y)
> 

別ウィンドウでグラフが表示された。問題なさそう。

グラフをSVGで出力する

d = data.frame(
  x = c(1, 2, 3)
 ,y = c(12, 13, 14)
)

svg(filename="sample.svg")
plot(d$x, d$y)
dev.off()

のようにして SVG に出力したいが、今の状態で実行するとこうなる:

> svg(filename="sample.svg")
Warning messages:
1: In svg(filename = "sample.svg") :
  unable to load shared object '/home/vagrant/.anyenv/envs/Renv/versions/3.5.1/lib/R/library/grDevices/libs//cairo.so':
  /home/vagrant/.anyenv/envs/Renv/versions/3.5.1/lib/R/library/grDevices/libs//cairo.so: cannot open shared object file: No such file or directory
2: In svg(filename = "sample.svg") : failed to load cairo DLL
> 

これを解消するには configure のオプションに --with-cairo=yes を付けて ビルドし直さないといけないらしい。

まず libcairo2-dev だけインストールしてみる:

$ sudo apt-get install libcairo2-dev
(略)
The following NEW packages will be installed:
  libcairo-script-interpreter2 libcairo2-dev libxcb-render0-dev libxcb-shm0-dev
(略)

configure のオプションはどこで指定すればいいか。 Renv install の時にビルド(configure 含む)しているようなので、適当に grep してみると:

$ pwd
/home/vagrant/.anyenv/envs/Renv

$ grep -r ./configure .
(略)
./plugins/R-build/README.md:* `CONFIGURE_OPTS` lets you pass additional options to `./configure`.
./plugins/R-build/bin/R-build:  { ./configure --prefix="$PREFIX_PATH" $CONFIGURE_OPTS
(略)

CONFIGURE_OPTS を使えば良いらしいと分かる。 R-build の readme もさらっと眺めて、 一度アンインストールして

Renv uninstall 3.5.1

再度 Renv install

CONFIGURE_OPTS='--with-cairo=yes' \
  Renv install 3.5.1

これで SVG 出力できるようになった。

$ R -q
> d = data.frame(
+   x = c(1, 2, 3)
+  ,y = c(12, 13, 14)
+ )
> 
> svg(filename="sample.svg")
> plot(d$x, d$y)
> dev.off()
null device 
          1 
> q()
Save workspace image? [y/n/c]: n

$ firefox sample.svg &
[1] 29954

(2018-10, 一応solved) Ubuntu 16.04 + VirtualBox / RTR3InitEx failed with rc=-1912 / VERR_VM_DRIVER_VERSION_MISMATCH

RTR3InitEx failed with rc=-1912
VM を起動しようとするとこういうメッセージが出る。 メッセージの通りに modprobe vboxdrv しても解消せず。
インストールされている VirtualBox は以前 apt でインストールして、上記のエラーが解消しないのでインストールし直したりした状態(ここらへんはメモってなかった……)。 バージョンを確認すると
$ LANG=C apt list --installed | grep virtualbox

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

unity-scope-virtualbox/xenial,xenial,now 0.1+13.10.20130723-0ubuntu1 all [installed,automatic]
virtualbox/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [installed]
virtualbox-dkms/now 5.0.40-dfsg-0ubuntu1.16.04.2 all [installed,upgradable to: 5.1.38-dfsg-0ubuntu1.16.04.1]
virtualbox-qt/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [installed,automatic]
virtualbox-dkms だけバージョンが違っていて、怪しい(5.1.38〜 にアップグレード可とも表示されている)。 一度アンインストールして再インストール。
sudo apt-get remove virtualbox-dkms
sudo apt-get install virtualbox-dkms
$ LANG=C apt list --installed | grep virtualbox

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

unity-scope-virtualbox/xenial,xenial,now 0.1+13.10.20130723-0ubuntu1 all [installed,automatic]
virtualbox/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [installed]
virtualbox-dkms/xenial-updates,xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 all [installed]
virtualbox-qt/xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 amd64 [installed,automatic]
virtualbox-source/xenial-updates,xenial-updates,now 5.1.38-dfsg-0ubuntu1.16.04.1 all [installed,automatic]
virtualbox-dkms のバージョンが 5.1.38〜 になり、VMが起動するようになった。

SQLのブロックコメントのネストについて軽く調べてみた(PostgreSQL, MySQL, SQLite3)

前回(SQLのブロックコメント終端がバックスラッシュエスケープできるか軽く調べてみた(PostgreSQL, MySQL, SQLite3))のを調べていたときに PostgreSQL のドキュメントに

これらのブロックコメントはC言語とは異なり、標準SQLで規定されているように入れ子にすることができます。

と書かれているのを見かけ、へーと思って MySQL、SQLite3 での対応状況を調べてみた。前回と同様に実際に動かして挙動を見ただけです。

結果

PostgreSQL はドキュメントに書かれている通りネスト可。
MySQL、SQLite3 では不可。

PostgreSQL

$ docker run -d --rm -v $(pwd):/work --name postgres postgres:10.5
973208008fe3ef5d12f4629507a55107802583167ad8511852965e42d0595a37
--------------------------------

$ docker exec -it postgres psql -U postgres
psql (10.5 (Debian 10.5-1.pgdg90+1))
Type "help" for help.

postgres=# select /* /* */ */ 1;
 ?column? 
----------
        1
(1 row)

postgres=# 

MySQL

$ docker run -d --rm --name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.7.23
d787f3142d10a93f461e6ff420d5304dbd02e46f76203a107ebe4fc5fe5ab401
--------------------------------

$ docker exec -it mysql /usr/bin/mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.23 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select /* /* */ */ 1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '/ 1' at line 1
mysql> 

SQLite3

$ sqlite3
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> select /* /* */ */ 1;
Error: near "/": syntax error
sqlite> 

SQLのブロックコメント終端がバックスラッシュエスケープできるか軽く調べてみた(PostgreSQL, MySQL, SQLite3)

仕様・ソースコードレベルまでは調べてなくて実際の挙動を見ただけ。

ブロックコメント終端は2文字あるので、この3パターンを確認すればよさそう。
(1) \*/
(2) *\/
(3) \*\/

結果

コメント終端として認識される or 認識されない

|          | PostgreSQL | MySQL    | SQLite3  |
| -------- | ---------- | -------- | -------- |
| (1) \*/  | される     | される   | される   |
| (2) *\/  | されない   | されない | されない |
| (3) \*\/ | されない   | されない | されない |

当初は「バックスラッシュでエスケープできるか?」という観点で見ていたが、この結果を見ると
「(2) と (3) のパターンだとバックスラッシュでエスケープできる」
のではなく、単純に "*/" を探す動きになっていると考えられる。

(2) と (3) のパターンは * と / の間に余計な文字が入っているので、コメント終端とは見なされない(間に挟まっているのがバックスラッシュであるかどうかも関係ない)のではないかと。

以下、使ったクエリと確認手順のログ。

テスト用クエリ

$ cat -A block_1.sql
select 11, /*22\*/ 33 */ 44;$
--------------------------------
$ cat -A block_2.sql
select 11, /*22*\/ 33 */ 44;$
--------------------------------
$ cat -A block_3.sql
select 11, /*22\*\/ 33 */ 44;$
--------------------------------

どうせなのでついでに行末コメント末尾にバックスラッシュなパターンも見てみた。

$ cat -A single_1.sql
-- foo\$
select 12;$
--------------------------------

PostgreSQL

$ docker run -d --rm -v $(pwd):/work --name postgres postgres:10.5
27d2bb5fa4bd0f44869fab5104e5c63271db194ffe30da58e743da323f2a8d2f
--------------------------------

$ docker exec -it postgres psql -U postgres -f /work/block_1.sql
psql:/work/block_1.sql:1: ERROR:  operator does not exist: integer */ integer
LINE 1: select 11, /*22\*/ 33 */ 44;
                              ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.

--------------------------------
$ docker exec -it postgres psql -U postgres -f /work/block_2.sql
 ?column? | ?column? 
----------+----------
       11 |       44
(1 row)

--------------------------------
$ docker exec -it postgres psql -U postgres -f /work/block_3.sql
 ?column? | ?column? 
----------+----------
       11 |       44
(1 row)

--------------------------------
$ docker exec -it postgres psql -U postgres -f /work/single_1.sql
 ?column? 
----------
       12
(1 row)
--------------------------------

MySQL

$ docker run -d --rm -v$(pwd):/work --name mysql -e MYSQL_ROOT_PASSWORD=fdsa mysql:5.7.23
--------------------------------

起動が完了するまで少し待ってから

$ docker exec -it mysql /bin/bash -c 'MYSQL_PWD=fdsa /usr/bin/mysql -uroot < /work/block_1.sql'
ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '/ 44' at line 1
--------------------------------
$ docker exec -it mysql /bin/bash -c 'MYSQL_PWD=fdsa /usr/bin/mysql -uroot < /work/block_2.sql'
11	44
11	44
--------------------------------
$ docker exec -it mysql /bin/bash -c 'MYSQL_PWD=fdsa /usr/bin/mysql -uroot < /work/block_3.sql'
11	44
11	44
--------------------------------
$ docker exec -it mysql /bin/bash -c 'MYSQL_PWD=fdsa /usr/bin/mysql -uroot < /work/single_1.sql'
12
12
--------------------------------

SQLite3

※見やすくなるように適宜改行を追加しています

$ sqlite3 
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.

sqlite> select 11, /*22\*/ 33 */ 44;
Error: near "/": syntax error

sqlite> select 11, /*22*\/ 33 */ 44;
11|44

sqlite> select 11, /*22\*\/ 33 */ 44;
11|44

sqlite> -- foo\
sqlite> select 12;
12

(Ruby) PTY.spawn("bash -i")でコマンド実行してプロンプトをexpectしつつ途中の出力も随時表示する

  • expectで素朴にプロンプトを待つだけだとコマンドの実行が完了するまで途中の出力が表示できなくていまいちだったのであれこれ試して下記のようにしてみた。
  • プロンプトのパターンに改行が含まれる場合はもうちょい工夫(途中の出力をバッファリングしておく)が必要。
# coding: utf-8

require 'pty'
require 'expect'
require 'timeout'

puts "-->> spawn"

PTY.spawn("bash -i") do |i, o|
  # 最初の出力を出し切る
  begin
    Timeout.timeout(1) do
      loop { print i.getc }
    end
  rescue Timeout::Error
  end

  # 実行するコマンドを送信
  o.puts "ruby command.rb"

  finished = false

  while ! finished
    # プロンプトに加えて途中の出力にもマッチさせる
    i.expect(/(.*)\$ |(.*)\n/) do |m|
      print m[0]
      if m[1]
        # プロンプトが来た場合
        finished = true
      else
        # 途中の出力
      end
    end
  end
end

puts "<<-- spawn"

テスト用の command.rb

(1..3).each do |i|
  $stdout.puts "out #{i}"
  $stderr.puts "err #{i}"
  sleep 1
end
$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]


追記 2018-02-25

単に標準出力に出したいだけなら $expect_verbose = true する方法もあります。

IO#expect のソースはコメントを除くと40行くらいなのでさらっと見ておくとよいです。場合によっては expect 自体をモンキーパッチするのもいいかも。
ruby/expect.rb at v2_5_0 · ruby/ruby

(2017-05) React.js の前に Webpack と Babel ちょっと触ったメモ

ちょっと触った程度のメモです。
標準的なやり方からは外れている(ちょっと試す程度ならいいけど本番には使えない)と思います。

準備

mkdir work && cd work
npm init -y

Babel

Babel で JSX から素の JS に変換できるとのことなので、試す。
なんで Babel で変換できるかは調べてない。とにかくできる。

npm install --save-dev babel-cli

テスト用のファイル。

// test.js
class Foo extends React.Component {
  render() {
    return (
      <p>foo content</p>
    );
  }
}
$ node_modules/.bin/babel test.js
SyntaxError: test.js: Unexpected token (4:6)

babel コマンドは使えるが JSX の部分でエラーになる。
プリセットというのが必要っぽい。

# npm install --save-dev と同じ
npm i -D babel-preset-react

.babelrc で react preset を使うように指定

{
  "presets": [
    "react"
  ]
}

変換できるようになった。
何も指定しないと標準出力に出る。

$ node_modules/.bin/babel test.js
class Foo extends React.Component {
  render() {
    return React.createElement(
      "p",
      null,
      "foo content"
    );
  }
}

-d (--out-dir) で指定したディレクトリに出力。

$ node_modules/.bin/babel test.js -d dist/
test.js -> dist/test.js

$ ls dist
test.js

変換元ファイルの拡張子が .jsx の場合、.js に置き換えられる。

$ node_modules/.bin/babel test.jsx -d dist/
test.jsx -> dist/test.js

変換元にファイルではなくディレクトリを指定すると、そのディレクトリにあるファイルを全部変換してくれる。

$ node_modules/.bin/babel src -d dist
src/test1.jsx -> dist/test1.js
src/test2.jsx -> dist/test2.js

Webpack

だいたい分かったので次は Webpack。
JSX の部分がなくなって素の JS ができてしまえば、後は Webpack のフローに乗せてしまえるという寸法。

src/
↓ Babel で JSX を変換
src_babeled/
↓ Webpack でまとめる
dist/bundle.js

のようにしてみる。

npm i -D webpack

上で作った余計なファイルを消して、src/entry.jsx を作る。

$ ls src
entry.jsx

$ cat src/entry.jsx
class Foo extends React.Component {
  render() {
    return (
      <p>foo content</p>
    );
  }
}
 
window.onload = function(){
  ReactDOM.render(<Foo />, document.querySelector("#content"));
};

webpack.config.js を書く。

module.exports = {
  entry: "./src_babeled/entry.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  }
};
$ node_modules/.bin/babel src -d src_babeled
src/entry.jsx -> src_babeled/entry.js

$ node_modules/.bin/webpack --config webpack.config.js
Hash: 436e069420e6239a9f13
Version: webpack 2.6.1
Time: 37ms
    Asset     Size  Chunks             Chunk Names
bundle.js  2.89 kB       0  [emitted]  main
   [0] ./src_babeled/entry.js 257 bytes {0} [built]

毎回これやるのだるいのでビルド用のスクリプトを書く。

#!/bin/bash

node_modules/.bin/babel src -d src_babeled
node_modules/.bin/webpack --config webpack.config.js

dist/bundle.js が生成された。
html からはこれを使う。

<html>
  <head>
    <script src="dist/bundle.js"></script>
  </head>
  <body>
    <div id="content"></div>
  </body>
</html>

ブラウザで開くとエラーが出る。

Uncaught ReferenceError: React is not defined

React 自体をまだインストールしていなかった。
あと、react-dom も必要なので一緒にインストールする。

npm install --save react react-dom

require する。

// src/entry.jsx の先頭に追加
var React = require("react");
var ReactDOM = require("react-dom");

ビルドして index.html をブラウザで見ると、画面に "foo content" と表示された。

バージョン

$ npm list --depth=0
work@1.0.0 /path/to/work
├── babel-cli@6.24.1
├── babel-preset-react@6.24.1
├── react@15.5.4
├── react-dom@15.5.4
└── webpack@2.6.1

$ node -v
v7.3.0