DevOps 与监控观测

Vert.x Shell

Vert.x Shell 是面向 Vert.x 运行时的命令行界面(CLI),支持通过不同协议从常规 Terminal 进行访问。

Vert.x Shell 提供了丰富的 Command,用于与 Vert.x 服务进行实时交互。

Vert.x Shell 支持使用 Vert.x 所支持的任意语言编写自定义 Command 进行扩展。

使用 Vert.x Shell

Vert.x Shell 是一项 Vert.x 服务,既可以通过 ShellService 以编程方式启动,也可以作为服务部署。

Shell 服务

Shell 可以直接从命令行或作为 Vert.x 部署启动为服务:

启动支持通过 Telnet 访问的 Shell 服务
vertx run -conf '{"telnetOptions":{"port":5000}}' maven:io.vertx:vertx-shell:5.2.1

或者

启动支持通过 SSH 访问的 Shell 服务
# 为 SSH 服务端创建密钥对
keytool -genkey -keyalg RSA -keystore ssh.jks -keysize 2048 -validity 1095 -dname CN=localhost -keypass secret -storepass secret
# 创建认证配置
echo user.admin=password > auth.properties
# 启动 Shell
vertx run -conf '{"sshOptions":{"port":4000,"keyPairOptions":{"path":"ssh.jks","password":"secret"},"authOptions":{"provider":"properties","config":{"file":"file:auth.properties"}}}}' maven:io.vertx:vertx-shell:5.2.1

或者

启动支持通过 HTTP 访问的 Shell 服务
# 为 HTTP 服务端创建证书
keytool -genkey -keyalg RSA -keystore keystore.jks -keysize 2048 -validity 1095 -dname CN=localhost -keypass secret -storepass secret
# 创建认证配置
echo user.admin=password > auth.properties
vertx run -conf '{"httpOptions":{"port":8080,"ssl":true,"keyStoreOptions":{"path":"keystore.jks","password":"secret"},"authOptions":{"provider":""properties,"config":{"file":"file:auth.properties"}}}}' maven:io.vertx:vertx-shell:5.2.1

你也可以在自己的 Verticle 内部部署该服务:

vertx
  .deployVerticle("maven:{maven-groupId}:{maven-artifactId}:{maven-version}",
    new DeploymentOptions().setConfig(
      new JsonObject().put("telnetOptions",
        new JsonObject().
          put("host", "localhost").
          put("port", 4000))
    )
  );

或者

vertx
  .deployVerticle("maven:{maven-groupId}:{maven-artifactId}:{maven-version}",
    new DeploymentOptions().setConfig(new JsonObject().
      put("sshOptions", new JsonObject().
        put("host", "localhost").
        put("port", 5000).
        put("keyPairOptions", new JsonObject().
          put("path", "src/test/resources/ssh.jks").
          put("password", "wibble")).
        put("authOptions", new JsonObject().
          put("provider", "properties").
          put("config", new JsonObject().
            put("file", "file:/path/to/my/auth.properties"))))
    )
  );

或者

vertx
  .deployVerticle("maven:{maven-groupId}:{maven-artifactId}:{maven-version}",
    new DeploymentOptions().setConfig(new JsonObject().
      put("httpOptions", new JsonObject().
        put("host", "localhost").
        put("port", 8080).
        put("ssl", true).
        put("keyPairOptions", new JsonObject().
          put("path", "src/test/resources/server-keystore.jks").
          put("password", "wibble")).
        put("authOptions", new JsonObject().
          put("provider", "properties").
          put("config", new JsonObject().
            put("file", "file:/path/to/my/auth.properties"))))
    )
  );
当 Vert.x Shell 已经在你的类路径(Classpath)中时,你可以使用 service:io.vertx.ext.shell 代替 maven:io.vertx:vertx-shell:5.2.1。

编程式服务

ShellService 负责启动 Vert.x Shell 的实例。

启动支持通过 SSH 访问的 Shell 服务:

ShellService service = ShellService.create(vertx,
  new ShellServiceOptions().setSSHOptions(
    new SSHTermOptions().
      setHost("localhost").
      setPort(5000).
      setKeyPairOptions(new JksOptions().
        setPath("server-keystore.jks").
        setPassword("wibble")
      ).
      setAuthOptions(
        new JsonObject()
          .put("provider", "properties")
          .put("type", "PROPERTIES")
          .put("config", new JsonObject().
            put("properties_path", "file:/path/to/my/auth.properties"))
      )
  )
);
service.start();

启动支持通过 Telnet 访问的 Shell 服务:

ShellService service = ShellService.create(vertx,
  new ShellServiceOptions().setTelnetOptions(
    new TelnetTermOptions().
      setHost("localhost").
      setPort(4000)
  )
);
service.start();

TelnetTermOptions 继承了 Vert.x Core 的 NetServerOptions,因为 Telnet 服务端实现是基于 NetServer 的。

Telnet 完全不提供任何认证和加密。

启动支持通过 HTTP 访问的 Shell 服务:

ShellService service = ShellService.create(vertx,
  new ShellServiceOptions().setHttpOptions(
    new HttpTermOptions().
      setHost("localhost").
      setPort(8080)
  )
);
service.start();

认证

SSH 与 HTTP 连接器均基于 vertx-auth 提供了认证支持,支持以下 Provider:

  • properties:提供 .properties 后端

  • ldap:提供 LDAP 后端

  • mongo:MongoDB 后端

这些选项可以直接使用对应的类来创建:

对于 JSON 格式的外部服务配置,authOptions 使用 provider 属性进行区分:

{
  ...
  "authOptions": {
    "provider":"properties",
    "config": {
      "file":"file:auth.properties"
    }
  }
  ...
}

Telnet Term 配置

Telnet Term 通过 setTelnetOptions 进行配置。TelnetTermOptions 继承了 NetServerOptions,因此它们具备完全相同的配置项。

SSH Term 配置

SSH Term 通过 setSSHOptions 进行配置:

目前仅支持用户名/密码认证,可通过属性文件或 LDAP 进行配置,详见 Vert.x Auth 文档:

服务端密钥配置复用了 Vert.x Core 提供的密钥对存储配置方案:

在 SSH 上使用 Mongo 认证部署 Shell 服务
vertx
  .deployVerticle("maven:{maven-groupId}:{maven-artifactId}:{maven-version}",
    new DeploymentOptions().setConfig(new JsonObject().
      put("sshOptions", new JsonObject().
        put("host", "localhost").
        put("port", 5000).
        put("keyPairOptions", new JsonObject().
          put("path", "src/test/resources/ssh.jks").
          put("password", "wibble")).
        put("authOptions", new JsonObject().
          put("provider", "mongo").
          put("config", new JsonObject().
            put("connection_string", "mongodb://localhost:27018"))))
    )
  );
在 SSH 上使用 Mongo 认证运行 Shell 服务
ShellService service = ShellService.create(vertx,
  new ShellServiceOptions().setSSHOptions(
    new SSHTermOptions().
      setHost("localhost").
      setPort(5000).
      setKeyPairOptions(new JksOptions().
        setPath("server-keystore.jks").
        setPassword("wibble")
      ).
      setAuthOptions(new JsonObject()
        .put("provider", "mongo")
        .put("config", new JsonObject()
          .put("connection_string", "mongodb://localhost:27018"))
      )
  )
);
service.start();

HTTP Term 配置

HTTP Term 通过 setHttpOptions 进行配置。HTTP 选项继承了 HttpServerOptions,因此它们暴露了完全相同的配置。

此外,还有一些用于配置 HTTP Term 的额外选项:

在 HTTP 上使用 Mongo 认证部署 Shell 服务
vertx
  .deployVerticle("maven:{maven-groupId}:{maven-artifactId}:{maven-version}",
    new DeploymentOptions().setConfig(new JsonObject().
      put("httpOptions", new JsonObject().
        put("host", "localhost").
        put("port", 8080).
        put("ssl", true).
        put("keyPairOptions", new JsonObject().
          put("path", "src/test/resources/server-keystore.jks").
          put("password", "wibble")).
        put("authOptions", new JsonObject().
          put("provider", "mongo").
          put("config", new JsonObject().
            put("connection_string", "mongodb://localhost:27018"))))
    )
  );
在 HTTP 上使用 Mongo 认证运行 Shell 服务
ShellService service = ShellService.create(vertx,
  new ShellServiceOptions().setHttpOptions(
    new HttpTermOptions().
      setHost("localhost").
      setPort(8080).
      setAuthOptions(new JsonObject()
        .put("provider", "mongo")
        .put("config", new JsonObject()
          .put("connection_string", "mongodb://localhost:27018"))
      )
  )
);
service.start();

键位映射(Keymap)配置

Shell 使用默认的键位映射配置,可以通过各类 Term 配置对象的 inputrc 属性进行覆盖:

inputrc 必须指向可通过类加载器(ClassLoader)或文件系统访问的文件。

inputrc 仅包含函数绑定,可用函数如下:

  • backward-char

  • forward-char

  • next-history

  • previous-history

  • backward-delete-char

  • backward-delete-char

  • backward-word

  • end-of-line

  • beginning-of-line

  • delete-char

  • delete-char

  • complete

  • accept-line

  • accept-line

  • kill-line

  • backward-word

  • forward-word

  • backward-kill-word

你也可以添加自定义函数,但这需要实现 Vert.x Shell 所依赖的 Term.d 项目中的函数。例如,可以实现 反转函数(Reverse Function),然后在 META-INF/services/io.termd.core.readline.Function 中声明,以便由 Shell 加载。

基础 Command

要查看可用 Command,你可以使用内置的 help Command:

  1. Verticle 交互 Command

    1. verticle-ls:列出所有已部署的 Verticle

    2. verticle-undeploy:撤回已部署的 Verticle

    3. verticle-deploy:使用以 JSON 字符串表示的部署选项部署 Verticle

    4. verticle-factories:列出所有已知的 Verticle Factory

  2. 文件系统 Command

    1. ls

    2. cd

    3. pwd

  3. Event Bus Command

    1. bus-tail:显示 Event Bus 某个地址上接收到的所有入站消息

    2. bus-send:向 Event Bus 发送消息

  4. 网络 Command

    1. net-ls:列出所有可用的网络服务端,包括 HTTP 服务端

  5. 共享数据 Command

    1. local-map-put

    2. local-map-get

    3. local-map-rm

  6. 常用杂项 Command

    1. echo

    2. sleep

    3. help

    4. exit

    5. logout

  7. 作业控制(Job Control)

    1. fg

    2. bg

    3. jobs

该 Command 列表在后续 Vert.x Shell 版本中可能会继续扩充。其他 Vert.x 项目也可能提供 Command 来扩展 Vert.x Shell,例如 Dropwizard Metrics。

扩展 Vert.x Shell

Vert.x Shell 支持使用任何支持代码生成的语言编写自定义 Command 进行扩展。

Command 通过 CommandBuilder.command 方法创建:当 Command 执行时,Shell 会调用该 Command 的进程处理器(Process Handler)。该处理器可以通过 processHandler 方法进行设置:

CommandBuilder builder = CommandBuilder.command("my-command");
builder.processHandler(process -> {

  // 向控制台输出消息
  process.write("Hello World");

  // 结束该进程
  process.end();
});

// 注册 Command
CommandRegistry registry = CommandRegistry.getShared(vertx);
registry.registerCommand(builder.build(vertx));

创建 Command 后,需要将其注册到 CommandRegistry 中。Command 注册中心持有该 Vert.x 实例中的所有 Command。

Command 一旦注册后将一直保持有效,直到调用 unregisterCommand 将其注销。若 Command 是在某个 Verticle 中注册的,则当该 Verticle 被撤回(Undeploy)时,该 Command 会被自动注销。

Command 的回调将在该 Command 向注册中心注册时的 Context 中被调用。如果你在 Command 中维护状态,请牢记这一点。

CommandProcess 对象可用于与 Shell 进行交互。

Command 参数

args 返回 Command 的参数列表:

command.processHandler(process -> {

  for (String arg : process.args()) {
    // 在控制台输出每个参数
    process.write("Argument " + arg);
  }

  process.end();
});

此外,还可以使用 Vert.x CLI 创建 Command,这使得编写命令行参数解析更加容易:

  • 选项(Option)与参数(Argument)解析

  • 参数校验(Validation)

  • 自动生成 Command 用法说明(Usage)

CLI cli = CLI.create("my-command").
  addArgument(new Argument().setArgName("my-arg")).
  addOption(new Option().setShortName("m").setLongName("my-option"));
CommandBuilder command = CommandBuilder.command(cli);
command.processHandler(process -> {

  CommandLine commandLine = process.commandLine();

  String argValue = commandLine.getArgumentValue(0);
  String optValue = commandLine.getOptionValue("my-option");
  process
    .write("The argument is " + argValue + " and the option is " + optValue);

  process.end();
});

当向 CLI 对象添加名为 help 的选项时,一旦触发该选项,Shell 将自动生成 Command 的用法说明:

CLI cli = CLI.create("my-command").
  addArgument(new Argument().setArgName("my-arg")).
  addOption(new Option()
    .setArgName("help")
    .setShortName("h")
    .setLongName("help"));

CommandBuilder command = CommandBuilder.command(cli);
command.processHandler(process -> {
  // ...
});

当 Command 执行时,会提供 process 用于与 Shell 交互。CommandProcess 继承自 Tty,后者用于与 Terminal 进行交互。

Terminal 使用说明

Terminal I/O

stdinHandler 处理器用于在 Terminal 接收到数据(例如用户在键盘上键入内容)时收到通知:

tty.stdinHandler(data -> System.out.println("Received " + data));

Command 可以使用 write 向标准输出写入内容。

tty.write("Hello World");

Terminal 尺寸

可以通过 width 和 height 获取当前 Terminal 的尺寸。

tty.write("Current terminal size: (" + tty.width() + ", " + tty.height() + ")");

尺寸调整事件(Resize Event)

当 Terminal 的尺寸发生变化时,会调用 resizehandler,新的 Terminal 尺寸可通过 width 和 height 获取。

tty.resizehandler(v ->
  System.out.println("terminal resized : " + tty.width() + " " + tty.height()));

Terminal 类型

Terminal 类型在向远程 Terminal 发送转义码(Escape Code)时非常有用:type 返回当前 Terminal 类型;如果 Terminal 未通告该值,则可能为 null。

System.out.println("terminal type : " + tty.type());

Shell Session

Shell 是一项面向连接的服务,会自然地维护与客户端之间的 Session。该 Session 可在 Command 中用于限制数据作用域。Command 可以通过 session 获取 Session:

command.processHandler(process -> {

  Session session = process.session();

  if (session.get("my_key") == null) {
    session.put("my key", "my value");
  }

  process.end();
});

进程终止

调用 end 会结束当前进程。它既可以在 Command 处理器调用中直接执行,也可以在之后的任意时刻调用:

command.processHandler(process -> {
  Vertx vertx = process.vertx();

  // 设置定时器
  vertx.setTimer(1000, id -> {

    // 当定时器触发时结束 Command
    process.end();
  });
});

进程事件

Command 可以订阅几种进程事件。

中断事件(Interrupt Event)

当进程被中断时会调用 interruptHandler。该事件在 Command 执行期间用户按下 Ctrl+C 时触发。该处理器可用于中断阻塞 CLI 的 Command,并优雅地结束 Command 进程:

command.processHandler(process -> {
  Vertx vertx = process.vertx();

  // 每秒在控制台打印一条消息
  long periodicId = vertx.setPeriodic(1000, id -> process.write("tick\n"));

  // 当用户按下 Ctrl+C 时:取消定时器并结束进程
  process.interruptHandler(v -> {
    vertx.cancelTimer(periodicId);
    process.end();
  });
});

如果未注册中断处理器,按下 Ctrl+C 对当前进程不会产生影响,该事件会被推迟并很可能由 Shell 自身处理(例如在控制台上换行)。

挂起/恢复事件(Suspend/Resume Event)

当进程正在运行且用户按下 Ctrl+Z 时,会调用 suspendHandler,此时 Command 会被挂起(suspended):

  • 若 Command 已注册了该事件的处理器,则可以接收挂起事件

  • Command 将不再接收来自标准输入的任何数据

  • Shell 将提示用户输入

  • Command 仍然可以接收中断事件或结束事件

当进程被恢复(通常是用户输入 fg)时,会调用 resumeHandler:

  • 若 Command 已注册了该事件的处理器,则可以接收恢复事件

  • 若 Command 已注册了 stdin 处理器,则将重新开始接收来自标准输入的数据

command.processHandler(process -> {

  // Command 被挂起
  process.suspendHandler(v -> System.out.println("Suspended"));

  // Command 被恢复
  process.resumeHandler(v -> System.out.println("Resumed"));
});

结束事件(End Event)

当进程处于运行或挂起状态且 Command 终止时(例如 Shell Session 关闭,Command 被终止),会调用 endHandler。

command.processHandler(process -> {

  // Command 终止
  process.endHandler(v -> System.out.println("Terminated"));
});

即使 Command 主动调用了 end,结束处理器也会被调用。

该处理器在 Command 终止时清理资源非常有用,例如关闭客户端或取消定时器。

Command 补全

当 Command 希望提供上下文相关的命令行界面补全时,可以提供一个补全处理器(Completion Handler)。

与进程处理器类似,completion handler 是非阻塞的,因为具体实现可能会用到 Vert.x 服务(例如文件系统)。

lineTokens 返回从行首到光标位置的 Token 列表。如果光标位于行首,则列表可能为空。

rawLine 以原始格式(即未执行任何字符转义)返回从行首到光标位置的当前已输入内容。

补全流程以调用 complete 结束。

Shell Server

Shell 服务是一个便捷的门面(Facade),用于以编程方式或作为 Vert.x 服务启动预先配置好的 Shell。当需要更高灵活性时,可以使用 ShellServer 代替该服务。

例如,可以将 Shell HTTP Term 配置为复用现有的 Router,而不是自行启动独立的 HTTP 服务端。

使用 Shell Server 需要进行显式配置,但能提供充分的灵活性。只需几个步骤即可完成 Shell Server 的搭建:

ShellServer server = ShellServer.create(vertx); (1)

Router shellRouter = Router.router(vertx); (2)
router.route("/shell/*").subRouter(shellRouter);
TermServer httpTermServer = TermServer.createHttpTermServer(vertx, router);

TermServer sshTermServer = TermServer.createSSHTermServer(vertx); (3)

server.registerTermServer(httpTermServer); (4)
server.registerTermServer(sshTermServer);

server.registerCommandResolver(CommandResolver.baseCommands(vertx)); (5)

server.listen(); (6)
1 创建 Shell Server
2 创建挂载在既有 Router 上的 HTTP Term Server
3 创建 SSH Term Server
4 注册 Term Server
5 注册所有基础 Command
6 最后启动 Shell Server

此外,Shell Server 还可用于创建进程内 Shell Session:它提供了一个编程式的交互 Shell。

可以通过 createShell 创建进程内 Shell Session:

Shell shell = shellServer.createShell();

主要使用场景是运行或测试某个 Command:

Shell shell = shellServer.createShell();

// 为 Command 创建 Job
Job job = shell.createJob("my-command 1234");

// 创建伪终端(Pseudo Terminal)
Pty pty = Pty.create();
pty.stdoutHandler(data -> System.out.println("Command wrote " + data));

// 运行 Command
job.setTty(pty.slave());
job.statusUpdateHandler(status ->
  System.out.println("Command terminated with status " + status));

Pty 伪终端是 Command 运行时与之交互的主要接口:

  • 使用标准输入/输出写入或读取字符串

  • 调整 Terminal 尺寸

close 会关闭 Shell,并终止当前 Shell Session 中的所有 Job。

Terminal Server

Vert.x Shell 还为需要编写纯终端应用的用户提供了裸 Terminal Server。

在启动 Term Server 之前,必须先为其设置 Term 处理器。当用户建立连接时,该处理器将负责处理每个 Term。

可以在 SSHTermOptions 和 HttpTermOptions 上设置 Auth*Options。或者,在启动 Term Server 之前,也可以直接在 Term Server 上 设置 一个 AuthenticationProvider。

SSH Term

Terminal Server 的 Term 处理器负责接收外来 Terminal 连接。当远程 Terminal 连接时,可以使用 Term 与已连接的 Terminal 进行交互。

TermServer server = TermServer
  .createSSHTermServer(vertx,
    new SSHTermOptions().setPort(5000).setHost("localhost"));

server.termHandler(term -> term.stdinHandler(term::write));
server.listen();

Term 本身也是一个 Tty,本节将说明如何使用该 TTY。

Telnet Term

TermServer server = TermServer
  .createTelnetTermServer(vertx,
    new TelnetTermOptions().setPort(5000).setHost("localhost"));

server.termHandler(term -> term.stdinHandler(term::write));
server.listen();

HTTP Term

TermServer.createHttpTermServer 方法用于创建基于 Vert.x Web 并使用 SockJS 协议的 HTTP Term Server。

TermServer server = TermServer
  .createHttpTermServer(vertx,
    new HttpTermOptions().setPort(5000).setHost("localhost"));

server.termHandler(term -> term.stdinHandler(term::write));
server.listen();

HTTP Term 既可以启动自己独立的 HTTP 服务端,也可以复用现有的 Vert.x Web Router。

可以在 /shell.html 访问该 Shell。

TermServer server = TermServer
  .createHttpTermServer(vertx,
    router,
    new HttpTermOptions().setPort(5000).setHost("localhost"));

server.termHandler(term -> term.stdinHandler(term::write));
server.listen();

后一种方式在需要将 HTTP Shell 集成到既有 HTTP 服务端中时非常方便。

默认情况下,HTTP Term Server 配置为提供以下静态内容服务:

  • shell.html 页面

  • term.js 客户端库

  • vertxshell.js 客户端库

vertxshell.js 集成了 term.js,是 HTTP Term 的客户端部分。

它将 term.js 与 SockJS 进行集成,需要传入 HTTP Term Server 端点的 URL:

window.addEventListener('load', function () {
  var url = 'http://localhost/shell';
  new VertxTerm(url, {
    cols: 80,
    rows: 24
   });
 });

也可以直接使用纯 WebSocket。如果使用纯 WebSocket,远程 Term 的 URL 应添加 /websocket 后缀:

window.addEventListener('load', function () {
  var url = 'ws://localhost/shell/websocket';
  new VertxTerm(url, {
    cols: 80,
    rows: 24
   });
 });

为了满足定制需求,可以复制并修改这些静态资源,它们位于 Vert.x Shell 的 JAR 包中的 io.vertx.ext.shell 包路径下。

Command 发现

当需要向 Vert.x 添加新 Command 而不进行显式注册时,可以使用 Command 发现机制。

例如,Dropwizard 度量服务会在运行时动态向 Shell 服务添加专用的度量 Command。

这可以通过基于 java.util.ServiceLoader 加载 CommandResolverFactory 来实现。

public class CustomCommands implements CommandResolverFactory {

  public void resolver(Vertx vertx, Handler<AsyncResult<CommandResolver>> resolverHandler) {
    resolverHandler.handler(() -> Arrays.asList(myCommand1, myCommand2));
  }
}

resolver 方法是异步的,因为解析器在解析 Command 前可能需要等待某些条件满足。

使用 ServiceLoader 机制的 Shell 服务发现配置如下:

服务提供商配置文件 META-INF/services/io.vertx.ext.shell.spi.CommandResolverFactory
my.CustomCommands

这仅对 ShellService 有效。ShellServer 不使用此机制。

Command Pack(命令包)

Command Pack 是一个提供新 Vert.x Shell Command 的 JAR 包。

此类 JAR 包只需存在于类路径中,即可被 Vert.x Shell 自动发现。

public class CommandPackExample implements CommandResolverFactory {

  @Override
  public void resolver(Vertx vertx, Handler<AsyncResult<CommandResolver>> resolveHandler) {
    List<Command> commands = new ArrayList<>();

    // 添加 Command
    commands.add(Command.create(vertx, JavaCommandExample.class));

    // 添加另一个 Command
    commands.add(CommandBuilder.command("another-command").processHandler(process -> {
      // 处理进程
    }).build(vertx));

    // 将解析出的 Command 列表传递给处理器
    resolveHandler.handle(Future.succeededFuture(() -> commands));
  }
}

Command Pack 使用了 Command 发现机制,因此需要提供以下描述文件:

META-INF/services/io.vertx.ext.shell.spi.CommandResolverFactory 描述符
examples.pack.CommandPackExample