自定义节点

如何自定义节点

有时我们希望根据自己的需要自定义节点。

例如,我们可能希望在会话开始执行之前进行一些额外的设置,并在会话运行完成后进行一些清理。

可以按照以下步骤进行此操作

  • 创建一个扩展 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
  • 在此工厂方法中,包含创建新类的逻辑。

  • 要将此新的自定义逻辑连接到中心,请启动节点并将上述类的完全限定类名传递给参数 --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 插件在合并多个服务提供程序接口文件 (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