Selenium Gridにイメージセレクタを付ける

# はじめに

ブラウザだけでなく操作リモート先端末の別アプリケーションを動かすなど、ちょっと複雑なことが必要な感じになってきました。

あーもうこれよくわかんねぇなと思い、いっそのこと画像イメージで動かせるようにしたれと作ってみました:tired_face:

コード内でStream使っていなかったり、いろいろやっつけているところはありますが備忘録として記録します。

イメージセレクタの関係で、Nodeは64bit  OSのみ対応となります。


# 利用したソフトウェア

検証するときに利用したソフトウェアです。
Windowsで検証構築を構築しています。

|ソフト|バージョン|用途|
|:--|:--|:--|
|[java 64bit](https://adoptopenjdk.net/index.html)|jdk-8.0.212.03-hotspot(AdoptOpenJDK)||
|[selenium-server-standalone.jar](https://www.seleniumhq.org/)|3.141.59||
|[sikulixapi](http://sikulix.com/)|1.1.4-SNAPSHOT|Nodeのイメージセレクタ<br />64bit限定で動作|
|gson|2.8.5|jsonファイルの入出力|
| httpclient                                                   |4.5.8|RESTを実行|


# 環境構築

Mavenで一瞬です。

### Mavenファイル

<details><summary>Mavenファイル（展開）</summary><div>

```xml:pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>selenium-grid-extend</groupId>
  <artifactId>selenium-grid-extend</artifactId>
  <version>0.0.1</version>
  <repositories>
    <repository>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <id>sonatype</id>
      <name>sonatype Repository</name>
      <url>http://oss.sonatype.org/content/groups/public</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <releases>
        <updatePolicy>never</updatePolicy>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <id>sonatype</id>
      <name>sonatype Repository</name>
      <url>http://oss.sonatype.org/content/groups/public</url>
    </pluginRepository>
  </pluginRepositories>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <testSourceDirectory>src</testSourceDirectory>
    <resources>
      <resource>
        <directory>resource</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>resource</directory>
      </testResource>
    </testResources>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-server</artifactId>
      <version>3.141.59</version>
    </dependency>
    <dependency>
    	<groupId>com.sikulix</groupId>
    	<artifactId>sikulixapi</artifactId>
    	<version>1.1.4-SNAPSHOT</version>
    </dependency>
    <dependency>
    	<groupId>com.google.code.gson</groupId>
    	<artifactId>gson</artifactId>
    	<version>2.8.5</version>
    </dependency>
    <dependency>
    	<groupId>org.apache.httpcomponents</groupId>
    	<artifactId>httpclient</artifactId>
    	<version>4.5.8</version>
    </dependency>
  </dependencies>
  <properties>
    <java.version>1.8</java.version>
    <file.encoding>UTF-8</file.encoding>
    <project.build.sourceEncoding>${file.encoding}</project.build.sourceEncoding>
    <project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
    <maven.compiler.encoding>${file.encoding}</maven.compiler.encoding>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
  </properties>
</project>
```

</div></details>


# プログラム

今回は /grid/admin/RequestToSessionMachine ～ のリクエストをURLに指定されているセッションの端末に送ります。
URL以外のやり取りは全てJSON形式で送りあいます。

要注意ですがNodeを接続しただけではsessionはありません。
ブラウザなりなんなりを操作できる状態になって初めてsessionがHubに作られます。

最終的には 以下のようにHubに送られてきたリクエストを

```console
http://HubIP:HubPort/grid/admin/RequestToSessionMachine/session/99999XXXXX99999/extra/ImageSelector/doubleclick
```

『99999XXXXX99999』セッションが動いているNodeに以下のようにリクエストして結果を待ち、呼出しもとにそのまま戻します。

```console
http://NodeIP:NodePort//extra/ImageSelector/doubleclick
```

### Hub側プログラム

TestSession.forward で送れるようにするのがベストなんでしょうが、SeleniumBasedRequestが引数に必要だったりで、~~調査が面倒だったので~~動けば良いと思い使うのをやめました。

sessionタイムアウトの時間にかからないようにアクセス時間を更新していますが、タイムアウト時間によってはうまくいかないかもしれません。

<details><summary>Hub側プログラム（展開）</summary><div>

```java:RequestToSessionMachine.java
package selenium.extend.hub.servlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.openqa.grid.common.exception.GridException;
import org.openqa.grid.internal.GridRegistry;
import org.openqa.grid.internal.TestSession;
import org.openqa.grid.internal.TestSlot;
import org.openqa.grid.web.servlet.RegistryBasedServlet;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

public class RequestToSessionMachine extends RegistryBasedServlet {

    private static final Pattern SESSION_ID_PATTERN = Pattern.compile("/grid/admin/RequestToSessionMachine/session/([^/]+).*");

    public RequestToSessionMachine() {
        this(null);
    }

    public RequestToSessionMachine(GridRegistry registry) {
        super(registry);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        process(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        process(request, response);
    }

    protected void process(HttpServletRequest request, HttpServletResponse response) throws IOException {
        System.out.println("Start RequestToSessionMachine");

        response.setContentType("application/json");
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());

        JsonObject json = new JsonObject();

        CloseableHttpClient client = null;
        CloseableHttpResponse res = null;

        try {
            // URLのセッションIDに紐付く情報を取得
            TestSession session = getActiveTestSession(getSessionIdFromPath(request.getRequestURI()));

            if (session != null) {

                // セッションのアクセス時間再設定(タイムアウト時間を伸ばす)
                session.setIgnoreTimeout(false);

                // セッションがあるノードへの接続URL生成
                TestSlot slot = session.getSlot();
                URL remoteRequestURL = new URL(slot.getRemoteURL(), trimSessionPath(request.getRequestURI()));

                // bodyのJsonをそのままノードにリクエスト
                client = HttpClients.createDefault();

                HttpPost httpPost = new HttpPost(remoteRequestURL.toURI());
                httpPost.setHeader("Content-type", "application/json; charset=UTF-8");

                BufferedReader bufferReaderBody = new BufferedReader(request.getReader());
                StringBuilder jsonBody = new StringBuilder();
                String line = null;

                while ((line = bufferReaderBody.readLine()) != null) {
                    jsonBody.append(line);
                }

                StringEntity entity = new StringEntity(jsonBody.toString(), StandardCharsets.UTF_8);
                httpPost.setEntity(entity);

                res = client.execute(httpPost);

                // セッションのアクセス時間再設定(タイムアウト時間を伸ばす)
                session.setIgnoreTimeout(false);

                int status = res.getStatusLine().getStatusCode();
                response.setStatus(status);

                if (status == 200) {
                    Gson gson = new Gson();
                    json = gson.fromJson(EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8), JsonObject.class);
                } else {
                    json.addProperty("error", "Response Code " + status);
                }

            } else {
                json.addProperty("error", "No Match Active Test Session for Session ID");
            }

        } catch (MalformedURLException e) {
            e.printStackTrace();
            json.addProperty("error", e.getMessage());
        } catch (URISyntaxException e) {
            e.printStackTrace();
            json.addProperty("error", e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            json.addProperty("error", e.getMessage());
        } finally {
            try {
                if (res != null) {
                    res.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new GridException(e.getMessage());
            } finally {
                if (client != null) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new GridException(e.getMessage());
                    }
                }
            }
        }

        System.out.println("ResponseJson:" + json.toString());
        response.getWriter().print(json);
        response.getWriter().close();

        System.out.println("End RequestToSessionMachine");
    }

    private TestSession getActiveTestSession(String sessionId) {
        Iterator<TestSession> itr = super.getRegistry().getActiveSessions().iterator();
        TestSession session = null;

        System.out.println("Active Session Size:" + super.getRegistry().getActiveSessions().size());
        System.out.println("Search Session ID:" + sessionId);

        while (itr.hasNext()) {
            TestSession s = itr.next();
            if (s.getExternalKey().getKey().equals(sessionId)) {
                session = s;
                break;
            }
        }

        return session;
    }

    private String getSessionIdFromPath(String pathInfo) {
        Matcher matcher = SESSION_ID_PATTERN.matcher(pathInfo);
        if (matcher.matches()) {
            return matcher.group(1);
        }
        throw new IllegalArgumentException("Invalid request. Session Id is not present");
    }

    private String trimSessionPath(String pathInfo) {
        return pathInfo.replaceFirst("/grid/admin/RequestToSessionMachine/session/" + getSessionIdFromPath(pathInfo), "");
    }

}
```

</div></details>

### Node側プログラム

sikulixのAPIを利用し、送られてきたJSONのBase64化したイメージ画像からデスクトップ内の一致する部分をダブルクリックします。

[sikulix-api](https://sikulix-2014.readthedocs.io/en/latest/index.html#sikulix-api-for-scripting-and-java-programming) 操作できる内容は公式ページをみてください。

<details><summary>Node側プログラム（展開）</summary><div>

```java:ImageSelector.java
package selenium.extend.node.servlet;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.sikuli.script.FindFailed;
import org.sikuli.script.Image;
import org.sikuli.script.Screen;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

public class ImageSelector extends HttpServlet {

    private static final String DOUBLECLICK = "/extra/ImageSelector/doubleclick";

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

        JsonObject jsonResponse = null;
        if (request.getRequestURI().startsWith(DOUBLECLICK)) {
            jsonResponse = doubleClick(request, response);

        } else {
            jsonResponse = new JsonObject();
            jsonResponse.addProperty("error", "Request Command is No Support");
        }

        response.getWriter().print(jsonResponse);
        response.getWriter().close();
    }

    protected JsonObject doubleClick(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("Start ImageSelector");

        response.setContentType("application/json");
        response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
        response.setStatus(200);

        JsonObject json = new JsonObject();

        try {
            // jsonから選択するbodyの情報を取り出す
            BufferedReader bufferReaderBody = new BufferedReader(request.getReader());
            StringBuilder jsonBody = new StringBuilder();
            String line = null;

            while ((line = bufferReaderBody.readLine()) != null) {
                jsonBody.append(line);
            }

            Gson gson = new Gson();
            JsonObject reqJson = gson.fromJson(jsonBody.toString(), JsonObject.class);
            String imageBase64 = reqJson.get("imageBase64").getAsString();

            String[] parts = imageBase64.split(",");
            String imageString = parts[1];

            byte[] imageByte = Base64.getDecoder().decode(imageString);
            ByteArrayInputStream bis = new ByteArrayInputStream(imageByte);
            Image image = new Image(ImageIO.read(bis));

            bis.close();

            // スクリーン全体からイメージを探しダブルクリック
            Screen sc = new Screen();
            // 待機時間設定
            sc.setAutoWaitTimeout(30);

            sc.doubleClick(image);

            json.addProperty("info", "Done DoubleClick");

        } catch (IOException e) {
            e.printStackTrace();
            json.addProperty("error", e.getMessage());
        } catch (FindFailed e) {
            e.printStackTrace();
            json.addProperty("error", e.getMessage());
        }

        System.out.println("End ImageSelector");

        return json;
    }

}
```

</div></details>


# jarファイル作成

Selenium Grid に読み込ませるためにjar化します。
以下のようなディレクトリ構成です。

以前の記事で作ったAllNodes系がありますが、今回は必要ありません。
![aWS050632.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187061/ef1e895a-12f7-f129-5d08-69f4c7694a65.jpeg)

以下のファイルだけjarにエクスポートします。
名前は『extend.jar』にしています。
![aWS050634.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187061/a0a8657f-1469-4d02-b6e1-515ae9876911.jpeg)


# 実行環境起動

『extend.jar』をHubとNodeのディレクトリに配置して読み込ませます。

### Hub起動

<details><summary>Hubのディレクトリ構成とコマンド（展開）</summary><div>

```console
C:.
│  start-hub.bat
│
└─lib
        commons-logging-1.2.jar
        extend.jar
        gson-2.8.5.jar
        httpclient-4.5.8.jar
        httpcore-4.4.11.jar
        selenium-server-standalone-3.141.59.jar
```

```console:start-hub.bat
java -cp lib/* org.openqa.grid.selenium.GridLauncherV3 -role hub -servlets "selenium.extend.hub.servlet.RequestToSessionMachine"
```

</div></details>

起動すると4行目のように RequestToSessionMachine へのアクセスパスが表示されます。

```console
C:\selenium>java -cp lib/* org.openqa.grid.selenium.GridLauncherV3 -role hub -servlets "selenium.extend.hub.servlet.RequestToSessionMachine"
15:57:41.639 INFO [GridLauncherV3.parse] - Selenium server version: 3.141.59, revision: e82be7d358
15:57:41.764 INFO [GridLauncherV3.lambda$buildLaunchers$5] - Launching Selenium Grid hub on port XXXX
15:57:41.858 INFO [Hub.<init>] - binding selenium.extend.hub.servlet.RequestToSessionMachine to /grid/admin/RequestToSessionMachine/*
2019-05-31 15:57:42.242:INFO::main: Logging initialized @1617ms to org.seleniumhq.jetty9.util.log.StdErrLog
15:57:42.757 INFO [Hub.start] - Selenium Grid hub is up and running
15:57:42.773 INFO [Hub.start] - Nodes should register to http://XXX.XXX.XXX.XXX:XXXX/grid/register/
15:57:42.773 INFO [Hub.start] - Clients should connect to http://XXX.XXX.XXX.XXX:XXXX/wd/hub
```

### Node起動

<details><summary>Hubのディレクトリ構成とコマンド（展開）</summary><div>

```console
C:.
│  chromedriver.exe
│  NodeConfigBrowser.json
│  start-node.bat
│
└─lib
        commons-logging-1.2.jar
        extend.jar
        gson-2.8.5.jar
        httpclient-4.5.8.jar
        httpcore-4.4.11.jar
        jna-4.5.2.jar
        jna-platform-4.5.2.jar
        selenium-server-standalone-3.141.59.jar
        sikulix2tigervnc-2.0.0-SNAPSHOT.jar
        sikulixapi-1.1.4-SNAPSHOT.jar
```

```json:NodeConfigBrowser.json
{
 "capabilities": [
    {
     "platform": "WINDOWS",
     "browserName": "chrome",
     "maxInstances": 1,
     "seleniumProtocol": "WebDriver"
    }
  ],
 "hub": "http://XXXXXXXX:XXXX/grid/register",
 "register": true
}
```

```console:start-node.bat
java -Dwebdriver.chrome.driver=chromedriver.exe -cp lib/* org.openqa.grid.selenium.GridLauncherV3 -role node -servlets "selenium.extend.node.servlet.ImageSelector" -nodeConfig NodeConfigBrowser.json
```

</div></details>

起動すると4行目のように ImageSelector へのアクセスパスが表示されます。

```console
C:\selenium-node>java -Dwebdriver.chrome.driver=chromedriver.exe -cp lib/* org.openqa.grid.selenium.GridLauncherV3 -role node -servlets "selenium.extend.node.servlet.ImageSelector" -nodeConfig NodeConfigBrowser.json
16:01:08.578 INFO [GridLauncherV3.parse] - Selenium server version: 3.141.59, revision: e82be7d358
16:01:08.704 INFO [GridLauncherV3.lambda$buildLaunchers$7] - Launching a Selenium Grid node on port XXXX
16:01:09.126 INFO [SelfRegisteringRemote.addExtraServlets] - binding selenium.extend.node.servlet.ImageSelector to /extra/ImageSelector/*
2019-05-31 16:01:09.220:INFO::main: Logging initialized @981ms to org.seleniumhq.jetty9.util.log.StdErrLog
16:01:09.485 INFO [WebDriverServlet.<init>] - Initialising WebDriverServlet
16:01:09.594 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 42345
16:01:09.594 INFO [GridLauncherV3.lambda$buildLaunchers$7] - Selenium Grid node is up and ready to register to the hub
16:01:09.750 INFO [SelfRegisteringRemote$1.run] - Starting auto registration thread. Will try to register every 5000 ms.
16:01:10.237 INFO [SelfRegisteringRemote.registerToHub] - Registering the node to the hub: http://XXX.XXX.XXX.XXX:XXXX/grid/register
16:01:10.549 INFO [SelfRegisteringRemote.registerToHub] - The node is registered to the hub and ready to use
```


# 動かしてみる

curlでもなんでも良いのですが、sessionを生成させるのが手間なので以前の記事で作った環境を利用します。

### テストプログラム

操作先の画像を適当なwebサービスでbase64化して、その情報をRESTしています。

base64化プログラムに組んでしまった方が今後の確認で楽だったかも。

<details><summary>テストプログラム（展開）</summary><div>

```java:GoogleTest.java
package test;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import org.openqa.selenium.Point;

import com.google.gson.JsonObject;

import page.Google;

public class GoogleTest extends TestBase {

    @Test
    public void image() throws Exception {

        driver.get(Google._url);
        // ブラウザを見えない位置に移動
        driver.manage().window().setPosition(new Point(-2000, 0));

        CloseableHttpClient client = null;
        CloseableHttpResponse res = null;

        JsonObject json = new JsonObject();

        try {

            // ノードの接続URL生成
            URL remoteRequestURL = new URL("http://localhost:4444/grid/admin/RequestToSessionMachine/session/" + driver.getSessionId() + "/extra/ImageSelector/doubleclick");

            // bodyのJsonをそのままノードにリクエスト
            client = HttpClients.createDefault();

            HttpPost httpPost = new HttpPost(remoteRequestURL.toURI());
            httpPost.setHeader("Content-type", "application/json; charset=UTF-8");

            json.addProperty("imageBase64", "data:image/png;base64,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");

            StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8);
            httpPost.setEntity(entity);

            res = client.execute(httpPost);

            System.out.println(res.getStatusLine().getStatusCode());
            System.out.println(EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8));

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (res != null) {
                    res.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (client != null) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
```

</div></details>

1. Chromeを起動
2. googleにアクセス
3. Chromeを見えない位置に移動
4. 以下の画像をダブルクリック

![aaWS050367.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187061/9885abaf-e2d3-dafe-177c-c389e8a2872a.png)

実際にNode側ではこのように動きます:kissing_heart:
![ggggg1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187061/fcfd8ca5-5bea-39f1-5e03-e8f56092c4e0.gif)