2013年8月14日 星期三

[python] 使用 python 呼叫 console 執行檔,一個通用的 recipe

用 python 呼叫 console 執行檔有什麼稀奇呢?不就 import os, os.system 就好了嗎?
對,就這麼簡單。

import os
os.system('net')

但是要在程式中知道執行的成功或失敗,甚至得到執行檔的輸出,又該如何呢?

最近挖了一個 guiminer 的 python 專案的程式碼來看,學到一些東西。
該專案使用 wxpython 當做 gui。正好是 gui 程式與執行檔的整合範例。
這範例有以下特點:

1. 使用 subprocess 是目前 python 建議方式

subprocess.Popen(cmd, cwd=cwd,
  stdout=subprocess.PIPE,
  stderr=subprocess.STDOUT,
  universal_newlines=True,
  creationflags=flags,
  shell=(sys.platform != 'win32'))

2. 通常希望不要新開視窗,上述的 flags 可以控制視窗形式。其值是如下方式取得

try: import win32process

except ImportError: flags = 0

else: flags = win32process.CREATE_NO_WINDOW


3. 為了要監控程式的輸出,把 stdout 及 stderr 導至 PIPE 及 STDOUT (如 1.) 如此程式便需要不斷地讀取 stdout。


4. 使用 regular expression 針對輸出獲得自己想要的結果。


5. 也因為執行外部程式時需不斷地讀取 stdout,必定要另開 thread 來處理。使用 threading.Event() 來控制 run() 裡面的 while 迴圈何時離開。


觀察到以上 5 點,接下來便是改寫他的程式成為通用的 recipe。因為不想跟 gui 套件綁在一起,所以就把 gui 相關的拿掉,整理之後成為如下的 class。



import threading
import re
import subprocess
import sys

class CmdThreadListener(threading.Thread):
    '''
    Assign cmd and cwd
    '''

    LINES = []
    def __init__(self, cmd, cwd, callback=None, unhandle_callback=None):
        threading.Thread.__init__(self)
        self.shutdown_event = threading.Event()
        self.cmd = cmd
        self.cwd = cwd
        self.callback = callback
        self.unhandle_callback = unhandle_callback

    def run(self):
        try:
            import win32process
        except ImportError:
            flags = 0
        else:
            flags = win32process.CREATE_NO_WINDOW

        sub_proc = subprocess.Popen(self.cmd, cwd=self.cwd,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.STDOUT,
                                      universal_newlines=True,
                                      creationflags=flags,
                                      shell=(sys.platform != 'win32'))

        self.proc = sub_proc
        while not self.shutdown_event.is_set():
            line = self.proc.stdout.readline().strip()
            if not line:
                continue
            if len(self.LINES) == 0:
                unhandleline = line
                if self.unhandle_callback is not None:
                    self.unhandle_callback(unhandleline)

            for s, event_func in self.LINES:
                match = re.search(s, line, flags=re.I)
                if match is not None:
                    event = event_func(match)
                    if event is not None:
                        if self.callback is not None:
                            self.callback(event)
                        break
                else:
                    unhandleline = line
                    if self.unhandle_callback is not None:
                        self.unhandle_callback(unhandleline)

    def stop(self):
        if self.proc is not None and self.proc.returncode is None:
            try:
                self.proc.terminate()
            except OSError:
                pass
        self.shutdown_event.set()

 


以下是它的使用方法:



    import time
    import os

    module_name = sys.executable if hasattr(sys, 'frozen') else __file__
    abs_path = os.path.abspath(module_name)
    cmd = 'net'
    cwd = os.path.dirname(abs_path)

    print cmd

    cmd = cmd.encode('cp950') # quick modify for windows console

    def p(x):
        print(x)

    listener_instance = CmdThreadListener(cmd, cwd, unhandle_callback=p)
    listener_instance.daemon = True
    listener_instance.start()

    time.sleep(5)

    listener_instance.stop()
    listener_instance.join()


它的輸出結果如下:



net
這個命令的語法是:
NET
[ ACCOUNTS | COMPUTER | CONFIG | CONTINUE | FILE | GROUP | HELP |
HELPMSG | LOCALGROUP | PAUSE | SESSION | SHARE | START |
STATISTICS | STOP | TIME | USE | USER | VIEW ]


在 windows 下,Popen 的 shell=False 時,cmd 不能夠是 shell 裡面的指令。所以 dir 之類的就不行用。若是想執行 shell 裡面的指令,Popen 的 shell 參數要設成 True。


註:若是照我的測試的程式碼放到 IDLE 直接執行,可能會出現 NameError: global name '__file__' is not defined。那是 IDLE 忘了 assign __file__ 的 bug。請到 cmd 底下執行 python 程式。

沒有留言:

張貼留言