Skip to content

Commit

Permalink
Add request_stop() to wrap setting the stop flag
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Mar 14, 2024
1 parent 6ea731a commit ebc42b8
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,7 +1411,7 @@ def start_emulator(

raise
finally:
emulator_streamer.stop_flag.set()
emulator_streamer.request_stop()

# Return the device ID and full name.
return device, full_name
Expand Down
16 changes: 15 additions & 1 deletion src/briefcase/integrations/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ def run(self):
"""Stream output for a Popen process."""
try:
while output_line := self._readline():
# The stop_flag is intentionally checked both at the top and bottom of
# this loop; if the flag was set during the call to readline(), then
# processing the output is skipped altogether. And if the flag is set
# as a consequence of filter_func(), the streamer still exits before
# calling readline() again and potentially blocking indefinitely.
if not self.stop_flag.is_set():
filtered_output, stop_streaming = self._filter(output_line)

Expand All @@ -198,6 +203,15 @@ def run(self):
self.logger.error(f"Error while streaming output: {type(e).__name__}: {e}")
self.logger.capture_stacktrace("Output thread")

def request_stop(self):
"""Set the stop flag to cause the streamer to exit.
If the streamer is currently blocking on `readline()` because the process'
stdout buffer is empty, then the streamer will not exit until `readline()`
returns or until Briefcase exits.
"""
self.stop_flag.set()

@property
def captured_output(self) -> str:
"""The captured output from the process."""
Expand Down Expand Up @@ -772,7 +786,7 @@ def stream_output_non_blocking(
:param label: A description of the content being streamed; used for to provide
context in logging messages.
:param popen_process: A running Popen process with output to print
:param capture_output:
:param capture_output: Retain process output instead of printing to the console
:param filter_func: A callable that will be invoked on every line of output that
is streamed. The function accepts the "raw" line of input (stripped of any
trailing newline); it returns a generator that yields the filtered output
Expand Down
28 changes: 24 additions & 4 deletions tests/integrations/subprocess/test_PopenOutputStreamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,42 @@ def monkeypatch_ensure_str(value):
)


def test_stop_flag_set_immediately(streamer, capsys):
def test_request_stop(streamer, capsys):
"""Requesting the streamer to stop sets the stop flag."""
streamer.start()
streamer.join(timeout=5)

# fmt: off
assert capsys.readouterr().out == (
"output line 1\n"
"\n"
"output line 3\n"
)
# fmt: on

assert not streamer.stop_flag.is_set()

streamer.request_stop()

assert streamer.stop_flag.is_set()


def test_request_stop_set_immediately(streamer, capsys):
"""Nothing is printed if stop flag is immediately set."""
streamer.stop_flag.set()
streamer.request_stop()

streamer.start()
streamer.join(timeout=5)

assert capsys.readouterr().out == ""


def test_stop_flag_set_during_output(streamer, monkeypatch, capsys):
def test_request_stop_set_during_output(streamer, monkeypatch, capsys):
"""Streamer prints nothing more after stop flag is set."""

def filter_func(value):
"""Simulate stop flag set while output is being read."""
streamer.stop_flag.set()
streamer.request_stop()
yield value

streamer.filter_func = filter_func
Expand Down

0 comments on commit ebc42b8

Please # to comment.