版本
3.141.59
入口类
org.openqa.grid.selenium.GridLauncherV3
相关启动代码:
public static void main(String[] args) {
new GridLauncherV3().launch(args);
}
public Stoppable launch(String[] args) {
return Optional.ofNullable(buildLauncher(args))
.map(l -> l.launch(args))
.orElse(()->{});
}
可见,实际上的启动过程分为两步:1.用main函数的外部参数创建启动器;2.用创建好的启动器运行launch方法进行启动。
创建启动器
/**
* From the {@code args}, builds a new {@link GridItemLauncher} and populates it properly.
*
* @return null if no role is found, or a properly populated {@link GridItemLauncher}.
*/
private GridItemLauncher buildLauncher(String[] args) {
if (Arrays.asList(args).contains("-htmlSuite")) {
out.println(Joiner.on("\n").join(
"Download the Selenium HTML Runner from http://www.seleniumhq.org/download/ and",
"use that to run your HTML suite."));
return null;
}
String role = "standalone";
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("-role=")) {
role = args[i].substring("-role=".length());
} else if (args[i].equals("-role")) {
i++; // Increment, because we're going to need this.
if (i < args.length) {
role = args[i];
} else {
role = null; // Will cause us to print the usage information.
}
}
}
GridRole gridRole = GridRole.get(role);
if (gridRole == null || LAUNCHERS.get(gridRole) == null) {
printInfoAboutRoles(role);
return null;
}
return LAUNCHERS.get(gridRole);
}
这里解释下这段代码。
首先检查下-htmlSuite
参数,如果有,那么会提示你去下载Selenium HTML Runner,然后退出启动过程。
如果没有-htmlSuite
参数,接着就获取-role
参数,通过判断字符串来转为枚举中的一种,默认判断为null,会退出启动过程。简单看下能实例化成哪几种类型:
public enum GridRole {
NOT_GRID, HUB, NODE;
这三种分别代表了三种模式,即注册中心和节点合一模式、注册中心模式、节点模式。注册中心需要配合节点使用。
在返回时,返回了LAUNCHERS.get(gridRole)
,LAUNCHERS
是由org.openqa.grid.selenium.GridLauncherV3#buildLaunchers
初始化的,是一个ImmutableMap
类型的Map。Size为3,key是GridRole
的三个枚举成员,value是每个枚举对应的初始化方法。
启动
.put(GridRole.HUB, (args) -> {
GridHubCliOptions options = new GridHubCliOptions();
if (!parse(args, options, options.getCommonGridOptions().getCommonOptions())) {
return ()->{};
}
GridHubConfiguration configuration = new GridHubConfiguration(options);
configuration.setRawArgs(args); // for grid console
log.info(String.format(
"Launching Selenium Grid hub on port %s", configuration.port));
Hub hub = new Hub(configuration);
hub.start();
return hub;
})
这里介绍Hub的启动,第一个if
检查命令中是否有version或者help,有的话就执行相应的打印提示操作后退出启动过程,否则继续执行。
继续执行的是初始化配置。这里涉及到一个配置类GridHubConfiguration
。这个类会默认利用内部的DEFAULT_CONFIG_FROM_JSON
静态成员变量初始化自己其他的成员变量。但如果在启动时通过参数-hubConfig
传入配置文件,则利用传入的配置文件路径初始化某些成员变量。从该类中可知,默认配置文件在为org/openqa/grid/common/defaults/DefaultHub.json
,在jar包的相关位置可以找到。自定义配置文件可以参考文件里的写法。
接着就通过给Hub
传入配置参数,来进行启动工作:
Hub hub = new Hub(configuration);
hub.start();
new Hub
public Hub(GridHubConfiguration gridHubConfiguration) {
config = gridHubConfiguration == null ? new GridHubConfiguration() : gridHubConfiguration;
try {
registry = (GridRegistry) Class.forName(config.registry).newInstance();
registry.setHub(this);
registry.setThrowOnCapabilityNotPresent(config.throwOnCapabilityNotPresent);
} catch (Throwable e) {
throw new GridConfigurationException("Error creating class with " + config.registry +
" : " + e.getMessage(), e);
}
if (config.host == null) {
config.host = "0.0.0.0"; //default to all adapters
}
if (config.port == null) {
config.port = 4444;
}
if (config.servlets != null) {
for (String s : config.servlets) {
Class<? extends Servlet> servletClass = ExtraServletUtil.createServlet(s);
if (servletClass != null) {
String path = "/grid/admin/" + servletClass.getSimpleName() + "/*";
log.info("binding " + servletClass.getCanonicalName() + " to " + path);
addServlet(path, servletClass);
}
}
}
// start the registry, now that 'config' is all setup
registry.start();
new JMXHelper().register(this);
}
这里用配置中的registry
反射实例化一个GridRegistry
实例,并简单设置下必备参数。值得注意的是config.servlets
的相关操作,这里相当于提示我们,如果还想给Hub节点拓展更多的Api接口,是如何进行初始化相关Servlet。注意,Selenium实际上是一个由内嵌Jetty管理的Server。
hub.start()
public void start() {
initServer();
try {
server.start();
} catch (Exception e) {
try {
stop();
} catch (Exception ignore) {
}
if (e instanceof BindException) {
log.severe(String.format(
"Port %s is busy, please choose a free port for the hub and specify it using -port option", config.port));
return;
} else {
throw new RuntimeException(e);
}
}
log.info("Selenium Grid hub is up and running");
log.info(String.format("Nodes should register to %s", getRegistrationURL()));
log.info(String.format("Clients should connect to %s", getWebDriverHubRequestURL()));
}
其中org.openqa.grid.web.Hub#initServer
中初始化一个QueuedThreadPool
,这是Selenium自定义的一个用来管理任务线程的类。可以通过配置文件中的jettyMaxThreads
参数来配置其线程池大小。利用线程池初始化一个org.seleniumhq.jetty9.server.Server
实例。
这个过程中,有个比较重要的方法调用,org.openqa.grid.web.Hub#addDefaultServlets
方法中将一些路径关联org.openqa.grid.web.servlet.RegistryBasedServlet
的子类上。令Selenium Hub真正对外提供服务。
private void addDefaultServlets(ServletContextHandler handler) {
// add mandatory default servlets
handler.addServlet(RegistrationServlet.class.getName(), "/grid/register/*");
handler.addServlet(DriverServlet.class.getName(), "/wd/hub/*");
handler.addServlet(DriverServlet.class.getName(), "/selenium-server/driver/*");
handler.addServlet(ProxyStatusServlet.class.getName(), "/grid/api/proxy/*");
handler.addServlet(NodeSessionsServlet.class.getName(), "/grid/api/sessions/*");
handler.addServlet(HubStatusServlet.class.getName(), "/grid/api/hub/*");
ServletHolder statusHolder = new ServletHolder(new HubW3CStatusServlet(getRegistry()));
handler.addServlet(statusHolder, "/status");
handler.addServlet(statusHolder, "/wd/hub/status");
handler.addServlet(TestSessionStatusServlet.class.getName(), "/grid/api/testsession/*");
// add optional default servlets
if (!config.isWithOutServlet(ResourceServlet.class)) {
handler.addServlet(ResourceServlet.class.getName(), "/grid/resources/*");
}
if (!config.isWithOutServlet(DisplayHelpServlet.class)) {
handler.addServlet(DisplayHelpServlet.class.getName(), "/*");
handler.setInitParameter(DisplayHelpServlet.HELPER_TYPE_PARAMETER, config.role);
}
if (!config.isWithOutServlet(ConsoleServlet.class)) {
handler.addServlet(ConsoleServlet.class.getName(), "/grid/console/*");
handler.setInitParameter(ConsoleServlet.CONSOLE_PATH_PARAMETER, "/grid/console");
}
if (!config.isWithOutServlet(LifecycleServlet.class)) {
handler.addServlet(LifecycleServlet.class.getName(), "/lifecycle-manager/*");
}
if (!config.isWithOutServlet(Grid1HeartbeatServlet.class)) {
handler.addServlet(Grid1HeartbeatServlet.class.getName(), "/heartbeat");
}
}
相关暴露的接口可以查询官方文档或者直接查看对应类中的处理方法。自定义接口可以参考它们的实现。
server.start()
public final void start() throws Exception {
synchronized(this._lock) {
try {
if (this._state == 2 || this._state == 1) {
return;
}
this.setStarting();
this.doStart();
this.setStarted();
} catch (Throwable var4) {
this.setFailed(var4);
throw var4;
}
}
}
这里都是Jetty的启动内容。
启动完成
当控制台看到“Selenium Grid hub is up and running”输出时,整个Hub节点就启动成功。
16:41:16.705 INFO [Hub.start] - Selenium Grid hub is up and running
16:41:16.740 INFO [Hub.start] - Nodes should register to http://******:5555/grid/register/
16:41:16.773 INFO [Hub.start] - Clients should connect to http://******:5555/wd/hub
Node应向第一条Url( http://******:5555/grid/register/)注册,使用WebDriver的客户端应向第二条Url(http://******:5555/wd/hub)连接。
总结
其他两种——Standalone和Node启动逻辑类似,跟着Hub的启动逻辑也可以看到它们的启动逻辑。
总之,Selenium Grid的启动依靠传入参数以及Json文件配置来完成启动流程。并且有默认的Json配置文件。可以自定义Sevlet添加扩展接口。
这就是Selenium启动过程能告诉我们的信息。