docker-machineでselenium実行時にダウンロードしたファイル名がおかしくなる

# docker-machineでselenium実行時にダウンロードしたファイル名がおかしくなる

`docker-machine`固有の問題なのかどうかわからないのですが、コンテナ上で実行している`selenium`でファイルをダウンロードした際に何故かダウンロードしたファイル名がおかしくなる（どうおかしいかは後述）という問題でかなりハマったのでその作業メモ

- 環境
  - ruby 2.3.0
  - docker-machine 0.7.0 (virtual box)
  - docker 1.11.0

##　seleniumでテストする準備

まず問題のファイルダウンロードの前に`selenium`でテストするための環境です。

### seleniumの操作環境構築
`selenium`の操作は`ruby`で行おうと思うので`gem`のインストールです。

- 環境
  - ruby 2.3.0

```
bundle init

echo "gem 'selenium-webdriver'" >> Gemfile

bundle install --path=vendor/bundle
```

### seleniumコンテナ起動

こちらはすごく簡単で、公式のコンテナを起動するだけです。今回は現時点での最新版の`2.53.0`で試します。

```
docker run -d -p 4444:4444 -p 5900:5900 selenium/standalone-chrome-debug:2.53.0
```

ポート`4444`が実際に`selenium`にアクセスするポートで、ポート`5900`がコンテナの中でブラウザが動いている様子を見るための`vnc`の入り口です。

#### vncサーバに接続

コンテナ内で動くブラウザの様子を確認するために`vnc`サーバに接続します。`mac`の場合は`vnc`クライアントが標準で入っているので、
Finder起動 -> `command + K` で`サーバへ接続`のダイアログが出てくるので、`サーバアドレス`のところに

```
vnc://<docker-machineのip>:5900
```

![スクリーンショット 2016-04-26 23.17.32.png](https://qiita-image-store.s3.amazonaws.com/0/34692/02c03a6a-e4c2-8a8c-80d3-4680c76c8ae3.png)

を入力します。
するとパスワードを聞かれるので`secret`を入力します。
![スクリーンショット 2016-04-26 23.18.51.png](https://qiita-image-store.s3.amazonaws.com/0/34692/ead79784-88ff-6a53-53b5-77b2339a5628.png)

下記のような画面になれば準備完了です。

<img width="799" alt="スクリーンショット 2016-04-26 23.19.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/34692/3e56d5d9-1a3d-9975-ded6-9171601f481d.png">

### `selenium`操作スクリプト

単純にグーグルを開いて検索して検索結果を取得、という処理を書いてみましょう。

```rb:run-selenium.rb
require 'selenium-webdriver'

#webdriver操作用のユーティリティ
def with_webdriver
  driver = Selenium::WebDriver.for(
    :remote,
    url: "http://192.168.99.100:4444/wd/hub",
    desired_capabilities: :chrome
  )

  # implict_waitを設定しておくとfind_elementなどで描画待ちでまだ取得できない場合などに待ってくれる
  driver.manage.timeouts.implicit_wait = 10

  yield driver
ensure
  driver.quit
end

with_webdriver do |d|
  # gooleにアクセス
  d.navigate.to "https://www.google.co.jp"

  # 検索ボックス取得
  search_box = d.find_element(id: "lst-ib")

  # 検索ボックスに検索ワードを入れて
  search_box.send_keys("docker selenium-webdriver")
  # enter押下
  search_box.send_keys(:return)

  # 検索結果の見出しはclassが"r"のdivに囲まれた"a"タグのようなのでそれを全部表示
  d.find_elements(:class, "r").each do |result|
    puts result.text
  end

  # この状態をスクリーンショットで保存
  d.save_screenshot "google_search_result.png"
end
```

これを実行すると下記のように検索結果の見出行が表示された上でその時のスクリーンショットが`png`で保存されます。

```sh
$ bundle exec ruby run-selenium.rb
GitHub - SeleniumHQ/docker-selenium: Docker images for ...
Docker で Selenium Grid の環境構築をもっと楽に！ - Cybozu ...
selenium/hub public - Docker Hub
Docker で Selenium Grid による並列実行環境を構築 - Qiita
Dockerを使って素早くSelenium Grid環境を構築しよう | hifive ...
SeleniumサーバもDockerで。「Docker Selenium」レビュー ...
Docker Selenium. Getting Started - Selenium Conf 2015 ...
Docker Hub の Selenium Hub/Node イメージ + fig で ...
Selenium Grid with Docker | TEST DETECTIVE
Running headless Selenium WebDriver tests in Docker ...
```

取得したスクリーンショット(`google_search_result.png`)
![google_search_result.png](https://qiita-image-store.s3.amazonaws.com/0/34692/d1f8b968-6498-63af-ff32-81a22f8d8641.png)

便利ですね。

### ファイルのダウンロードで問題発生

さて、次にファイルのダウンロードを行いましょう。

適当に[郵便局のページ](http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html)から北海道の郵便番号ファイルを取得してみます。


![スクリーンショット 2016-04-27 0.05.29.png](https://qiita-image-store.s3.amazonaws.com/0/34692/6adadd9d-afb0-11db-49a3-1c1b9737eeb1.png)

```rb:download-zip.rb
require 'selenium-webdriver'

def with_webdriver
  driver = Selenium::WebDriver.for(
    :remote,
    url: "http://192.168.99.100:4444/wd/hub",
    desired_capabilities: :chrome
  )
  driver.manage.timeouts.implicit_wait = 10
  yield driver
ensure
  driver.quit
end

with_webdriver do |d|
  d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"

  # ダウンロードリンク取得
  link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
  link.click

  #ダウンロード待ち
  sleep 10
end
```

実行

```sh
bundle exec ruby download-zip.rb
```

ダウンロードされたファイルはどこにあるんでしょうか。コンテナの中を検索してみましょう。

```sh
# 起動中のseleniumコンテナのコンテナ名確認
$ docker ps --format "table {{.Image}}\t{{.Names}}"
IMAGE                                     NAMES
selenium/standalone-chrome-debug:2.53.0   hopeful_albattani
# そのコンテナに対してzipファイルの検索実行
$ docker exec hopeful_albattani find / -name '*.zip'
/home/seluser/Downloads/01hokkai.zip
```

`/home/seluser/Downloads`以下に保存されるようですね。

#### コンテナのファイルをホスト側で取得
ダウンロードしたならそのファイルをスクリプトから取得したいですね。
というわけで保存場所も分かったので、`volume`でマウントしてみます。

```sh
# コンテナを落とします。（コンテナ名は先程調べた名称)
$ docker kill hopeful_albattani
# ダウンロードファイルを保存するフォルダを作ります。
$ mkdir downloads
# このフォルダをマウントして再度コンテナを起動します。
$ docker run -d -v $(pwd)/downloads:/home/seluser/Downloads -p 4444:4444 -p 5900:5900 selenium/standalone-chrome-debug:2.53.0
$ ls downloads
# => まだ空っぽ
```

この状態でダウンロード処理を実行してから`downloads`フォルダを確認します。

```sh
$ bundle exec ruby download-zip.rb
$ ls downloads
01hokkai.zip
```

ダウンロードファイルをホスト側に取り込めました！

もう一回実行してみましょう。

```sh
$ bundle exec ruby download-zip.rb
$ ls downloads
01hokkai (1).zip  01hokkai.zip
```

実際に手動で操作した時のように同じファイルが既に存在したら、`ファイル名 (n).拡張子`の形式で保存されてますね。

###　ここでハマる

いまやろうとしているのは繰り返しダウンロード処理を実行出来るようにしたかったのでプログラム中でダウンロードしたファイルを使ったら都度消して次回実行時も同じファイル名で取得できるようにしたかったので、ダウンロードが完了するまで待って、中身を使って、消す、という処理を行う必要がありました。

#### seleniumでのwait処理
先ほどの処理のようにダウンロード完了を`sleep`で待っていると無駄にまったり、まだ終わっていないのに`sleep`が終わる可能性があります。

seleniumには`wait`することも考慮されていて待つためのユーティリティがそろっています。

#### waitオブジェクト

waitオブジェクトを作って`until`メソッドをブロック付きで実行すると
最大`waitオブジェクトに設定した待ち時間`待つかブロックが真を返すまで待ってくれます。
待ち時間をすぎれば例外、ブロックが真を返せばその結果が`until`の結果として返されます。
このときブロックは複数回実行されるので注意が必要です。

```rb
  w = Selenium::WebDriver::Wait.new(:timeout => 10)
  w.until do 
    # このブロックが真(というか非nil)を返すまで複数回実行される
  end
```

これを利用して特定の条件がそろうまでwaitするメソッドを作成します。

```rb
def wait_until(&b)
  w = Selenium::WebDriver::Wait.new(:timeout => 10)
  w.until(&b)
end
```

これを使うと`donwloads`フォルダに`01hokkai.zip`が出来るまで待つ、という処理は

```rb
def wait_until_download(file)
  wait_until do 
    File.exists?(file)
  end
end
```

というような感じでかけます。

というわけで下記のような処理を実行してみます。

```rb:download-zip.rb
require 'selenium-webdriver'

def with_webdriver
  driver = Selenium::WebDriver.for(
    :remote,
    url: "http://192.168.99.100:4444/wd/hub",
    desired_capabilities: :chrome
  )
  driver.manage.timeouts.implicit_wait = 10
  yield driver
ensure
  driver.quit
end

def wait_until(&b)
  w = Selenium::WebDriver::Wait.new(:timeout => 10)
  w.until(&b)
end

def wait_until_download(file)
  wait_until do 
    File.exists?(file)
  end
end

with_webdriver do |d|
  d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"

  # ダウンロードリンク取得
  link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
  link.click

  begin
    # ダウンロードを待つ
    path = "./downloads/01hokkai.zip"
    wait_until_download(path)

    # ダウンロードしたファイルを使う
    p [path, File.size(path)]
  ensure
    # 終わったらダウンロード削除
    File.delete(path) if File.exists?(path)
  end
end
```

実行してみましょう。した後、downloadsフォルダを確認してみましょう。

```sh
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
$ #何も無い
```

きちんとダウンローしたものをプログラムで使用してから削除できました。

では、続けて2回目を実行してみましょう。

```sh
$ bundle exec ruby download-zip.rb
〜略〜/vendor/bundle/ruby/2.3.0/gems/selenium-webdriver-2.53.0/lib/selenium/webdriver/common/wait.rb:76:in `until': timed out after 10 seconds (Selenium::WebDriver::Error::TimeOutError)
        from download-zip.rb:17:in `wait_until'
        from download-zip.rb:21:in `wait_until_download'
        from download-zip.rb:36:in `block in <main>'
        from download-zip.rb:10:in `with_webdriver'
        from download-zip.rb:26:in `<main>'
$ ls downloads
01hokkai (1).zip
```

なぜかダウンロードに失敗して異常終了し、downloadsフォルダには連番付きで`01hokkai (1).zip`でファイルができています。
ファイルはきちんと消していたのに何故？？

#### 解決？

いろいろ調べましたが。これ！というような原因はわかりませんでした。ですが、1つ考えられるのは、docker-machineでvirtual boxのファイル共有機能でコンテナとファイルを共有しているので、その辺かな？と思い、
ダウンロードファイルがを待つ処理(`wait_until_download`)を

- ファイルが存在しており、そのファイルが前回調べた時と同じファイルサイズのまま

という条件にしてみました。

```rb
def wait_until_download(file)
  wait_until do 
    prev_size = -1
    wait_until do
      if File.exists?(pati)
        size = File.size(file)
        ret = if size == prev_size
                true
              else
                prev_size = size
                false
              end
        sleep 1
        ret
      end
    end
  end
end
```

`sleep 1`なんかが、なかなか無理矢理感がありますがこれ入れないと何故かエラーがでてしまい。。

で最終的に下記のスクリプトが完成品です。

```rb:download-zip.rb
require 'selenium-webdriver'

def with_webdriver
  driver = Selenium::WebDriver.for(
    :remote,
    url: "http://192.168.99.100:4444/wd/hub",
    desired_capabilities: :chrome
  )
  driver.manage.timeouts.implicit_wait = 10
  yield driver
ensure
  driver.quit
end

def wait_until(&b)
  w = Selenium::WebDriver::Wait.new(:timeout => 10)
  w.until(&b)
end

def wait_until_download(file)
  prev_size = -1
  wait_until do
    if File.exists?(file)
      size = File.size(file)
      ret = if size == prev_size
              true
            else
              prev_size = size
              false
            end
      sleep 1
      ret
    end
  end
end

with_webdriver do |d|
  d.navigate.to "http://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html"

  # ダウンロードリンク取得
  link = d.find_element(:xpath, "//*[@href='oogaki/zip/01hokkai.zip']")
  link.click

  begin
    # ダウンロードを待つ
    path = "./downloads/01hokkai.zip"
    wait_until_download(path)

    # ダウンロードしたファイルを使う
    p [path, File.size(path)]
  ensure
    # 終わったらダウンロード削除
    File.delete(path) if File.exists?(path)
  end
end
```

これで2回連続実行してみましょう。

```sh
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
#=>何も無い
$ bundle exec ruby download-zip.rb
["./downloads/01hokkai.zip", 107252]
$ ls downloads
#=>何も無い
```

無事連続してダウンロード出来るようになりました。しかしどういう理由なんでしょうねぇ。

ダウンロードするコンテナ側のプロセスがファイルを掴んでる間にホスト側でファイルを消してしまってる？？よくわかりません。


以上の内容を

https://github.com/pocari/docker-selenium-sample

に置いています。