自訂節點

如何自訂節點

有時候我們會希望根據自身需求自訂節點。

例如,我們可能希望在會話開始執行前進行一些額外設定,並在會話執行完成後進行一些清理。

可以按照以下步驟進行

  • 建立一個類別,繼承 org.openqa.selenium.grid.node.Node

  • 在新建立的類別中新增一個靜態方法 (這將是我們的工廠方法),其簽名如下所示

    public static Node create(Config config)。其中

    • Node 的類型為 org.openqa.selenium.grid.node.Node
    • Config 的類型為 org.openqa.selenium.grid.config.Config
  • 在此工廠方法中,加入建立新類別的邏輯。

  • 若要將此新的自訂邏輯連結到 hub,請啟動節點,並將上述類別的完整類別名稱傳遞至引數 `--node-implementation`

讓我們來看一個關於這一切的範例

將自訂節點作為 uber jar

  1. 使用您最喜歡的建置工具 (Maven|Gradle) 建立一個範例專案。
  2. 將以下依賴項新增至您的範例專案。
  3. 將您的自訂節點新增至專案。
  4. 建置一個 uber jar,以便能夠使用 `java -jar` 指令啟動節點。
  5. 現在使用以下指令啟動節點
java -jar custom_node-server.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

注意: 如果您使用 Maven 作為建置工具,請優先使用 maven-shade-plugin 而非 maven-assembly-plugin,因為 maven-assembly plugin 似乎在合併多個 Service Provider Interface 檔案 (META-INF/services) 時存在問題。

將自訂節點作為一般 jar

  1. 使用您最喜歡的建置工具 (Maven|Gradle) 建立一個範例專案。
  2. 將以下依賴項新增至您的範例專案。
  3. 將您的自訂節點新增至專案。
  4. 使用您的建置工具建置專案的 jar 檔案。
  5. 現在使用以下指令啟動節點
java -jar selenium-server-4.6.0.jar \
--ext custom_node-1.0-SNAPSHOT.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

以下是一個範例,僅在節點上發生感興趣的活動 (會話已建立、會話已刪除、已執行 webdriver 指令等) 時,將一些訊息列印到主控台。

自訂節點範例
package org.seleniumhq.samples;

import java.io.IOException;
import java.net.URI;
import java.util.UUID;
import java.util.function.Supplier;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.NoSuchSessionException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.grid.config.Config;
import org.openqa.selenium.grid.data.CreateSessionRequest;
import org.openqa.selenium.grid.data.CreateSessionResponse;
import org.openqa.selenium.grid.data.NodeId;
import org.openqa.selenium.grid.data.NodeStatus;
import org.openqa.selenium.grid.data.Session;
import org.openqa.selenium.grid.log.LoggingOptions;
import org.openqa.selenium.grid.node.HealthCheck;
import org.openqa.selenium.grid.node.Node;
import org.openqa.selenium.grid.node.local.LocalNodeFactory;
import org.openqa.selenium.grid.security.Secret;
import org.openqa.selenium.grid.security.SecretOptions;
import org.openqa.selenium.grid.server.BaseServerOptions;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.io.TemporaryFilesystem;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.tracing.Tracer;

public class DecoratedLoggingNode extends Node {

  private Node node;

  protected DecoratedLoggingNode(Tracer tracer, NodeId nodeId, URI uri, Secret registrationSecret, Duration sessionTimeout) {
    super(tracer, nodeId, uri, registrationSecret, sessionTimeout);
  }

  public static Node create(Config config) {
    LoggingOptions loggingOptions = new LoggingOptions(config);
    BaseServerOptions serverOptions = new BaseServerOptions(config);
    URI uri = serverOptions.getExternalUri();
    SecretOptions secretOptions = new SecretOptions(config);
    NodeOptions nodeOptions = new NodeOptions(config);
    Duration sessionTimeout = nodeOptions.getSessionTimeout();

    // Refer to the foot notes for additional context on this line.
    Node node = LocalNodeFactory.create(config);

    DecoratedLoggingNode wrapper = new DecoratedLoggingNode(loggingOptions.getTracer(),
        node.getId(),
        uri,
        secretOptions.getRegistrationSecret(),
        sessionTimeout);
    wrapper.node = node;
    return wrapper;
  }

  @Override
  public Either<WebDriverException, CreateSessionResponse> newSession(
      CreateSessionRequest sessionRequest) {
    return perform(() -> node.newSession(sessionRequest), "newSession");
  }

  @Override
  public HttpResponse executeWebDriverCommand(HttpRequest req) {
    return perform(() -> node.executeWebDriverCommand(req), "executeWebDriverCommand");
  }

  @Override
  public Session getSession(SessionId id) throws NoSuchSessionException {
    return perform(() -> node.getSession(id), "getSession");
  }

  @Override
  public HttpResponse uploadFile(HttpRequest req, SessionId id) {
    return perform(() -> node.uploadFile(req, id), "uploadFile");
  }

  @Override
  public HttpResponse downloadFile(HttpRequest req, SessionId id) {
    return perform(() -> node.downloadFile(req, id), "downloadFile");
  }

  @Override
  public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) {
    return perform(() -> {
      try {
        return node.getDownloadsFilesystem(uuid);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }, "downloadsFilesystem");
  }

  @Override
  public TemporaryFilesystem getUploadsFilesystem(SessionId id) throws IOException {
    return perform(() -> {
      try {
        return node.getUploadsFilesystem(id);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }, "uploadsFilesystem");

  }

  @Override
  public void stop(SessionId id) throws NoSuchSessionException {
    perform(() -> node.stop(id), "stop");
  }

  @Override
  public boolean isSessionOwner(SessionId id) {
    return perform(() -> node.isSessionOwner(id), "isSessionOwner");
  }

  @Override
  public boolean isSupporting(Capabilities capabilities) {
    return perform(() -> node.isSupporting(capabilities), "isSupporting");
  }

  @Override
  public NodeStatus getStatus() {
    return perform(() -> node.getStatus(), "getStatus");
  }

  @Override
  public HealthCheck getHealthCheck() {
    return perform(() -> node.getHealthCheck(), "getHealthCheck");
  }

  @Override
  public void drain() {
    perform(() -> node.drain(), "drain");
  }

  @Override
  public boolean isReady() {
    return perform(() -> node.isReady(), "isReady");
  }

  private void perform(Runnable function, String operation) {
    try {
      System.err.printf("[COMMENTATOR] Before %s()%n", operation);
      function.run();
    } finally {
      System.err.printf("[COMMENTATOR] After %s()%n", operation);
    }
  }

  private <T> T perform(Supplier<T> function, String operation) {
    try {
      System.err.printf("[COMMENTATOR] Before %s()%n", operation);
      return function.get();
    } finally {
      System.err.printf("[COMMENTATOR] After %s()%n", operation);
    }
  }
}

註腳

在上述範例中,程式碼 `Node node = LocalNodeFactory.create(config);` 明確地建立了一個 `LocalNode`。

基本上有 2 種可用的 `org.openqa.selenium.grid.node.Node` *使用者介面實作* 類型。

這些類別是學習如何建置自訂節點以及學習節點內部原理的良好起點。

  • org.openqa.selenium.grid.node.local.LocalNode - 用於表示長時間執行的節點,並且是當您啟動 `node` 時連結的預設實作。
    • 可以透過呼叫 `LocalNodeFactory.create(config);` 來建立,其中
      • LocalNodeFactory 屬於 org.openqa.selenium.grid.node.local
      • Config 屬於 org.openqa.selenium.grid.config
  • org.openqa.selenium.grid.node.k8s.OneShotNode - 這是一個特殊的參考實作,其中節點在服務完一個測試會話後會優雅地自行關閉。此類別目前不包含在任何預先建置的 maven 構件中。
    • 您可以參考 此處 的原始碼以了解其內部原理。
    • 若要在本機建置它,請參考 此處
    • 可以透過呼叫 `OneShotNode.create(config)` 來建立,其中
      • OneShotNode 屬於 org.openqa.selenium.grid.node.k8s
      • Config 屬於 org.openqa.selenium.grid.config