2010-08-26 37 views
10

Tôi muốn thực hiện một quá trình, giới hạn thời gian thực hiện bởi một số timeout trong vài giây và lấy đầu ra được sản xuất bởi quá trình này. Và tôi muốn làm điều này trên windows, linux và freebsd.python subprocess với timeout và đầu ra lớn (> 64K)

Tôi đã cố gắng thực hiện điều này theo ba cách khác nhau:

  1. cmd - Nếu không có thời gian chờ và subprocess.PIPE cho chụp đầu ra.

    HÀNH VI: Hoạt động như mong đợi nhưng không hỗ trợ thời gian chờ, tôi cần thời gian chờ ...

  2. cmd_to - Với thời gian chờ và subprocess.PIPE cho chụp đầu ra.

    BEHAVIOR: Chặn thực thi quy trình con khi đầu ra> = 2^16 byte.

  3. cmd_totf - Với thời gian chờ và tempfile.NamedTemporaryfile để chụp đầu ra.

    BEHAVIOR: Hoạt động như mong đợi nhưng sử dụng tệp tạm thời trên đĩa.

Chúng có sẵn bên dưới để kiểm tra kỹ hơn.

Như có thể thấy ở đầu ra bên dưới, sau đó mã hết thời gian thực thi quy trình phụ khi sử dụng xử lý con.PIPE và đầu ra từ tiến trình con là> = 2^16 byte.

Tài liệu quy trình con nói rằng điều này được mong đợi khi gọi process.wait() và sử dụng subprocessing.PIPE, tuy nhiên không có cảnh báo nào được đưa ra khi sử dụng process.poll(), vậy có gì sai ở đây?

Tôi có một giải pháp trong cmd_totf sử dụng mô-đun tempfile nhưng sự cân bằng là nó ghi đầu ra vào đĩa, cái gì tôi thực sự muốn tránh.

Vì vậy, câu hỏi của tôi là:

  • Tôi đang làm gì sai trong cmd_to?
  • Có cách nào để làm những gì tôi muốn và không sử dụng tempfiles/giữ đầu ra trong bộ nhớ.

Script để tạo ra một loạt các đầu ra ('exp_gen.py'):

#!/usr/bin/env python 
import sys 
output = "b"*int(sys.argv[1]) 
print output 

Ba triển khai khác nhau (cmd, cmd_to, cmd_totf) của hàm bao quanh subprocessing.Popen:

#!/usr/bin/env python 
import subprocess, time, tempfile 
bufsize = -1 

def cmd(cmdline, timeout=60): 
    """ 
    Execute cmdline. 
    Uses subprocessing and subprocess.PIPE. 
    """ 

    p = subprocess.Popen(
    cmdline, 
    bufsize = bufsize, 
    shell = False, 
    stdin = subprocess.PIPE, 
    stdout = subprocess.PIPE, 
    stderr = subprocess.PIPE 
) 

    out, err = p.communicate() 
    returncode = p.returncode 

    return (returncode, err, out) 

def cmd_to(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses subprocessing and subprocess.PIPE. 
    """ 

    p = subprocess.Popen(
    cmdline, 
    bufsize = bufsize, 
    shell = False, 
    stdin = subprocess.PIPE, 
    stdout = subprocess.PIPE, 
    stderr = subprocess.PIPE 
) 

    t_begin   = time.time()    # Monitor execution time 
    seconds_passed = 0 

    while p.poll() is None and seconds_passed < timeout: 
    seconds_passed = time.time() - t_begin 
    time.sleep(0.1) 

    #if seconds_passed > timeout: 
    # 
    # try: 
    # p.stdout.close() # If they are not closed the fds will hang around until 
    # p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
    # p.terminate()  # Important to close the fds prior to terminating the process! 
    #      # NOTE: Are there any other "non-freed" resources? 
    # except: 
    # pass 
    # 
    # raise TimeoutInterrupt 

    out, err = p.communicate() 
    returncode = p.returncode 

    return (returncode, err, out) 

def cmd_totf(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses subprocessing and tempfile instead of subprocessing.PIPE. 
    """ 

    output = tempfile.NamedTemporaryFile(delete=False) 
    error = tempfile.NamedTemporaryFile(delete=False) 

    p = subprocess.Popen(
    cmdline, 
    bufsize = 0, 
    shell = False, 
    stdin = None, 
    stdout = output, 
    stderr = error 
) 

    t_begin   = time.time()    # Monitor execution time 
    seconds_passed = 0 

    while p.poll() is None and seconds_passed < timeout: 
    seconds_passed = time.time() - t_begin 
    time.sleep(0.1) 

    #if seconds_passed > timeout: 
    # 
    # try: 
    # p.stdout.close() # If they are not closed the fds will hang around until 
    # p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
    # p.terminate()  # Important to close the fds prior to terminating the process! 
    #      # NOTE: Are there any other "non-freed" resources? 
    # except: 
    # pass 
    # 
    # raise TimeoutInterrupt 

    p.wait() 

    returncode = p.returncode 

    fd   = open(output.name) 
    out   = fd.read() 
    fd.close() 

    fd = open(error.name) 
    err = fd.read() 
    fd.close() 

    error.close() 
    output.close() 

    return (returncode, err, out) 

if __name__ == "__main__": 

    implementations = [cmd, cmd_to, cmd_totf] 
    bytes  = ['65535', '65536', str(1024*1024)] 
    timeouts = [5] 

    for timeout in timeouts:  
    for size in bytes:  
     for i in implementations: 
     t_begin   = time.time() 
     seconds_passed = 0   
     rc, err, output = i(['exp_gen.py', size], timeout) 
     seconds_passed = time.time() - t_begin 
     filler = ' '*(8-len(i.func_name)) 
     print "[%s%s: timeout=%d, iosize=%s, seconds=%f]" % (repr(i.func_name), filler, timeout, size, seconds_passed) 

Kết quả thực hiện:

['cmd'  : timeout=5, iosize=65535, seconds=0.016447] 
['cmd_to' : timeout=5, iosize=65535, seconds=0.103022] 
['cmd_totf': timeout=5, iosize=65535, seconds=0.107176] 
['cmd'  : timeout=5, iosize=65536, seconds=0.028105] 
['cmd_to' : timeout=5, iosize=65536, seconds=5.116658] 
['cmd_totf': timeout=5, iosize=65536, seconds=0.104905] 
['cmd'  : timeout=5, iosize=1048576, seconds=0.025964] 
['cmd_to' : timeout=5, iosize=1048576, seconds=5.128062] 
['cmd_totf': timeout=5, iosize=1048576, seconds=0.103183] 
+0

Thử câu trả lời từ http://stackoverflow.com/questions/874815/how-do-i-get-real-time-information-back-from-a-subprocess-popen-in-python-2-5 . –

+0

Bạn nên đề cập đến phiên bản của python. Vì AFAIK, có khá ít thay đổi từ 2,6 đến 2,7 liên quan đến mô-đun 'subprocess' –

+0

Xem thêm http://stackoverflow.com/questions/1191374/subprocess-with-timeout/8507775#8507775 – bortzmeyer

Trả lời

4

Trái ngược với tất cả các cảnh báo trong tài liệu quy trình con sau đó đọc trực tiếp từ process.stdout và process.stderr đã cung cấp một giải pháp tốt hơn.

Bằng tốt hơn, tôi có nghĩa là tôi có thể đọc đầu ra từ một quy trình vượt quá 2^16 byte mà không phải lưu trữ tạm thời đầu ra trên đĩa.

mã sau:

import fcntl 
import os 
import subprocess 
import time 

def nonBlockRead(output): 
    fd = output.fileno() 
    fl = fcntl.fcntl(fd, fcntl.F_GETFL) 
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 
    try: 
     return output.read() 
    except: 
     return '' 

def cmd(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses the subprocess module and subprocess.PIPE. 

    Raises TimeoutInterrupt 
    """ 

    p = subprocess.Popen(
     cmdline, 
     bufsize = bufsize, # default value of 0 (unbuffered) is best 
     shell = False, # not really needed; it's disabled by default 
     stdout = subprocess.PIPE, 
     stderr = subprocess.PIPE 
    ) 

    t_begin = time.time() # Monitor execution time 
    seconds_passed = 0 

    stdout = '' 
    stderr = '' 

    while p.poll() is None and seconds_passed < timeout: # Monitor process 
     time.sleep(0.1) # Wait a little 
     seconds_passed = time.time() - t_begin 

     # p.std* blocks on read(), which messes up the timeout timer. 
     # To fix this, we use a nonblocking read() 
     # Note: Not sure if this is Windows compatible 
     stdout += nonBlockRead(p.stdout) 
     stderr += nonBlockRead(p.stderr) 

    if seconds_passed >= timeout: 
     try: 
      p.stdout.close() # If they are not closed the fds will hang around until 
      p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
      p.terminate()  # Important to close the fds prior to terminating the process! 
           # NOTE: Are there any other "non-freed" resources? 
     except: 
      pass 

     raise TimeoutInterrupt 

    returncode = p.returncode 

    return (returncode, stdout, stderr) 
+0

Điều này là tốt đẹp, nhưng nó chặn trên đọc nếu không có đầu ra để đọc, mà messes lên bộ đếm thời gian. Tôi đã sửa nó trong phiên bản của tôi, và thêm bạn một bản chỉnh sửa. –

+0

@JohnDoe: ['fcntl' không hoạt động trên Windows] (http://stackoverflow.com/q/375427/4279) – jfs

1

Disclaimer: Câu trả lời này không được thử nghiệm trên các cửa sổ, và cũng không freebsd. Nhưng các mô-đun đã sử dụng sẽ hoạt động trên các hệ thống này. Tôi tin rằng đây sẽ là một câu trả lời làm việc cho câu hỏi của bạn - nó làm việc cho tôi.

Đây là mã tôi vừa bị hack để giải quyết vấn đề trên linux. Nó là sự kết hợp của một số luồng Stackoverflow và nghiên cứu của riêng tôi trong các tài liệu Python 3.

đặc điểm chính của mã này:

  • Sử dụng quy trình không đề cho blocking I/O, vì họ đáng tin cậy hơn có thể được p.terminated()
  • Thực hiện một thời gian chờ cơ quan giám sát retriggerable rằng khởi động lại bất cứ khi nào đếm số lượng xảy ra
  • Thực hiện một cơ quan giám sát thời gian chờ lâu dài để hạn chế thời gian chạy tổng
  • có thể nuôi trong stdin (mặc dù tôi chỉ cần để nuôi trong một lần chuỗi ngắn)
  • Có thể chụp stdout/stderr trong các phương thức Popen thông thường (Chỉ stdout được mã hóa, và stderr chuyển hướng đến stdout; nhưng có thể dễ dàng được tách ra)
  • Nó gần như thời gian thực vì nó chỉ kiểm tra mỗi 0,2 giây cho đầu ra. Nhưng bạn có thể giảm hoặc xóa khoảng thời gian chờ một cách dễ dàng
  • Rất nhiều bản in gỡ lỗi vẫn được bật để xem điều gì đang xảy ra khi nào.

Phụ thuộc mã duy nhất là enum như được triển khai here, nhưng mã có thể dễ dàng được thay đổi để hoạt động mà không có. Nó chỉ được sử dụng để phân biệt hai thời gian chờ - sử dụng các ngoại lệ riêng biệt nếu bạn muốn.

Dưới đây là mã - như thường lệ - phản hồi được đánh giá cao: (Chỉnh sửa 29-Jun-2012 - mã bây giờ thực sự làm việc)

# Python module runcmd 
# Implements a class to launch shell commands which 
# are killed after a timeout. Timeouts can be reset 
# after each line of output 
# 
# Use inside other script with: 
# 
# import runcmd 
# (return_code, out) = runcmd.RunCmd(['ls', '-l', '/etc'], 
#         timeout_runtime, 
#         timeout_no_output, 
#         stdin_string).go() 
# 

import multiprocessing 
import queue 
import subprocess 
import time 

import enum 

def timestamp(): 
    return time.strftime('%Y%m%d-%H%M%S') 


class ErrorRunCmd(Exception): pass 
class ErrorRunCmdTimeOut(ErrorRunCmd): pass 

class Enqueue_output(multiprocessing.Process): 
    def __init__(self, out, queue): 
     multiprocessing.Process.__init__(self) 
     self.out = out 
     self.queue = queue 
     self.daemon = True 
    def run(self): 
     try: 
      for line in iter(self.out.readline, b''): 
       #print('worker read:', line) 
       self.queue.put(line) 
     except ValueError: pass # Readline of closed file 
     self.out.close() 
class Enqueue_input(multiprocessing.Process): 
    def __init__(self, inp, iterable): 
     multiprocessing.Process.__init__(self) 
     self.inp = inp 
     self.iterable = iterable 
     self.daemon = True 
    def run(self): 
     #print("writing stdin") 
     for line in self.iterable: 
      self.inp.write(bytes(line,'utf-8')) 
     self.inp.close() 
     #print("writing stdin DONE") 

class RunCmd(): 
    """RunCmd - class to launch shell commands 

    Captures and returns stdout. Kills child after a given 
    amount (timeout_runtime) wallclock seconds. Can also 
    kill after timeout_retriggerable wallclock seconds. 
    This second timer is reset whenever the child does some 
    output 

     (return_code, out) = RunCmd(['ls', '-l', '/etc'], 
            timeout_runtime, 
            timeout_no_output, 
            stdin_string).go() 

    """ 
    Timeout = enum.Enum('No','Retriggerable','Runtime') 

    def __init__(self, cmd, timeout_runtime, timeout_retriggerable, stdin=None): 
     self.dbg = False 
     self.cmd = cmd 
     self.timeout_retriggerable = timeout_retriggerable 
     self.timeout_runtime = timeout_runtime 
     self.timeout_hit = self.Timeout.No 
     self.stdout = '--Cmd did not yield any output--' 
     self.stdin = stdin 
    def read_queue(self, q): 
     time_last_output = None 
     try: 
      bstr = q.get(False) # non-blocking 
      if self.dbg: print('{} chars read'.format(len(bstr))) 
      time_last_output = time.time() 
      self.stdout += bstr 
     except queue.Empty: 
      #print('queue empty') 
      pass 
     return time_last_output 
    def go(self): 
     if self.stdin: 
      pstdin = subprocess.PIPE 
     else: 
      pstdin = None 
     p = subprocess.Popen(self.cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=pstdin) 
     pin = None 
     if (pstdin): 
      pin = Enqueue_input(p.stdin, [self.stdin + '\n']) 
      pin.start() 
     q = multiprocessing.Queue() 
     pout = Enqueue_output(p.stdout, q) 
     pout.start() 
     try: 
      if self.dbg: print('Beginning subprocess with timeout {}/{} s on {}'.format(self.timeout_retriggerable, self.timeout_runtime, time.asctime())) 
      time_begin = time.time() 
      time_last_output = time_begin 
      seconds_passed = 0 
      self.stdout = b'' 
      once = True     # ensure loop's executed at least once 
             # some child cmds may exit very fast, but still produce output 
      while once or p.poll() is None or not q.empty(): 
       once = False 
       if self.dbg: print('a) {} of {}/{} secs passed and overall {} chars read'.format(seconds_passed, self.timeout_retriggerable, self.timeout_runtime, len(self.stdout))) 

       tlo = self.read_queue(q) 
       if tlo: 
        time_last_output = tlo 

       now = time.time() 
       if now - time_last_output >= self.timeout_retriggerable: 
        self.timeout_hit = self.Timeout.Retriggerable 
        raise ErrorRunCmdTimeOut(self) 
       if now - time_begin >= self.timeout_runtime: 
        self.timeout_hit = self.Timeout.Runtime 
        raise ErrorRunCmdTimeOut(self) 

       if q.empty(): 
        time.sleep(0.1) 
      # Final try to get "last-millisecond" output 
      self.read_queue(q)    
     finally: 
      self._close(p, [pout, pin])    
     return (self.returncode, self.stdout)    

    def _close(self, p, procs): 
     if self.dbg: 
      if self.timeout_hit != self.Timeout.No: 
       print('{} A TIMEOUT occured: {}'.format(timestamp(), self.timeout_hit)) 
      else: 
       print('{} No timeout occured'.format(timestamp())) 
     for process in [proc for proc in procs if proc]: 
      try: 
       process.terminate() 
      except: 
       print('{} Process termination raised trouble'.format(timestamp())) 
       raise 
     try: 
      p.stdin.close() 
     except: pass 
     if self.dbg: print('{} _closed stdin'.format(timestamp())) 
     try: 
      p.stdout.close() # If they are not closed the fds will hang around until 
     except: pass 
     if self.dbg: print('{} _closed stdout'.format(timestamp())) 
      #p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
     try: 
      p.terminate()  # Important to close the fds prior to terminating the process! 
           # NOTE: Are there any other "non-freed" resources? 
     except: pass 
     if self.dbg: print('{} _closed Popen'.format(timestamp())) 
     try: 
      self.stdout = self.stdout.decode('utf-8') 
     except: pass 
     self.returncode = p.returncode 
     if self.dbg: print('{} _closed all'.format(timestamp())) 

Sử dụng với:

import runcmd 

cmd = ['ls', '-l', '/etc'] 

worker = runcmd.RunCmd(cmd, 
         40, # limit runtime [wallclock seconds] 
         2,  # limit runtime after last output [wallclk secs] 
         ''  # stdin input string 
         ) 
(return_code, out) = worker.go() 

if worker.timeout_hit != worker.Timeout.No: 
    print('A TIMEOUT occured: {}'.format(worker.timeout_hit)) 
else: 
    print('No timeout occured') 


print("Running '{:s}' returned {:d} and {:d} chars of output".format(cmd, return_code, len(out))) 
print('Output:') 
print(out) 

command - đối số đầu tiên - phải là danh sách lệnh và các đối số của nó. Nó được sử dụng cho các cuộc gọi Popen(shell=False) và thời gian chờ của nó là trong vài giây. Hiện tại không có mã để tắt thời gian chờ. Đặt timeout_no_output thành time_runtime để vô hiệu hóa hiệu quả số an toàn có thể thu hồi được timeout_no_output. stdin_string có thể là bất kỳ chuỗi nào được gửi đến đầu vào tiêu chuẩn của lệnh. Đặt thành None nếu lệnh của bạn không cần bất kỳ đầu vào nào. Nếu một chuỗi được cung cấp, một '\ n' cuối cùng được nối vào.

Các vấn đề liên quan