python打开伪终端_关于 macOS 伪终端(PTY)的学习
轉載請注明來源 https://justinyan.me
1973 年 Xerox PARC 第一次在 Xerox Alto 這款個人計算機上推出帶有 GUI 界面的操作系統,自此讓極大地降低個人計算機的使用門檻,也開啟了更加豐富多彩的計算機發展。
不過作為一個碼農,終端依然是平日不可或缺的生產力工具。在 macOS 上,系統自帶的 Termianl.app 或者更加好用的開源的 iTerm2.app 是最受歡迎的終端應用(其他 X Windows 系統也有像 xterm 之類的優秀應用)。他們也都是一個 Cocoa App。那么一個 Cocoa App 是如何把自己變成一個能跟用戶通過鍵盤交互,有標準輸入輸出的“偽終端”(Pseudoterminal)的呢?
〇、歷史上的終端 Terminal
在帶有電子顯示器的終端發明以前,人們真的就是在一臺帶鍵盤的打印機上,一邊打字輸入,一邊等待計算機在紙上打印輸出。所以大家寫 Hello World! 的時候都是用 print("Hello World!"),因為它是真地在打印。
第一臺帶有顯示器,支持 ANSI escape codes 的終端是 DEC 公司生產的 VT100。在這之前他們已經生產過很多種型號的電子終端,不過這臺機器是最成功的。
一、Cocoa App 如何調用系統自帶的 Binaries
我們知道 ls 這個命令在 Unix 系統里就是一個 binary,一般放在 /bin 或者 /usr/bin 這樣的目錄里,用 whereis 可以找到它在哪里。
whereis ls
ObjectiveC 在 Foundation 里提供了 NSTask 這樣的高級封裝,用它的接口可以非常簡單地實現類似 shell command 的效果。
但是首先一個沙盒 App 的能力是有限的,其次就算是沙盒外的 App,NSTask 也不允許直接訪問 /usr/bin 目錄里的 binaries,直接調用要嘛無響應要嘛直接 crash。
所以我們還得迂回一下,我們不直接運行 binaries,而是利用 bash 來運行:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/bash"];
[task setArguments:@[ @"-c", @"/usr/bin/killall Dock" ]];
[task launch];
但是即便如此,想要使用 NSTask 的接口來模擬終端還是非常困難的事情。所以,Termianl Apps 們是怎么實現的呢?
二、iTerm2
iTerm2 的代碼是開源的,歷史原因內部實現比較復雜,而且 iTerm2 支持在 Cocoa App 里直接和 python 腳本交互,相當于他提供了一套橋接的接口,可以用 python 來實現對 iTerm2 App 的自動化,類似 Hammerspoon 這類 App 的效果。所以閱讀過程中我還看到一堆 client/server 的通信,有點繞。
最后我發現真正實現終端功能的地方在這里: iTermPosixTTYReplacements.c,關鍵函數是:
int openpty(int *amaster, int *aslave, char *name, struct termios *termp, struct winsize *winp);
這個函數的實現在 Libc 里,可以參考蘋果開源頁面。
openpty() 是 BSD 函數,并不在 POSIX 標準里,不過 Linux 也有把這個函數 port 過去。從應用層的角度來看,openpty() 會跟 open("/dev/ptmx") 獲取一個可用的 pseudoterminal。iTerm2 的做法就是通過該函數獲得一個 pseudoterminal master 和 slave 的 fd 句柄,后續用戶在 UI 界面上的輸入都通過這兩個句柄來交互。
iTerm 在 openpty() 之后還 fork() 了一下自己,然后父進程釋放所有的句柄,這樣父進程處理 UI 輸入,一個窗口對應一個子進程,一個子進程對應一個 pty。
為什么 Unix 要這么設計 pty 接口呢?歷史原因。
早期的計算機比如 1970 年 DEC 生產的 PDP-11,他需要通過一系列的電線跟用戶的終端(也就是鍵盤和打印機)連接到一起。這種只有鍵盤和打印機的終端也叫做 TTY。后來有了電子顯示器之后,就得使用軟件模擬一個硬件終端,也叫做"偽終端"(pseudoterminal)。
UNIX 采用的設計是加入了一個中間層,當你使用 openpty() 打開一個偽終端的時候,會給你一個 master 一個 slave 句柄。GUI 軟件把鍵盤輸入作為 master 的 input 寫入,master 的 output 就會作為 slave 的 input 寫入,然后再作為 output 輸出。所以對于我們的 Cocoa App 應用層來說,可以簡單地把 master fd 作為 writer,把 slave fd 作為 reader。
聽起來好像沒什么必要但是其實 slave 做了一些特殊的處理。比如 GUI 直接把鍵盤輸入的 CTRL+C(0x03) 寫入 master 句柄。這時候 slave 接收到后會把 0x03 轉換成 SIGINT signal 發出。對此感興趣的同學可以參考微軟關于 ConPTY 的這篇文章。
所以 iTerm2 既是一個 Cocoa App 又是一個“終端模擬器”,你可以在這個 App 里跑任意 shell 命令。
三、其他應用
openpty() 這種 master/slave fd 的設計還體現在 SSH 遠程登錄上。可以參考 macOS 的 OpenSSH 源碼。客戶端通過 SSH 協議連上服務端時,服務端的 sshd 進程開了一個 pty 用來跑客戶端輸入的命令。
另外 VSCode 也基于 Node.js 實現了一個編輯器內的 console,源碼在這里。
回到我們的 Cocoa App 來,一個 NSTask 對象在被 launch() 之前我們可以當做是一個數據存儲的結構體來對待。通常我們會直接調用它的 launch() 方法,然后使用 NSPipe 來讀寫。
這里如果要繞過上文所述的 crash 問題,我們可以改用 openpty():
NSCAssert(openpty(&masterFD, &slaveFD, NULL, NULL, NULL) == 0,
@"A pseudoterminal couldn't be opened.");
*readHandle = [[NSFileHandle alloc] initWithFileDescriptor:masterFD closeOnDealloc:YES];
*writeHandle = [[NSFileHandle alloc] initWithFileDescriptor:slaveFD closeOnDealloc:YES];
有興趣的讀者朋友不妨一試。
參考資料
總結
以上是生活随笔為你收集整理的python打开伪终端_关于 macOS 伪终端(PTY)的学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring cloud tencent
- 下一篇: 爬取东方财富的十大成交股