Why
背景:我開源了一個 ssh 客戶端,叫 trzsz-ssh ( tssh ),定制了一些網(wǎng)友需要的功能,解決了一些 ssh 相關(guān)的痛點,具體詳看開源地址:https://github.com/trzsz/trzsz-ssh
起因:在 Warp 終端中,為什么原生的 ssh 客戶端就可以支持 blocks feature,而我自己寫的 tssh 客戶端就不行呢?于是我一步步地深挖了其實現(xiàn)原理。
What
在 Warp 終端,當(dāng)你 ssh 登錄到服務(wù)器上,默認(rèn)情況下,你在服務(wù)器上執(zhí)行的每條命令以及其輸出就會被 Warp 分別定義成一個個 block 塊,你可以一塊塊地選中和移動,非常的酷。如果不支持,那整個 ssh 登錄后的所有命令及輸出就會被 Warp 定義成同一個 block 塊,選中和移動都是整個登錄后的所有命令及其輸出,那就沒那么酷了。
另外,當(dāng)你在服務(wù)器上輸入命令按 tab 鍵時,Warp 終端會彈出一個浮層顯示可選的目錄或文件,也很帥。如果不支持,那 tab 鍵也不能正常地進(jìn)行補全了,這對我來說簡直不能忍。
How
言歸正傳,Warp 終端是怎么實現(xiàn) blocks feature 和自定義 tab 行為等功能的呢?
在 Wrap 終端中,內(nèi)置了一些 shell 函數(shù),bash 可以通過 type 函數(shù)名 進(jìn)行查看函數(shù)定義,zsh 可以通過 which 函數(shù)名 進(jìn)行查看函數(shù)定義。
-
Warp定義了個ssh函數(shù)在
Warp中執(zhí)行ssh xxx登錄服務(wù)器,實際是執(zhí)行同名的ssh函數(shù),其定義如下: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函數(shù)判斷是否為交互式的 ssh 登錄。 - 若不是交互式的 ssh 登錄,則直接調(diào)用原生的
ssh命令command ssh "$@"。 - 若是交互式的 ssh 登錄,則調(diào)用
warp_send_json_message函數(shù),輸出一串用戶看不見的 json,Warp可能會做一些統(tǒng)計之類。 - 若
WARP_USE_SSH_WRAPPER環(huán)境變量不是1,則直接調(diào)用原生的ssh命令command ssh "$@"。默認(rèn)是1的。 - 調(diào)試相關(guān)的
TRACE_FLAG_IF_WARP_DEBUG_MODE和WARP_DEBUG_MODE可以忽略,默認(rèn)是不調(diào)試的。 - 核心邏輯在
warp_ssh_helper函數(shù)中實現(xiàn)warp_ssh_helper "$@",下文再詳細(xì)介紹。
- 通過
-
判斷是否為交互式的 ssh 登錄
在
Warp中通過is_interactive_ssh_session函數(shù)判斷是否為交互式 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 命令中是否帶有目標(biāo)機器
[[ ${#ARGS[@]} -ne 1 ]],若沒有目標(biāo)機器,也認(rèn)為不是交互式的,返回1( 非交互 )。-
trzsz ssh ( tssh )支持不帶參數(shù)運行,會列出所有服務(wù)器的列表,支持搜索和選擇進(jìn)行登錄,這里需要調(diào)整才能支持blocks feature:# 注意里面的 `command` 關(guān)鍵字,若沒有它,就會循環(huán)調(diào)用 `ssh` 函數(shù),而不是執(zhí)行 `ssh` 命令了。不要問我怎么知道的。 if [[ ${#ARGS[@]} -ne 1 ]] && [[ $(command ssh -V 2>&1) != "trzsz ssh"* ]]; then return 1; fi
-
輸出一段用戶看不見的 json 內(nèi)容
在
Warp中通過warp_send_json_message輸出一段用戶看不見的 json 內(nèi)容,這是Warp的內(nèi)部邏輯,可以忽略,實測不輸出也不影響的,其定義如下:warp_send_json_message () { encoded_message=$(warp_hex_encode_string "$1"); printf $DCS_START$DCS_JSON_MARKER$encoded_message$DCS_END }- 其實就是先進(jìn)行
hex編碼,然后加上\x1bP$d開頭,加上\x9c結(jié)尾,最終輸出的內(nèi)容如下:
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. - 其實就是先進(jìn)行
-
核心邏輯
warp_ssh_helper函數(shù)在
Warp中通過warp_ssh_helper函數(shù)實現(xiàn)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多路復(fù)用,Warp就可以通過同一個連接,在服務(wù)器上執(zhí)行命令,獲取當(dāng)前目錄下有哪些文件等,tab相關(guān)功能就是靠這實現(xiàn)的。 - 通過
-o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID指定多路復(fù)用的socket路徑,是長~/.ssh/170252756912781這樣子的。 - 通過
-t選項強制分配一個偽終端,因為后面指定了登錄后要初始化執(zhí)行的腳本,沒有-t選項就會默認(rèn)禁止分配偽終端,就影響用戶使用了。 - 參數(shù)
"${@:1}"就是要登錄的目標(biāo)機器,從前面ssh命令行傳遞過來的。 - 最后這一大段腳本,就是登錄后要初始化執(zhí)行的,下文再詳細(xì)介紹。這里要改成用
-o RemoteCommand實現(xiàn),才能兼容trzsz ssh ( tssh )的搜索模式。
- 前面
-
在服務(wù)器執(zhí)行的初始化腳本
前面說到,在
Warp中ssh登錄到服務(wù)器之后,會執(zhí)行一大段腳本,以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進(jìn)行輸出,用戶看不見,但是Warp可以解釋并獲取到。 -
Warp獲取到這些信息之后,就會生成另一段腳本,(模擬用戶輸入)直接發(fā)送到服務(wù)器執(zhí)行,修改一些 shell 的設(shè)置等,從而感知到每一個命令,實現(xiàn)blocks feature等。 - 由于篇幅和時間關(guān)系,先介紹到這。是不是很簡單?你學(xué)會了嗎?歡迎留言評論。
- 其實就是通過 shell 獲取一些信息,然后通過
Btw
我給 Warp 提了個 feature request https://github.com/warpdotdev/Warp/issues/3960,解決 tssh xxx 直接登錄可以支持 blocks feature, 而 tssh 搜索和選擇服務(wù)器登錄卻不支持 的問題。有需要的朋友去幫忙點個贊,提高下優(yōu)先級。
附在 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