最近做自動化測試碰到需要在背景另起一個互動式的 c++ console ,並在適當時機用程式對他下指令,聽起來用 python 做應該滿簡單的,沒想到我卻在 python 跟 c++ 溝通的部份卡了一天!沒錯就是一天 😢
接下來就聽我娓娓道來採坑的流程:
首先先提供我自己的環境數據,因為我懷疑碰到的問題跟 os、c++ 編譯器、python 版本可能都有點關係。
C++ application: build by visual studio 2022
python application: python3.11
host: windows 10
我打算透過 python 的 subprocess 在背景 launch c++ 的 application ,透過 subprocess stdin 的方式把指令傳給 c++,並等待執行結果決定下一步的動作,重點是我"必須"跟這個 c++ 互動。
底下提供的範例, c++ 的程式會讀取使用者輸入直到 \n 然後將讀到的值 output 到 console 上。雖然是 c++ 的程式,但仍然會有不少人使用 C 的寫法,我們的程式也是如此,這邊也是我先採到的第一個坑。
#include <iostream>
int main() {
printf("console application!\n");
char input[100] = {0};
while(1) {
scanf("%[^\n]%*c", input);
printf("Receive input : %s\n", input);
if (_stricmp(input, "quit") == 0) {
break;
}
}
return 0;
}
import subprocess
if __name__ == "__main__":
proc = subprocess.Popen("test.exe", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
line = proc.stdout.readline()
print(line)
第一坑 : python卡在 readline()
這是因為 C++ 的 printf 如果不是直接執行,printf 時會先把資料緩存在暫存區裡直到程式結束才會一次輸出到 console,因此 python 那邊就會卡在 readline() 等待讀取 stdout
解法一 : 使用 fflush(stdout)
fflush 會迫使緩存區的內容直接輸出,這樣 python 那端就能馬上收到輸出並接續後面的動作
#include <iostream>
int main() {
printf("console application!\n");
fflush(stdout);
char input[100] = {0};
while(1) {
scanf("%[^\n]%*c", input);
if (_stricmp(input, "quit") == 0) {
break;
}
}
return 0;
}
解法二: 使用 c++ 的標準輸出
上面的方法得在每個 printf 的地方補上 fflush(),只要沒加到就有可能會卡住,另一種方式則是使用 c++ 的標準輸出 std::cout 和 std::endl。因為 std::endl 會在字串結尾插入 '\n' 並迫使緩存區的內容直接輸出到指定的輸出流
#include <iostream>
int main() {
std::cout << "console application" << std::endl;
char input[100] = {0};
while(1) {
std::cin.getline(input, 100);
std::cout << "Receive input : " << input << std::endl;
if (_stricmp(input, "quit") == 0) {
break;
}
}
return 0;
}
假設今天要讀取的輸出不是單行怎麼辦 ?
這個需求可能也不算少見,像是 help 之類的指令常常會在 console 輸出一大堆字串,如果使用 readline,你得明確的知道要 read 幾行,否則超過的話會導致程式卡在 readline 一直在等後面的輸出,但後面根本已經沒東西可以輸出了
直覺地想到可能可以使用readlines 讀取多行,接下來就會採到第二個坑:
#include <iostream>
int main() {
const char *sample = "line1\nline2\nline3\nline4";
std::cout << sample << std::endl;
char input[100] = {0};
while(1) {
std::cin.getline(input, 100);
std::cout << "Receive input : " << input << std::endl;
if (_stricmp(input, "quit") == 0) {
break;
}
}
return 0;
}
if __name__ == "__main__":
proc = subprocess.Popen("test.exe", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
print(proc.stdout.readlines())
第二坑 : python卡在 readlines()
python 的 readlines() 終止條件是碰到 EOF,跟讀檔案是一樣的,但我試過很多種方式從 C++ 這邊打 EOF 或是 ctrl + c/ ctrl + z 都沒辦法達到預期的效果 ,不是卡住出不來,不然就是好不容易跳出卻拿不到任何 output ( 這邊我到現在還是不知道為什麼 ? 不知道有沒有神人可以幫我解惑 😢 )
解法一: 使用 readline() 但判斷預計收到的最後一個字串
if __name__ == "__main__":
proc = subprocess.Popen("test.exe", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline()
print(line)
if line == b'line4\r\n':
break
這個方法簡單好懂,跟下面做法有異曲同工之妙,但我更喜歡下面這一種方式。
解法二: 使用 pexpect
pexpect 把 redline 包裝成 expect,並且帶上 timeout 的屬性,如果在一定時間內沒有等到預期的字串就直接 break,不怕碰到無窮迴圈,使用起來也很直覺。
from pexpect import popen_spawn
if __name__ == "__main__":
proc = popen_spawn.PopenSpawn("test.exe")
proc.sendline(b"test input to c++ application\n")
proc.expect("Receive input")
搞定 stdout 之後下一步就希望能透過 pipe 把指令打給 stdin ,並根據執行的結果來採取下一步的動作。 subprocess 如果是用 pipe 的方式輸入 stdin,可以直接用 write 把需要的資料寫進去 stdin,但執行下去之後發現怎麼又卡住了 😱
import subprocess
if __name__ == "__main__":
proc = subprocess.Popen("test.exe", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline()
print(line)
if line == b'line4\r\n':
break
proc.stdin.write(b"test input to c++ application")
print(proc.stdout.readline())
第三坑: 程式卡在 readline,而 C++ 則沒收到 stdin
python 同樣會把 stdin 緩存在 buffer 內,導致 c++ 根本沒收到任何輸入,而 python 則認為已經成功送出正滿心期待地等著 C++ 的 output。
解法: 呼叫 flush 強制把暫存區內的資料輸出到 stdin,並且資料一定要加上 '\n' 結尾
import subprocess
if __name__ == "__main__":
proc = subprocess.Popen("test.exe", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline()
print(line)
if line == b'line4\r\n':
break
proc.stdin.write(b"test input to c++ application\n")
proc.stdin.flush()
print(proc.stdout.readline())
對於 c++ 來說讀取通常都是判斷讀到 \n 就停止,類似於你在 console 輸入完畢後按下 enter 的這個動作,你也可以調整 c++ 根據你想要的字元中止讀取,比方以空白做為結束字元。
這邊其實還有一個小小採坑是 C++ 那端必須使用讀取完 stdin 會把暫存區清空的 function,像是 std 提供的 getline 或是 c 的 scanf ( fgets則不行 ),否則得另外找 function 把暫存區清除,不然會發生每次讀取 stdin 都一直讀到重複的內容,一直到下次重新刷新 stdin ( 比方說重新寫入新的數據 ) 。
不太確定這些問題是因為 subprocess 的 pipe 在 windows 環境還沒完全相容,也或是使用不同 python 的版本問題,之前試過不少 stackoverflow 上的解法,對我來說還是不 work。
在多次的 try and error 之後,終於試出一些可行的方法,不過過程中仍然碰到很多不知其所以然的結果,希望有熟悉的朋友可以互相交流一下意見 😃