Why
背景:我開源了一個(gè) ssh
客戶端,叫 trzsz-ssh ( tssh )
,定制了一些網(wǎng)友需要的功能,解決了一些 ssh
相關(guān)的痛點(diǎn)京腥,具體詳看開源地址:https://github.com/trzsz/trzsz-ssh
起因:在 Warp
終端中,為什么原生的 ssh
客戶端就可以支持 blocks feature
溅蛉,而我自己寫的 tssh
客戶端就不行呢?于是我一步步地深挖了其實(shí)現(xiàn)原理他宛。
What
在 Warp
終端船侧,當(dāng)你 ssh
登錄到服務(wù)器上,默認(rèn)情況下厅各,你在服務(wù)器上執(zhí)行的每條命令以及其輸出就會被 Warp
分別定義成一個(gè)個(gè) block
塊镜撩,你可以一塊塊地選中和移動,非常的酷。如果不支持袁梗,那整個(gè) ssh
登錄后的所有命令及輸出就會被 Warp
定義成同一個(gè) block
塊宜鸯,選中和移動都是整個(gè)登錄后的所有命令及其輸出,那就沒那么酷了遮怜。
另外淋袖,當(dāng)你在服務(wù)器上輸入命令按 tab
鍵時(shí),Warp
終端會彈出一個(gè)浮層顯示可選的目錄或文件锯梁,也很帥即碗。如果不支持,那 tab
鍵也不能正常地進(jìn)行補(bǔ)全了陌凳,這對我來說簡直不能忍剥懒。
How
言歸正傳,Warp
終端是怎么實(shí)現(xiàn) blocks feature
和自定義 tab
行為等功能的呢合敦?
在 Wrap
終端中初橘,內(nèi)置了一些 shell
函數(shù),bash
可以通過 type 函數(shù)名
進(jìn)行查看函數(shù)定義充岛,zsh
可以通過 which 函數(shù)名
進(jìn)行查看函數(shù)定義壁却。
-
Warp
定義了個(gè)ssh
函數(shù)在
Warp
中執(zhí)行ssh xxx
登錄服務(wù)器,實(shí)際是執(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)計(jì)之類砸王。 - 若
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ù)中實(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
等選項(xiàng),若有則說明不是交互式的诵肛,直接返回1
( 非交互 )屹培。判斷 ssh 命令中是否帶有目標(biāo)機(jī)器
[[ ${#ARGS[@]} -ne 1 ]]
,若沒有目標(biāo)機(jī)器,也認(rèn)為不是交互式的褪秀,返回1
( 非交互 )蓄诽。-
trzsz ssh ( tssh )
支持不帶參數(shù)運(yùn)行,會列出所有服務(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)部邏輯,可以忽略沛豌,實(shí)測不輸出也不影響的趋箩,其定義如下:warp_send_json_message () { encoded_message=$(warp_hex_encode_string "$1"); printf $DCS_START$DCS_JSON_MARKER$encoded_message$DCS_END }
- 其實(shí)就是先進(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.
- 其實(shí)就是先進(jìn)行
-
核心邏輯
warp_ssh_helper
函數(shù)在
Warp
中通過warp_ssh_helper
函數(shù)實(shí)現(xiàn)blocks feature
和tab
補(bǔ)全等功能,其定義如下: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
先忽略竹勉,不是本文重點(diǎn),重點(diǎn)是command ssh ...
那行娄琉。 - 通過
-o ControlMaster=yes
啟用了ssh
多路復(fù)用次乓,Warp
就可以通過同一個(gè)連接,在服務(wù)器上執(zhí)行命令孽水,獲取當(dāng)前目錄下有哪些文件等票腰,tab
相關(guān)功能就是靠這實(shí)現(xiàn)的。 - 通過
-o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID
指定多路復(fù)用的socket
路徑女气,是長~/.ssh/170252756912781
這樣子的杏慰。 - 通過
-t
選項(xiàng)強(qiáng)制分配一個(gè)偽終端,因?yàn)楹竺嬷付说卿浐笠跏蓟瘓?zhí)行的腳本炼鞠,沒有-t
選項(xiàng)就會默認(rèn)禁止分配偽終端缘滥,就影響用戶使用了。 - 參數(shù)
"${@:1}"
就是要登錄的目標(biāo)機(jī)器簇搅,從前面ssh
命令行傳遞過來的完域。 - 最后這一大段腳本,就是登錄后要初始化執(zhí)行的瘩将,下文再詳細(xì)介紹。這里要改成用
-o RemoteCommand
實(shí)現(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
- 其實(shí)就是通過 shell 獲取一些信息异旧,然后通過
Device Control String
進(jìn)行輸出,用戶看不見提佣,但是Warp
可以解釋并獲取到吮蛹。 -
Warp
獲取到這些信息之后,就會生成另一段腳本拌屏,(模擬用戶輸入)直接發(fā)送到服務(wù)器執(zhí)行潮针,修改一些 shell 的設(shè)置等,從而感知到每一個(gè)命令倚喂,實(shí)現(xiàn)blocks feature
等每篷。 - 由于篇幅和時(shí)間關(guān)系,先介紹到這端圈。是不是很簡單焦读?你學(xué)會了嗎?歡迎留言評論舱权。
- 其實(shí)就是通過 shell 獲取一些信息异旧,然后通過
Btw
我給 Warp
提了個(gè) feature request
https://github.com/warpdotdev/Warp/issues/3960矗晃,解決 tssh xxx 直接登錄可以支持 blocks feature, 而 tssh 搜索和選擇服務(wù)器登錄卻不支持
的問題宴倍。有需要的朋友去幫忙點(diǎn)個(gè)贊张症,提高下優(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