• 请不要在回答技术问题时复制粘贴 AI 生成的内容
LonnyWong
V2EX  ›  程序员

简单分享一下 Warp 终端 blocks feature(分块)的实现原理

  •  1
     
  •   LonnyWong · Dec 14, 2023 · 3609 views
    This topic created in 1020 days ago, the information mentioned may be changed or developed.

    Why

    背景:我开源了一个 ssh 客户端,叫 trzsz-ssh ( tssh ),定制了一些网友需要的功能,解决了一些 ssh 相关的痛点,具体详看开源地址:https://github.com/trzsz/trzsz-ssh

    起因:在 Warp 终端中,为什么原生的 ssh 客户端就可以支持 blocks feature,而我自己写的 tssh 客户端就不行呢?于是我一步步地深挖了其实现原理。

    What

    在 Warp 终端,当你 ssh 登录到服务器上,默认情况下,你在服务器上执行的每条命令以及其输出就会被 Warp 分别定义成一个个 block 块,你可以一块块地选中和移动,非常的酷。如果不支持,那整个 ssh 登录后的所有命令及输出就会被 Warp 定义成同一个 block 块,选中和移动都是整个登录后的所有命令及其输出,那就没那么酷了。

    另外,当你在服务器上输入命令按 tab 键时,Warp 终端会弹出一个浮层显示可选的目录或文件,也很帅。如果不支持,那 tab 键也不能正常地进行补全了,这对我来说简直不能忍。

    How

    言归正传,Warp 终端是怎么实现 blocks feature 和自定义 tab 行为等功能的呢?

    在 Wrap 终端中,内置了一些 shell 函数,bash 可以通过 type 函数名 进行查看函数定义,zsh 可以通过 which 函数名 进行查看函数定义。

    • Warp 定义了个 ssh 函数

      在 Warp 中执行 ssh xxx 登录服务器,实际是执行同名的 ssh 函数,其定义如下:

      ssh ()
      {
          if is_interactive_ssh_session "$@"; then
              warp_send_json_message "{\"hook\": \"PreInteractiveSSHSession\", \"value\": {}}";
              if [ "$WARP_USE_SSH_WRAPPER" = "1" ]; then
                  local TRACE_FLAG_IF_WARP_DEBUG_MODE="";
                  if [[ "$WARP_DEBUG_MODE" == "1" ]]; then
                      TRACE_FLAG_IF_WARP_DEBUG_MODE="-x";
                  fi;
                  warp_ssh_helper "$@";
              else
                  command ssh "$@";
              fi;
          else
              command ssh "$@";
          fi
      }
      
      • 通过 is_interactive_ssh_session 函数判断是否为交互式的 ssh 登录。
      • 若不是交互式的 ssh 登录,则直接调用原生的 ssh 命令 command ssh "$@"。
      • 若是交互式的 ssh 登录,则调用 warp_send_json_message 函数,输出一串用户看不见的 json ,Warp 可能会做一些统计之类。
      • 若 WARP_USE_SSH_WRAPPER 环境变量不是 1,则直接调用原生的 ssh 命令 command ssh "$@"。默认是 1 的。
      • 调试相关的 TRACE_FLAG_IF_WARP_DEBUG_MODE 和 WARP_DEBUG_MODE 可以忽略,默认是不调试的。
      • 核心逻辑在 warp_ssh_helper 函数中实现 warp_ssh_helper "$@",下文再详细介绍。
    • 判断是否为交互式的 ssh 登录

      在 Warp 中通过 is_interactive_ssh_session 函数判断是否为交互式 ssh 登录,其定义如下:

      is_interactive_ssh_session ()
      {
          ARGS=();
          while [ $# -gt 0 ]; do
              OPTIND=1;
              while getopts :1246AaCfgKkMNnqsTtVvXxYyb:c:D:e:F:i:L:l:m:O:o:p:R:S:W:w: OPTION; do
                  case $OPTION in
                      T)
                          return 1
                      ;;
                      W)
                          return 1
                      ;;
                      \?)
                          return 1
                      ;;
                      :)
                          return 1
                      ;;
                  esac;
              done;
              [ $? -eq 0 ] || return 2;
              [ $OPTIND -gt $# ] && break;
              shift "$((OPTIND - 1))";
              ARGS[${#ARGS[@]}]=$1;
              shift;
          done;
          if [[ ${#ARGS[@]} -ne 1 ]]; then
              return 1;
          fi
      }
      
      • 判断 ssh 命令中是否含有 -T、-W 等选项,若有则说明不是交互式的,直接返回 1( 非交互 )。

      • 判断 ssh 命令中是否带有目标机器 [[ ${#ARGS[@]} -ne 1 ]],若没有目标机器,也认为不是交互式的,返回 1( 非交互 )。

      • trzsz ssh ( tssh ) 支持不带参数运行,会列出所有服务器的列表,支持搜索和选择进行登录,这里需要调整才能支持 blocks feature:

        # 注意里面的 `command` 关键字,若没有它,就会循环调用 `ssh` 函数,而不是执行 `ssh` 命令了。不要问我怎么知道的。
        if [[ ${#ARGS[@]} -ne 1 ]] && [[ $(command ssh -V 2>&1) != "trzsz ssh"* ]]; then
            return 1;
        fi
        
    • 输出一段用户看不见的 json 内容

      在 Warp 中通过 warp_send_json_message 输出一段用户看不见的 json 内容,这是 Warp 的内部逻辑,可以忽略,实测不输出也不影响的,其定义如下:

      warp_send_json_message ()
      {
          encoded_message=$(warp_hex_encode_string "$1");
          printf $DCS_START$DCS_JSON_MARKER$encoded_message$DCS_END
      }
      
      • 其实就是先进行 hex 编码,然后加上 \x1bP$d 开头,加上 \x9c 结尾,最终输出的内容如下:
      00000000: 1b50 2464 3762 3232 3638 3666 3666 3662  .P$d7b22686f6f6b
      00000010: 3232 3361 3230 3232 3530 3732 3635 3439  223a202250726549
      00000020: 3665 3734 3635 3732 3631 3633 3734 3639  6e74657261637469
      00000030: 3736 3635 3533 3533 3438 3533 3635 3733  7665535348536573
      00000040: 3733 3639 3666 3665 3232 3263 3230 3232  73696f6e222c2022
      00000050: 3736 3631 3663 3735 3635 3232 3361 3230  76616c7565223a20
      00000060: 3762 3764 3764 3061 9c                   7b7d7d0a.
      
    • 核心逻辑 warp_ssh_helper 函数

      在 Warp 中通过 warp_ssh_helper 函数实现 blocks feature 和 tab 补全等功能,其定义如下:

      warp_ssh_helper ()
      {
          init_shell_bash=$(init_shell_hook "bash");
          init_shell_zsh=$(init_shell_hook "zsh");
          local zsh_env_script=$(printf '%s' '...太长省略系列...');
          command ssh -o ControlMaster=yes -o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID -t "${@:1}" "
              # ...太长省略系列...
          "
      }
      
      • 前面 init_shell_bash、init_shell_zsh 和 zsh_env_script 先忽略,不是本文重点,重点是 command ssh ... 那行。
      • 通过 -o ControlMaster=yes 启用了 ssh 多路复用,Warp 就可以通过同一个连接,在服务器上执行命令,获取当前目录下有哪些文件等,tab 相关功能就是靠这实现的。
      • 通过 -o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID 指定多路复用的 socket 路径,是长 ~/.ssh/170252756912781 这样子的。
      • 通过 -t 选项强制分配一个伪终端,因为后面指定了登录后要初始化执行的脚本,没有 -t 选项就会默认禁止分配伪终端,就影响用户使用了。
      • 参数 "${@:1}" 就是要登录的目标机器,从前面 ssh 命令行传递过来的。
      • 最后这一大段脚本,就是登录后要初始化执行的,下文再详细介绍。这里要改成用 -o RemoteCommand 实现,才能兼容 trzsz ssh ( tssh ) 的搜索模式。
    • 在服务器执行的初始化脚本

      前面说到,在 Warp 中 ssh 登录到服务器之后,会执行一大段脚本,以 bash 为例:

      export TERM_PROGRAM='WarpTerminal'
      hook="'$(printf "{\"hook\": \"SSH\", \"value\": {\"socket_path\": \"'$SSH_SOCKET_DIR/$WARP_SESSION_ID'\", \"remote_shell\": \"%s\"}}" "${SHELL##*/}" | command -p od -An -v -tx1 | command -p tr -d " \n")'"
      printf '$DCS_START$DCS_JSON_MARKER%s$DCS_END' "'$hook'"
      # ...此处省略对 shell 类型的判断...
      exec -a bash bash --rcfile <(echo '"'
          command -p stty raw
          HISTCONTROL=ignorespace
          HISTIGNORE=" *"
          WARP_SESSION_ID="$(command -p date +%s)$RANDOM"
          _hostname=$(command -pv hostname >/dev/null 2>&1 && command -p hostname 2>/dev/null || command -p uname -n)
          _user=$(command -v whoami >/dev/null 2>&1 && command whoami 2>/dev/null || echo $USER)
          _msg=$(printf "{\"hook\": \"InitShell\", \"value\": {\"session_id\": $WARP_SESSION_ID, \"shell\": \"bash\", \"user\": \"$_user\", \"hostname\": \"$_hostname\"}}" | command -p od -An -v -tx1 | command -p tr -d " \n")'"
          printf '\''"'\eP$d%s\x9c'"'\'' \""'$_msg'"\"')
      unset _hostname _user _msg
      
      • 其实就是通过 shell 获取一些信息,然后通过 Device Control String 进行输出,用户看不见,但是 Warp 可以解释并获取到。
      • Warp 获取到这些信息之后,就会生成另一段脚本,(模拟用户输入)直接发送到服务器执行,修改一些 shell 的设置等,从而感知到每一个命令,实现 blocks feature 等。
      • 由于篇幅和时间关系,先介绍到这。是不是很简单?你学会了吗?欢迎留言评论。

    Btw

    我给 Warp 提了个 feature request https://github.com/warpdotdev/Warp/issues/3960,解决 tssh xxx 直接登录可以支持 blocks feature , 而 tssh 搜索和选择服务器登录却不支持 的问题。有需要的朋友去帮忙点个赞,提高下优先级。

    附在 Warp 中正确安装和使用 trzsz ssh ( tssh ) https://github.com/trzsz/trzsz-ssh 的方法:

    # Install
    brew install trzsz-ssh
    sudo ln -sv $(which tssh) /usr/local/bin/ssh
    
    # Usage
    ssh xxx
    
    5 replies  •  2023-12-14 18:39:25 +08:00
    Sligcm
        1
    Sligcm  
       Dec 14, 2023
    牛的。trzsz-ssh 也很好用。
    hxy100
        2
    hxy100  
       Dec 14, 2023
    好东西
    GoodRui
        3
    GoodRui  
       Dec 14, 2023 via Android
    warp 不能正确设置本地环境变量是吧?如果遇到中文经常出现中文???的问题。
    现在主用 tssh+iterm2 ,给大佬点赞!真的好用!
    LonnyWong
        4
    LonnyWong  
    OP
       Dec 14, 2023
    @GoodRui 我也是用 iTerm2 ,不过 Warp 创造性的分块功能,值得研究下。
    xiaojun996
        5
    xiaojun996  
       Dec 14, 2023
    牛的
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   960 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 52ms · UTC 22:25 · PVG 06:25 · LAX 15:25 · JFK 18:25
    ♥ Do have faith in what you're doing.