Skip to content

Commit 41e93f7

Browse files
author
Vladimir Kotal
authored
Mirror timeout (#2093)
make it possible to specify timeout for mirroring commands
1 parent 8626e94 commit 41e93f7

File tree

12 files changed

+243
-51
lines changed

12 files changed

+243
-51
lines changed

tools/sync/command.py

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
import subprocess
2727
import string
2828
import threading
29+
import time
30+
31+
32+
class TimeoutException(Exception):
33+
"""
34+
Exception returned when command exceeded its timeout.
35+
"""
36+
pass
2937

3038

3139
class Command:
@@ -38,14 +46,17 @@ class Command:
3846
FINISHED = "finished"
3947
INTERRUPTED = "interrupted"
4048
ERRORED = "errored"
49+
TIMEDOUT = "timed out"
4150

4251
def __init__(self, cmd, args_subst=None, args_append=None, logger=None,
43-
excl_subst=False, work_dir=None, env_vars=None):
52+
excl_subst=False, work_dir=None, env_vars=None, timeout=None):
4453
self.cmd = cmd
4554
self.state = "notrun"
4655
self.excl_subst = excl_subst
4756
self.work_dir = work_dir
4857
self.env_vars = env_vars
58+
self.timeout = timeout
59+
self.pid = None
4960

5061
self.logger = logger or logging.getLogger(__name__)
5162
logging.basicConfig()
@@ -61,6 +72,41 @@ def execute(self):
6172
Execute the command and capture its output and return code.
6273
"""
6374

75+
class TimeoutThread(threading.Thread):
76+
"""
77+
Wait until the timeout specified in seconds expires and kill
78+
the process specified by the Popen object after that.
79+
If timeout expires, TimeoutException is stored in the object
80+
and can be retrieved by the caller.
81+
"""
82+
83+
def __init__(self, logger, timeout, condition, p):
84+
super(TimeoutThread, self).__init__()
85+
self.timeout = timeout
86+
self.popen = p
87+
self.condition = condition
88+
self.logger = logger
89+
self.start()
90+
self.exception = None
91+
92+
def run(self):
93+
with self.condition:
94+
if not self.condition.wait(self.timeout):
95+
p = self.popen
96+
self.logger.info("Terminating command {} with PID {} "
97+
"after timeout of {} seconds".
98+
format(p.args, p.pid, self.timeout))
99+
p.terminate()
100+
self.exception = TimeoutException("Command {} with pid"
101+
" {} timed out".
102+
format(p.args,
103+
p.pid))
104+
else:
105+
return None
106+
107+
def get_exception(self):
108+
return self.exception
109+
64110
class OutputThread(threading.Thread):
65111
"""
66112
Capture data from subprocess.Popen(). This avoids hangs when
@@ -116,33 +162,59 @@ def close(self):
116162
format(self.work_dir), exc_info=True)
117163
return
118164

119-
othr = OutputThread()
165+
timeout_thread = None
166+
output_thread = OutputThread()
120167
try:
168+
start_time = time.time()
121169
self.logger.debug("working directory = {}".format(os.getcwd()))
122170
self.logger.debug("command = {}".format(self.cmd))
123171
if self.env_vars:
124172
my_env = os.environ.copy()
125173
my_env.update(self.env_vars)
126174
p = subprocess.Popen(self.cmd, stderr=subprocess.STDOUT,
127-
stdout=othr, env=my_env)
175+
stdout=output_thread, env=my_env)
128176
else:
129177
p = subprocess.Popen(self.cmd, stderr=subprocess.STDOUT,
130-
stdout=othr)
178+
stdout=output_thread)
179+
180+
self.pid = p.pid
181+
182+
if self.timeout:
183+
condition = threading.Condition()
184+
self.logger.debug("Setting timeout to {}".format(self.timeout))
185+
timeout_thread = TimeoutThread(self.logger, self.timeout,
186+
condition, p)
187+
188+
self.logger.debug("Waiting for process with PID {}".format(p.pid))
131189
p.wait()
190+
191+
if self.timeout:
192+
e = timeout_thread.get_exception()
193+
if e:
194+
raise e
132195
except KeyboardInterrupt as e:
133196
self.logger.info("Got KeyboardException while processing ",
134197
exc_info=True)
135198
self.state = Command.INTERRUPTED
136199
except OSError as e:
137200
self.logger.error("Got OS error", exc_info=True)
138201
self.state = Command.ERRORED
202+
except TimeoutException as e:
203+
self.logger.error("Timed out")
204+
self.state = Command.TIMEDOUT
139205
else:
140206
self.state = Command.FINISHED
141207
self.returncode = int(p.returncode)
142208
self.logger.debug("{} -> {}".format(self.cmd, self.getretcode()))
143209
finally:
144-
othr.close()
145-
self.out = othr.getoutput()
210+
if self.timeout != 0 and timeout_thread:
211+
with condition:
212+
condition.notifyAll()
213+
output_thread.close()
214+
self.out = output_thread.getoutput()
215+
elapsed_time = time.time() - start_time
216+
self.logger.debug("Command {} took {} seconds".
217+
format(self.cmd, int(elapsed_time)))
146218

147219
if orig_work_dir:
148220
try:
@@ -199,3 +271,6 @@ def getoutput(self):
199271

200272
def getstate(self):
201273
return self.state
274+
275+
def getpid(self):
276+
return self.pid

tools/sync/cvs.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828

2929
class CVSRepository(Repository):
30-
def __init__(self, logger, path, project, command, env, hooks):
30+
def __init__(self, logger, path, project, command, env, hooks, timeout):
3131

32-
super().__init__(logger, path, project, command, env, hooks)
32+
super().__init__(logger, path, project, command, env, hooks, timeout)
3333

3434
if command:
3535
self.command = command
@@ -42,7 +42,8 @@ def __init__(self, logger, path, project, command, env, hooks):
4242

4343
def reposync(self):
4444
hg_command = [self.command, "update", "-dP"]
45-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
45+
cmd = self.getCommand(hg_command, work_dir=self.path,
46+
env_vars=self.env, logger=self.logger)
4647
cmd.execute()
4748
self.logger.info(cmd.getoutputstr())
4849
if cmd.getretcode() != 0 or cmd.getstate() != Command.FINISHED:

tools/sync/git.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828

2929
class GitRepository(Repository):
30-
def __init__(self, logger, path, project, command, env, hooks):
30+
def __init__(self, logger, path, project, command, env, hooks, timeout):
3131

32-
super().__init__(logger, path, project, command, env, hooks)
32+
super().__init__(logger, path, project, command, env, hooks, timeout)
3333

3434
if command:
3535
self.command = command
@@ -42,7 +42,8 @@ def __init__(self, logger, path, project, command, env, hooks):
4242

4343
def reposync(self):
4444
hg_command = [self.command, "pull", "--ff-only"]
45-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
45+
cmd = self.getCommand(hg_command, work_dir=self.path,
46+
env_vars=self.env, logger=self.logger)
4647
cmd.execute()
4748
self.logger.info(cmd.getoutputstr())
4849
if cmd.getretcode() != 0 or cmd.getstate() != Command.FINISHED:

tools/sync/hook.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import logging
2727

2828

29-
def run_hook(logger, script, path, env):
29+
def run_hook(logger, script, path, env, timeout):
3030
"""
3131
Change a working directory to specified path, run a command
3232
and change the working directory back to its original value.
@@ -37,7 +37,8 @@ def run_hook(logger, script, path, env):
3737
ret = 0
3838
logger.debug("Running hook '{}' in directory {}".
3939
format(script, path))
40-
cmd = Command([script], work_dir=path, env_vars=env)
40+
cmd = Command([script], logger=logger, work_dir=path, env_vars=env,
41+
timeout=timeout)
4142
cmd.execute()
4243
if cmd.state is not "finished" or cmd.getretcode() != 0:
4344
logger.error("command failed: {} -> {}".format(cmd, cmd.getretcode()))

tools/sync/mercurial.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828

2929
class MercurialRepository(Repository):
30-
def __init__(self, logger, path, project, command, env, hooks):
30+
def __init__(self, logger, path, project, command, env, hooks, timeout):
3131

32-
super().__init__(logger, path, project, command, env, hooks)
32+
super().__init__(logger, path, project, command, env, hooks, timeout)
3333

3434
if command:
3535
self.command = command
@@ -42,7 +42,8 @@ def __init__(self, logger, path, project, command, env, hooks):
4242

4343
def get_branch(self):
4444
hg_command = [self.command, "branch"]
45-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
45+
cmd = self.getCommand(hg_command, work_dir=self.path,
46+
env_vars=self.env, logger=self.logger)
4647
cmd.execute()
4748
if cmd.getstate() != Command.FINISHED:
4849
self.logger.debug(cmd.getoutput())
@@ -66,7 +67,8 @@ def reposync(self):
6667
if branch != "default":
6768
hg_command.append("-b")
6869
hg_command.append(branch)
69-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
70+
cmd = self.getCommand(hg_command, work_dir=self.path,
71+
env_vars=self.env, logger=self.logger)
7072
cmd.execute()
7173
self.logger.info(cmd.getoutputstr())
7274
#
@@ -81,7 +83,8 @@ def reposync(self):
8183
if branch != "default":
8284
hg_command.append("-b")
8385
hg_command.append(branch)
84-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
86+
cmd = self.getCommand(hg_command, work_dir=self.path,
87+
env_vars=self.env, logger=self.logger)
8588
cmd.execute()
8689
self.logger.info(cmd.getoutputstr())
8790
if cmd.getretcode() != 0 or cmd.getstate() != Command.FINISHED:
@@ -93,7 +96,8 @@ def reposync(self):
9396
# some servers do not support it.
9497
if branch == "default":
9598
hg_command.append("--check")
96-
cmd = Command(hg_command, work_dir=self.path, env_vars=self.env)
99+
cmd = self.getCommand(hg_command, work_dir=self.path,
100+
env_vars=self.env, logger=self.logger)
97101
cmd.execute()
98102
self.logger.info(cmd.getoutputstr())
99103
if cmd.getretcode() != 0 or cmd.getstate() != Command.FINISHED:

0 commit comments

Comments
 (0)