Skip to content

Commit 5037bf6

Browse files
authored
use upper limit for output data in Command (#4475)
fixes #4474
1 parent 78a8812 commit 5037bf6

File tree

3 files changed

+66
-20
lines changed

3 files changed

+66
-20
lines changed

tools/src/main/python/opengrok_tools/utils/command.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class Command:
5151

5252
def __init__(self, cmd, args_subst=None, args_append=None, logger=None,
5353
excl_subst=False, work_dir=None, env_vars=None, timeout=None,
54-
redirect_stderr=True, resource_limits=None, doprint=False):
54+
redirect_stderr=True, resource_limits=None, doprint=False,
55+
max_line_length=250, max_lines=10000):
5556

5657
if doprint is None:
5758
doprint = False
@@ -71,6 +72,8 @@ def __init__(self, cmd, args_subst=None, args_append=None, logger=None,
7172
self.doprint = doprint
7273
self.err = None
7374
self.returncode = None
75+
self.max_line_length = int(max_line_length)
76+
self.max_lines = int(max_lines)
7477

7578
self.logger = logger or logging.getLogger(__name__)
7679

@@ -159,14 +162,18 @@ class OutputThread(threading.Thread):
159162
stdout/stderr buffers fill up.
160163
"""
161164

162-
def __init__(self, event, logger, doprint=False):
165+
def __init__(self, event, logger, doprint=False, max_line_length=250, max_lines=10000):
163166
super(OutputThread, self).__init__()
164167
self.read_fd, self.write_fd = os.pipe()
165168
self.pipe_fobj = os.fdopen(self.read_fd, encoding='utf8')
166169
self.out = []
167170
self.event = event
168171
self.logger = logger
169172
self.doprint = doprint
173+
# Convert the maximums to integers to avoid exceptions when using them as indexes
174+
# in case they are passed as floats.
175+
self.max_line_length = int(max_line_length)
176+
self.max_lines = int(max_lines)
170177

171178
# Start the thread now.
172179
self.start()
@@ -185,17 +192,31 @@ def run(self):
185192
self.event.set()
186193
return
187194

195+
line = line.rstrip() # This will remove not only newline but also whitespace.
196+
197+
# Assuming that self.max_line_length is bigger than 3.
198+
if len(line) > self.max_line_length:
199+
line = line[:self.max_line_length] + "..."
200+
201+
# Shorten the list to be one less than the maximum because a line is going to be added.
202+
if len(self.out) >= self.max_lines:
203+
self.out = self.out[-self.max_lines + 1:]
204+
self.out[0] = "... <truncated>"
205+
188206
self.out.append(line)
189207

190208
if self.doprint:
191209
# Even if logging below fails, the thread has to keep
192210
# running to avoid hangups of the executed command.
193211
try:
194-
self.logger.info(line.rstrip())
212+
self.logger.info(line)
195213
except Exception as print_exc:
196214
self.logger.error(print_exc)
197215

198216
def getoutput(self):
217+
"""
218+
:return: list of lines with trailing whitespace (including newlines) stripped
219+
"""
199220
return self.out
200221

201222
def fileno(self):
@@ -226,18 +247,21 @@ def close(self):
226247
timeout_thread = None
227248
output_event = threading.Event()
228249
output_thread = OutputThread(output_event, self.logger,
229-
doprint=self.doprint)
250+
doprint=self.doprint,
251+
max_lines=self.max_lines,
252+
max_line_length=self.max_line_length)
230253

231-
# If stderr redirection is off, setup a thread that will capture
232-
# stderr data.
254+
# If stderr redirection is off, set up a thread that will capture stderr data.
233255
stderr_thread = None
234256
stderr_event = None
235257
if self.redirect_stderr:
236258
stderr_dest = subprocess.STDOUT
237259
else:
238260
stderr_event = threading.Event()
239261
stderr_thread = OutputThread(stderr_event, self.logger,
240-
doprint=self.doprint)
262+
doprint=self.doprint,
263+
max_lines=self.max_lines,
264+
max_line_length=self.max_line_length)
241265
stderr_dest = stderr_thread
242266

243267
start_time = None
@@ -403,11 +427,18 @@ def getretcode(self):
403427

404428
def getoutputstr(self):
405429
if self.state == Command.FINISHED:
406-
return "".join(self.out).strip()
430+
s = os.linesep.join(self.out)
431+
if self.out:
432+
s += os.linesep
433+
return s
407434
else:
408435
return None
409436

410437
def getoutput(self):
438+
"""
439+
:return: list of lines (with trailing whitespace and newlines stripped)
440+
or None if the command has not finished yet
441+
"""
411442
if self.state == Command.FINISHED:
412443
return self.out
413444
else:
@@ -418,7 +449,10 @@ def geterroutput(self):
418449

419450
def geterroutputstr(self):
420451
if self.err:
421-
return "".join(self.err).strip()
452+
s = os.linesep.join(self.err)
453+
if len(self.err) > 0:
454+
s += os.linesep
455+
return s
422456
else:
423457
return ""
424458

tools/src/test/python/test_command.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,15 @@ def test_execute_nonexistent():
9696
assert cmd.getstate() == Command.ERRORED
9797

9898

99+
# Uses /bin/ls, therefore Unix only.
99100
@posix_only
100101
def test_getoutput():
101102
cmd = Command(['/bin/ls', '/etc/passwd'])
102103
cmd.execute()
103-
assert cmd.getoutput() == ['/etc/passwd\n']
104+
assert cmd.getoutput() == ['/etc/passwd']
104105

105106

107+
# Uses /bin/ls, therefore Unix only.
106108
@posix_only
107109
def test_work_dir():
108110
os.chdir("/")
@@ -118,7 +120,7 @@ def test_work_dir():
118120
def test_env(env_binary):
119121
cmd = Command([env_binary], env_vars={'FOO': 'BAR', 'A': 'B'})
120122
cmd.execute()
121-
assert "FOO=BAR\n" in cmd.getoutput()
123+
assert "FOO=BAR" in cmd.getoutput()
122124

123125

124126
@system_binary('true')
@@ -181,16 +183,17 @@ def test_stderr():
181183

182184
# This test needs the "/bin/cat" command, therefore it is Unix only.
183185
@posix_only
184-
def test_long_output():
186+
@pytest.mark.parametrize('shorten', [True, False])
187+
def test_long_output(shorten):
185188
"""
186-
Test that output thread in the Command class captures all of the output.
189+
Test that output thread in the Command class captures all the output.
187190
(and also it does not hang the command by filling up the pipe)
188191
189-
By default stderr is redirected to stdout.
192+
By default, stderr is redirected to stdout.
190193
"""
191-
# in bytes, should be enough to fill a pipe
192194
num_lines = 5000
193195
line_length = 1000
196+
# should be enough to fill a pipe
194197
num_bytes = num_lines * (line_length + 1)
195198
with tempfile.NamedTemporaryFile() as file:
196199
for _ in range(num_lines):
@@ -199,13 +202,22 @@ def test_long_output():
199202
file.flush()
200203
assert os.path.getsize(file.name) == num_bytes
201204

202-
cmd = Command(["/bin/cat", file.name])
205+
if shorten:
206+
num_lines //= 2
207+
line_length //= 2
208+
cmd = Command(["/bin/cat", file.name],
209+
max_lines=num_lines, max_line_length=line_length)
203210
cmd.execute()
204211

205212
assert cmd.getstate() == Command.FINISHED
206213
assert cmd.getretcode() == 0
207214
assert cmd.geterroutput() is None
208-
assert len("".join(cmd.getoutput())) == num_bytes
215+
assert len(cmd.getoutput()) == num_lines
216+
if shorten:
217+
# Add 3 for the '...' suffix.
218+
assert all(len(x) <= line_length + 3 for x in cmd.getoutput())
219+
else:
220+
assert len("\n".join(cmd.getoutput()) + "\n") == num_bytes
209221

210222

211223
@posix_only

tools/src/test/python/test_command_sequence.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def test_project_subst():
173173
cmds = CommandSequence(CommandSequenceBase("test-subst", cmd_list))
174174
cmds.run()
175175

176-
assert cmds.outputs['/bin/echo test-subst'] == ['test-subst\n']
176+
assert cmds.outputs['/bin/echo test-subst'] == ['test-subst']
177177

178178

179179
@pytest.mark.skipif(not os.path.exists('/bin/echo'),
@@ -184,7 +184,7 @@ def test_args_subst():
184184
cmds = CommandSequence(CommandSequenceBase("test-subst", cmd_list))
185185
cmds.run()
186186

187-
assert cmds.outputs['/bin/echo foo'] == ['foo\n']
187+
assert cmds.outputs['/bin/echo foo'] == ['foo']
188188

189189

190190
@pytest.mark.skipif(not os.path.exists('/bin/echo'),
@@ -197,7 +197,7 @@ def test_args_subst_env():
197197
cmds.run()
198198
os.environ.pop("FOO")
199199

200-
assert cmds.outputs['/bin/echo bar'] == ['bar\n']
200+
assert cmds.outputs['/bin/echo bar'] == ['bar']
201201

202202

203203
def test_cleanup_exception():

0 commit comments

Comments
 (0)