1.問題
python可以作為shell替代,代碼比較直觀,易于維護(hù)。 python支持調(diào)用外部shell命令。不過,這個問題沒有看上去簡單,要完美的解決此問題,比較復(fù)雜,就連標(biāo)準(zhǔn)庫也不見得處理得很好。
2.方案
2.1.方案一
首先最簡單的方法就是調(diào)用system方法,直接執(zhí)行系統(tǒng)shell命令,代碼如下:
import os
os.system('ls -l')
system主要問題,就是無法獲取shell命令的輸出,無法進(jìn)行輸入;也沒有超時設(shè)置,如果外部命令掛死,會直接導(dǎo)致當(dāng)前進(jìn)程掛死。
2.2.方案二
python3的subprocess提供了check_output方法,可以直接獲取進(jìn)程的輸出,也支持輸入,同時關(guān)鍵的是支持超時設(shè)置。這就防止了shell命令掛死的問題。
def __exec_command(cmd: str, input: str = None, timeout=10) -> str:
try:
output_bytes = subprocess.check_output(cmd, input=input, stderr=subprocess.STDOUT, shell=True, timeout=timeout)
except subprocess.CalledProcessError as err:
output = err.output.decode('utf-8')
logger.debug(output)
raise err
result = output_bytes.decode('utf-8')
return result
print(__exec_command('ls -l'))
現(xiàn)在可以成功獲取系統(tǒng)命令的結(jié)果,并且很好的支持超時功能,防止命令掛死。不過,我們看看下面這個例子:
print(__exec_command('echo begin;sleep 10; echo end; sleep 3'), timeout=30)
上述代碼中,要想獲取shell命令的結(jié)果,實際測試的結(jié)果,只能等到子進(jìn)程結(jié)束才可以獲取,父進(jìn)程只能傻傻得等,對子進(jìn)程的執(zhí)行過程一無所知。
2.3.方案三
上述的問題,看上容易解決,實際上比較復(fù)雜。我們先看下,使用更低層的subprocess.Popen能否解決:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
while True:
if process.poll() is None and timeout > 0:
output_bs = process.stdout.read()
if not output_bs:
....
time.sleep(0.5)
timeout = timeout - 0.5
if process.poll() is None or timeout <= 0:
process.kill()
上述問題是無法解決我們的問題,因為process.stdout.read()是阻塞的,如果子進(jìn)程沒有輸出,就掛住了。
我們使用有超時功能communicate方法再試試:
def exec_command(cmd: str, input: str = None, encoding='utf-8', shell=True, timeout=5) -> str:
output_bytes = b''
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
while process.poll() is None and timeout > 0:
try:
output_bytes, output_err_bytes = process.communicate(timeout=0.5)
except subprocess.TimeoutExpired as err:
if err.stdout:
output = err.output.decode(encoding)
print(output)
timeout -= 0.5
continue
if process.poll() is None or timeout <= 0:
process.kill()
raise ValueError(f'exec command: {cmd} timeout')
result = output_bytes.decode(encoding)
return result
communicate超時時拋出異常subprocess.TimeoutExpired,這個異常對象的stdout帶有子進(jìn)程已經(jīng)輸出的內(nèi)容。
目前還不錯,可以滿足開頭提的問題。不過,不能算完美,因為輸出是有點奇怪,如下所示:
hello
hello
hello
hello
hello
hello
hello-end
每次TimeoutExpired超時,stdout所帶的內(nèi)容,是子進(jìn)程已經(jīng)輸出的內(nèi)容,而不是新增加的內(nèi)容。
2.4.方案四
要想實時獲取子進(jìn)程是否有內(nèi)容輸出,我們可以使用文件進(jìn)行重定下,代碼如下:
def exec_command(cmd: str, input: str = None, encoding='utf-8', shell=True, timeout=10, wait=0.5) -> str:
_opener = lambda name, flag, mode=0o7770: os.open(name, flag | os.O_RDWR, mode)
output_bytes = bytearray()
with tempfile.NamedTemporaryFile('w+b') as writer, open(writer.name, 'rb', opener=_opener) as reader:
try:
process = subprocess.Popen(cmd, stdout=writer, stderr=writer, stdin=subprocess.PIPE, shell=shell)
if input:
process.stdin.write(input.encode(encoding))
process.stdin.close()
while process.poll() is None and timeout > 0:
new_bytes = reader.read()
if new_bytes or new_bytes != b'':
logger.debug(f'{new_bytes}')
output_bytes = output_bytes + bytearray(new_bytes)
else:
logger.debug('waiting sub process output......')
time.sleep(wait)
timeout -= wait
except Exception as err:
process.kill()
raise err
if process.poll() is None:
process.kill()
raise ValueError(f'exec cmd:{cmd} timeout')
new_bytes = reader.read()
if new_bytes:
output_bytes = output_bytes + bytearray(new_bytes)
result = output_bytes.decode(encoding)
return result
這里,我們試用了臨時文件對子進(jìn)程的輸入輸出進(jìn)行重定向,對于文件的讀取reader.read()實際上并不是阻塞的?;就昝缹崿F(xiàn)了本文的問題。
3.討論
在windows系統(tǒng)中,python創(chuàng)建子進(jìn)程的時候,可以使用管道作為子進(jìn)程的輸入?yún)?shù)startupinfo,從而完美實現(xiàn)子進(jìn)程輸入輸出重定向。但在linux確不行,不支持參數(shù)startupinfo。
process=subprocess.Popen參數(shù)subprocess.PIPE字面上是返回管道,但子進(jìn)程process.stdout實際是文件句柄,讀操作完全是阻塞,沒有非阻塞得讀,這是問題的關(guān)鍵所在。
方案二和方案四不妨結(jié)合起來使用,對于長時間執(zhí)行任務(wù),選擇方案四,對于一般的任務(wù)執(zhí)行直接使用方案二。
python中執(zhí)行系統(tǒng)shell命令,也可以創(chuàng)建一個線程進(jìn)行子進(jìn)程輸出讀取,超時就殺掉線程;或者使用協(xié)程版本的subprocess,但是實現(xiàn)起來更加復(fù)雜,效率顯得更差。有興趣的同學(xué),可以自己實現(xiàn)試試。
enjoy~~~