aboutsummaryrefslogtreecommitdiff
blob: b5fa4764aeaa55feaababb015d5f3677efa91dd1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# R overlay -- tools, subprocess helpers
# -*- coding: utf-8 -*-
# Copyright (C) 2014 André Erdmann <dywi@mailerd.de>
# Distributed under the terms of the GNU General Public License;
# either version 2 of the License, or (at your option) any later version.

import os
import subprocess
import sys
import time

__all__ = [
   'get_subproc_devnull',
   'subproc_send_term', 'subproc_send_kill',
   'stop_subprocess', 'gracefully_stop_subprocess',
   'create_subprocess', 'run_subprocess'
]

# python >= 3.3 has ProcessLookupError, use more generic exception otherwise
if sys.hexversion >= 0x3030000:
   _ProcessLookupError = ProcessLookupError
else:
   _ProcessLookupError = OSError

# python >= 3.3:
if hasattr ( subprocess, 'DEVNULL' ):
   def get_subproc_devnull(mode=None):
      """Returns a devnull object suitable
      for passing it as stdin/stdout/stderr to subprocess.Popen().

      Python 3.3 and later variant: uses subprocess.DEVNULL

      arguments:
      * mode -- ignored
      """
      return subprocess.DEVNULL
else:
   def get_subproc_devnull(mode='a+'):
      """Returns a devnull object suitable
      for passing it as stdin/stdout/stderr to subprocess.Popen().

      Python 3.2 and earlier variant: opens os.devnull

      arguments:
      * mode -- mode for open(). Defaults to read/append
      """
      return open ( os.devnull, mode )
# --

def _proc_catch_lookup_err ( func ):
   def wrapped ( proc ):
      try:
         func ( proc )
      except _ProcessLookupError:
         return False
      return True

   return wrapped

@_proc_catch_lookup_err
def subproc_send_term ( proc ):
   proc.terminate()

@_proc_catch_lookup_err
def subproc_send_kill ( proc ):
   proc.kill()


def stop_subprocess ( proc, kill_timeout_cs=10 ):
   """Terminates or kills a subprocess created by subprocess.Popen().

   Sends SIGTERM first and sends SIGKILL if the process is still alive after
   the given timeout.

   Returns: None

   arguments:
   * proc            -- subprocess
   * kill_timeout_cs -- max time to wait after terminate() before sending a
                        kill signal (in centiseconds). Should be an int.
                        Defaults to 10 (= 1s).
   """
   if not subproc_send_term ( proc ):
      return

   try:
      for k in range ( kill_timeout_cs ):
         if proc.poll() is not None:
            return
         time.sleep ( 0.1 )
   except:
      subproc_send_kill ( proc )
   else:
      subproc_send_kill ( proc )
# --- end of stop_subprocess (...) ---

def gracefully_stop_subprocess ( proc, **kill_kwargs ):
   try:
      if subproc_send_term ( proc ):
         proc.communicate()
   except:
      stop_subprocess ( proc, **kill_kwargs )
      raise

def create_subprocess ( cmdv, **kwargs ):
   """subprocess.Popen() wrapper that redirects stdin/stdout/stderr to
   devnull or to a pipe if set to False/True.

   Returns: subprocess

   arguments:
   * cmdv     --
   * **kwargs --
   """
   devnull_obj = None

   for key in { 'stdin', 'stdout', 'stderr' }:
      if key not in kwargs:
         pass
      elif kwargs [key] is True:
         kwargs [key] = subprocess.PIPE

      elif kwargs [key] is False:
         if devnull_obj is None:
            devnull_obj = get_subproc_devnull()
            assert devnull_obj is not None

         kwargs [key] = devnull_obj
      # else don't care
   # --

   return subprocess.Popen ( cmdv, **kwargs )
# --- end of create_subprocess (...) ---

def run_subprocess ( cmdv, kill_timeout_cs=10, **kwargs ):
   """Calls create_subprocess() and waits for the process to exit.
   Catches exceptions and terminates/kills the process in that case

   Returns: 2-tuple ( subprocess, output )

   arguments:
   * cmdv            --
   * kill_timeout_cs -- time to wait after SIGTERM before sending SIGKILL
                        (in centiseconds), see stop_subprocess() for details
                        Defaults to 10.
   * **kwargs        --
   """
   proc = None
   try:
      proc   = create_subprocess ( cmdv, **kwargs )
      output = proc.communicate()
   except:
      if proc is not None:
         stop_subprocess ( proc, kill_timeout_cs=kill_timeout_cs )
      raise

   return ( proc, output )
# --- end of run_subprocess (...) ---