V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LonnyWong
V2EX  ›  程序员

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

  •  1
     
  •   LonnyWong · 331 天前 · 1857 次点击
    这是一个创建于 331 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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_MODEWARP_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 featuretab 补全等功能,其定义如下:

      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_bashinit_shell_zshzsh_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 ) 的搜索模式。
    • 在服务器执行的初始化脚本

      前面说到,在 Warpssh 登录到服务器之后,会执行一大段脚本,以 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 条回复    2023-12-14 18:39:25 +08:00
    Sligcm
        1
    Sligcm  
       331 天前
    牛的。trzsz-ssh 也很好用。
    hxy100
        2
    hxy100  
       331 天前
    好东西
    GoodRui
        3
    GoodRui  
       331 天前 via Android
    warp 不能正确设置本地环境变量是吧?如果遇到中文经常出现中文???的问题。
    现在主用 tssh+iterm2 ,给大佬点赞!真的好用!
    LonnyWong
        4
    LonnyWong  
    OP
       331 天前
    @GoodRui 我也是用 iTerm2 ,不过 Warp 创造性的分块功能,值得研究下。
    xiaojun996
        5
    xiaojun996  
       331 天前
    牛的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1433 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 17:28 · PVG 01:28 · LAX 09:28 · JFK 12:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.