10
10
import shlex
11
11
import shutil
12
12
import subprocess
13
+ import time
13
14
import traceback
14
15
from enum import Enum , IntEnum
15
16
from pathlib import Path
55
56
execution_lock = tornado .locks .Lock ()
56
57
57
58
59
+ async def call_subprocess_with_authentication (
60
+ cmdline : "List[str]" ,
61
+ username : "str" ,
62
+ password : "str" ,
63
+ cwd : "Optional[str]" = None ,
64
+ env : "Optional[Dict[str, str]]" = None ,
65
+ timeout : "float" = 20 ,
66
+ ) -> "Tuple[int, str, str]" :
67
+ """Call subprocess with authentication handling."""
68
+ try :
69
+ p = pexpect .spawn (
70
+ cmdline [0 ],
71
+ cmdline [1 :],
72
+ cwd = cwd ,
73
+ env = env ,
74
+ encoding = "utf-8" ,
75
+ timeout = timeout ,
76
+ )
77
+
78
+ # We expect a prompt from git
79
+ # In most of cases git will prompt for username and
80
+ # then for password
81
+ # In some cases (Bitbucket) username is included in
82
+ # remote URL, so git will not ask for username
83
+ i = await p .expect (["Username for .*: " , "Password for .*:" ], async_ = True )
84
+ if i == 0 : # ask for username then password
85
+ p .sendline (username )
86
+ await p .expect ("Password for .*:" , async_ = True )
87
+ p .sendline (password )
88
+ elif i == 1 : # only ask for password
89
+ p .sendline (password )
90
+
91
+ await p .expect (pexpect .EOF , async_ = True )
92
+ response = p .before
93
+
94
+ returncode = p .wait ()
95
+ p .close ()
96
+ return returncode , "" , response
97
+ except pexpect .exceptions .EOF : # In case of pexpect failure
98
+ response = p .before
99
+ returncode = p .exitstatus
100
+ p .close () # close process
101
+ return returncode , "" , response
102
+ except pexpect .exceptions .TIMEOUT : # Handle timeout
103
+ p .terminate (force = True )
104
+ p .close ()
105
+ raise TimeoutError (
106
+ f"Git authentication timed out after { timeout } seconds: { ' ' .join (cmdline )} "
107
+ )
108
+ except Exception as e :
109
+ # Ensure process is always closed on any exception
110
+ if p and p .isalive ():
111
+ p .terminate (force = True )
112
+ if p :
113
+ p .close ()
114
+ raise e
115
+
116
+
117
+ def call_subprocess (
118
+ cmdline : "List[str]" ,
119
+ cwd : "Optional[str]" = None ,
120
+ env : "Optional[Dict[str, str]]" = None ,
121
+ timeout : "float" = 20 ,
122
+ is_binary = False ,
123
+ ) -> "Tuple[int, str, str]" :
124
+ """Call subprocess with timeout and process cleanup."""
125
+ process = subprocess .Popen (
126
+ cmdline , stdout = subprocess .PIPE , stderr = subprocess .PIPE , cwd = cwd , env = env
127
+ )
128
+ try :
129
+ output , error = process .communicate (timeout = timeout )
130
+ except subprocess .TimeoutExpired :
131
+ # Terminate the process gracefully
132
+ process .terminate ()
133
+ try :
134
+ process .wait (timeout = 5 )
135
+ except subprocess .TimeoutExpired :
136
+ # Force kill if still alive after 5 seconds
137
+ process .kill ()
138
+ process .wait ()
139
+ get_logger ().warning ("Git command timed out: %s" , cmdline )
140
+ raise TimeoutError (
141
+ f"Git command timed out after { timeout } seconds: { ' ' .join (cmdline )} "
142
+ )
143
+ except OSError as e :
144
+ get_logger ().warning ("Failed to execute git command: %s" , cmdline )
145
+ raise
146
+
147
+ if is_binary :
148
+ return (
149
+ process .returncode ,
150
+ base64 .encodebytes (output ).decode ("ascii" ),
151
+ error .decode ("utf-8" ),
152
+ )
153
+ else :
154
+ return (process .returncode , output .decode ("utf-8" ), error .decode ("utf-8" ))
155
+
156
+
58
157
class State (IntEnum ):
59
158
"""Git repository state."""
60
159
@@ -99,94 +198,6 @@ async def execute(
99
198
(int, str, str): (return code, stdout, stderr)
100
199
"""
101
200
102
- async def call_subprocess_with_authentication (
103
- cmdline : "List[str]" ,
104
- username : "str" ,
105
- password : "str" ,
106
- cwd : "Optional[str]" = None ,
107
- env : "Optional[Dict[str, str]]" = None ,
108
- ) -> "Tuple[int, str, str]" :
109
- try :
110
- p = pexpect .spawn (
111
- cmdline [0 ],
112
- cmdline [1 :],
113
- cwd = cwd ,
114
- env = env ,
115
- encoding = "utf-8" ,
116
- timeout = timeout ,
117
- )
118
-
119
- # We expect a prompt from git
120
- # In most of cases git will prompt for username and
121
- # then for password
122
- # In some cases (Bitbucket) username is included in
123
- # remote URL, so git will not ask for username
124
- i = await p .expect (["Username for .*: " , "Password for .*:" ], async_ = True )
125
- if i == 0 : # ask for username then password
126
- p .sendline (username )
127
- await p .expect ("Password for .*:" , async_ = True )
128
- p .sendline (password )
129
- elif i == 1 : # only ask for password
130
- p .sendline (password )
131
-
132
- await p .expect (pexpect .EOF , async_ = True )
133
- response = p .before
134
-
135
- returncode = p .wait ()
136
- p .close ()
137
- return returncode , "" , response
138
- except pexpect .exceptions .EOF : # In case of pexpect failure
139
- response = p .before
140
- returncode = p .exitstatus
141
- p .close () # close process
142
- return returncode , "" , response
143
- except pexpect .exceptions .TIMEOUT : # Handle timeout
144
- p .terminate (force = True )
145
- p .close ()
146
- raise TimeoutError (
147
- f"Git authentication timed out after { timeout } seconds: { ' ' .join (cmdline )} "
148
- )
149
- except Exception as e :
150
- # Ensure process is always closed on any exception
151
- if p and p .isalive ():
152
- p .terminate (force = True )
153
- if p :
154
- p .close ()
155
- raise e
156
-
157
- def call_subprocess (
158
- cmdline : "List[str]" ,
159
- cwd : "Optional[str]" = None ,
160
- env : "Optional[Dict[str, str]]" = None ,
161
- is_binary = is_binary ,
162
- ) -> "Tuple[int, str, str]" :
163
- process = subprocess .Popen (
164
- cmdline , stdout = subprocess .PIPE , stderr = subprocess .PIPE , cwd = cwd , env = env
165
- )
166
- try :
167
- output , error = process .communicate (timeout = timeout )
168
- except subprocess .TimeoutExpired :
169
- # Terminate the process gracefully
170
- process .terminate ()
171
- try :
172
- process .wait (timeout = 5 )
173
- except subprocess .TimeoutExpired :
174
- # Force kill if still alive after 5 seconds
175
- process .kill ()
176
- process .wait ()
177
- raise TimeoutError (
178
- f"Git command timed out after { timeout } seconds: { ' ' .join (cmdline )} "
179
- )
180
-
181
- if is_binary :
182
- return (
183
- process .returncode ,
184
- base64 .encodebytes (output ).decode ("ascii" ),
185
- error .decode ("utf-8" ),
186
- )
187
- else :
188
- return (process .returncode , output .decode ("utf-8" ), error .decode ("utf-8" ))
189
-
190
201
try :
191
202
await execution_lock .acquire (timeout = datetime .timedelta (seconds = timeout ))
192
203
except tornado .util .TimeoutError :
@@ -210,11 +221,12 @@ def call_subprocess(
210
221
password ,
211
222
cwd ,
212
223
env ,
224
+ timeout ,
213
225
)
214
226
else :
215
227
current_loop = tornado .ioloop .IOLoop .current ()
216
228
code , output , error = await current_loop .run_in_executor (
217
- None , call_subprocess , cmdline , cwd , env
229
+ None , call_subprocess , cmdline , cwd , env , timeout , is_binary
218
230
)
219
231
log_output = (
220
232
output [:MAX_LOG_OUTPUT ] + "..." if len (output ) > MAX_LOG_OUTPUT else output
@@ -271,7 +283,11 @@ def _cleanup_processes(self):
271
283
self ._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS .wait (timeout = 5 )
272
284
except subprocess .TimeoutExpired :
273
285
self ._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS .kill ()
274
- self ._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS .wait ()
286
+ try :
287
+ self ._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS .wait (timeout = 1 )
288
+ except subprocess .TimeoutExpired :
289
+ # If kill() doesn't work within 1 second, just give up
290
+ pass
275
291
get_logger ().debug ("Git credential cache daemon process (PID: %s) cleaned up successfully" , self ._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS .pid )
276
292
except Exception as e :
277
293
get_logger ().warning ("Failed to cleanup credential cache daemon: %s" , e )
0 commit comments