2020
2121
2222class Process (asyncio .subprocess .Process ): # noqa: D101
23- def __init__ (self , * args , no_output_timeout_secs : int | timedelta | None = None , ** kwargs ):
23+ def __init__ (
24+ self ,
25+ * args ,
26+ timeout_secs : int | timedelta | None = None ,
27+ no_output_timeout_secs : int | timedelta | None = None ,
28+ ** kwargs ,
29+ ):
2430 super ().__init__ (* args , ** kwargs )
31+ timeout_task = None
32+ if isinstance (timeout_secs , int ):
33+ assert timeout_secs >= 1
34+ timeout_secs = timedelta (seconds = timeout_secs )
35+ elif isinstance (timeout_secs , timedelta ):
36+ assert timeout_secs >= timedelta (seconds = 1 )
37+ if timeout_secs is not None :
38+ timeout_task = self ._loop .create_task ( # type: ignore[attr-defined]
39+ self ._check_timeout ()
40+ )
41+ self ._timeout_secs = timeout_secs
42+ self ._timeout_task = timeout_task
2543 no_output_timeout_task = None
2644 if isinstance (no_output_timeout_secs , int ):
2745 assert no_output_timeout_secs >= 1
@@ -35,6 +53,34 @@ def __init__(self, *args, no_output_timeout_secs: int | timedelta | None = None,
3553 self ._no_output_timeout_secs = no_output_timeout_secs
3654 self ._no_output_timeout_task = no_output_timeout_task
3755
56+ async def _check_timeout (self ):
57+ try :
58+ if TYPE_CHECKING :
59+ assert self ._timeout_secs
60+ await asyncio .sleep (self ._timeout_secs .seconds )
61+ try :
62+ self .terminate ()
63+ log .warning (
64+ "The command has been running for more than %s second(s). "
65+ "Terminating process." ,
66+ self ._timeout_secs .seconds ,
67+ )
68+ except ProcessLookupError :
69+ pass
70+ except asyncio .CancelledError :
71+ pass
72+
73+ async def _cancel_timeout_task (self ):
74+ task = self ._timeout_task
75+ if task is None :
76+ return
77+ self ._timeout_task = None
78+ if task .done ():
79+ return
80+ if not task .cancelled ():
81+ task .cancel ()
82+ await task
83+
3884 async def _check_no_output_timeout (self ):
3985 self ._protocol ._last_write = datetime .utcnow () # type: ignore[attr-defined]
4086 try :
@@ -73,6 +119,7 @@ async def wait(self):
73119 Wait until the process exit and return the process return code.
74120 """
75121 retcode = await super ().wait ()
122+ await self ._cancel_timeout_task ()
76123 await self ._cancel_no_output_timeout_task ()
77124 return retcode
78125
@@ -112,6 +159,7 @@ async def _create_subprocess_exec(
112159 stdout = None ,
113160 stderr = None ,
114161 limit = asyncio .streams ._DEFAULT_LIMIT , # type: ignore[attr-defined]
162+ timeout_secs : int | None = None ,
115163 no_output_timeout_secs : int | None = None ,
116164 capture : bool = False ,
117165 ** kwds ,
@@ -129,7 +177,13 @@ def protocol_factory():
129177 stderr = stderr ,
130178 ** kwds ,
131179 )
132- return Process (transport , protocol , loop , no_output_timeout_secs = no_output_timeout_secs )
180+ return Process (
181+ transport ,
182+ protocol ,
183+ loop ,
184+ timeout_secs = timeout_secs ,
185+ no_output_timeout_secs = no_output_timeout_secs ,
186+ )
133187
134188
135189def _handle_signal (proc , sig ):
@@ -148,6 +202,7 @@ async def _subprocess_run(
148202 future ,
149203 cmdline ,
150204 check = True ,
205+ timeout_secs : int | None = None ,
151206 no_output_timeout_secs : int | None = None ,
152207 capture : bool = False ,
153208 interactive : bool = False ,
@@ -167,6 +222,7 @@ async def _subprocess_run(
167222 stderr = stderr ,
168223 stdin = sys .stdin ,
169224 limit = 1 ,
225+ timeout_secs = timeout_secs ,
170226 no_output_timeout_secs = no_output_timeout_secs ,
171227 capture = capture ,
172228 ** kwargs ,
@@ -189,6 +245,7 @@ async def _subprocess_run(
189245def run (
190246 * cmdline ,
191247 check = True ,
248+ timeout_secs : int | None = None ,
192249 no_output_timeout_secs : int | None = None ,
193250 capture : bool = False ,
194251 interactive : bool = False ,
@@ -204,6 +261,7 @@ def run(
204261 future ,
205262 cmdline ,
206263 check ,
264+ timeout_secs = timeout_secs ,
207265 no_output_timeout_secs = no_output_timeout_secs ,
208266 capture = capture ,
209267 interactive = interactive ,
0 commit comments