Skip to content

Commit b762348

Browse files
authored
gh-364: Prompt to abort background downloads on cancel (#388)
1 parent 526f036 commit b762348

4 files changed

Lines changed: 101 additions & 2 deletions

File tree

src/manage/install_command.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,13 @@ def _find_creds(url):
137137
return None
138138

139139
ensure_tree(dest)
140-
urlretrieve(install["url"], dest, on_progress=on_progress, on_auth_request=_find_creds)
140+
urlretrieve(
141+
install["url"],
142+
dest,
143+
on_progress=on_progress,
144+
on_auth_request=_find_creds,
145+
on_cancel=lambda: cmd.ask_yn("Abort download?"),
146+
)
141147
LOGGER.debug("Downloaded to %s", dest)
142148
return dest
143149

src/manage/urlutils.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def __init__(self, url, method="GET", headers={}, outfile=None):
150150
self.proxy_settings = _proxy_settings_from_env()
151151
self._on_progress = None
152152
self._on_auth_request = None
153+
self._on_cancel = None
153154

154155
def __str__(self):
155156
return sanitise_url(self.url)
@@ -167,6 +168,11 @@ def on_auth_request(self, url=None):
167168
return self.username, self.password
168169
return None
169170

171+
def on_cancel(self):
172+
if self._on_cancel:
173+
return self._on_cancel()
174+
return False
175+
170176

171177
def _bits_urlretrieve(request):
172178
from _native import (coinitialize, bits_connect, bits_begin, bits_cancel,
@@ -234,6 +240,18 @@ def _bits_urlretrieve(request):
234240
request.on_progress(progress)
235241
last_progress = progress
236242
time.sleep(0.1)
243+
except KeyboardInterrupt:
244+
request.on_progress(None)
245+
if job and request.on_cancel():
246+
try:
247+
bits_cancel(bits, job)
248+
except OSError:
249+
LOGGER.warn("Failed to cancel background download.")
250+
LOGGER.debug("ERROR:", exc_info=True)
251+
else:
252+
if jobfile.is_file():
253+
unlink(jobfile)
254+
raise
237255
except OSError as ex:
238256
if job:
239257
bits_cancel(bits, job)
@@ -542,7 +560,8 @@ def urlopen(url, method="GET", headers={}, on_progress=None, on_auth_request=Non
542560
raise RuntimeError("Unable to download from the internet")
543561

544562

545-
def urlretrieve(url, outfile, method="GET", headers={}, chunksize=64 * 1024, on_progress=None, on_auth_request=None):
563+
def urlretrieve(url, outfile, method="GET", headers={}, chunksize=64 * 1024,
564+
on_progress=None, on_auth_request=None, on_cancel=None):
546565
scheme, sep, path = url.partition("://")
547566
if not sep:
548567
scheme = "file"
@@ -574,6 +593,7 @@ def on_progress(_): pass
574593
request.chunksize = chunksize
575594
request._on_progress = on_progress
576595
request._on_auth_request = on_auth_request
596+
request._on_cancel = on_cancel
577597

578598
first_error = None
579599

tests/test_install_command.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from manage import install_command as IC
77
from manage import installs
8+
from manage.commands import BaseCommand
89
from manage.exceptions import NoInstallFoundError
910
from manage.logging import LOGGER
1011

@@ -73,6 +74,33 @@ def test_merge_existing_index(tmp_path):
7374
]
7475

7576

77+
def test_download_package_yes_aborts_background_download(tmp_path):
78+
class Cmd:
79+
force = False
80+
bundled_dir = None
81+
source = "https://example.com/index.json"
82+
confirm = False
83+
_ask = BaseCommand._ask
84+
ask_yn = BaseCommand.ask_yn
85+
86+
def urlretrieve(url, dest, **kwargs):
87+
assert url == "https://example.com/download.zip"
88+
assert kwargs["on_cancel"]() is True
89+
dest.write_bytes(b"download")
90+
91+
dest = tmp_path / "download.zip"
92+
result = IC.download_package(
93+
Cmd(),
94+
{"url": "https://example.com/download.zip"},
95+
dest,
96+
{},
97+
urlretrieve=urlretrieve,
98+
)
99+
100+
assert result == dest
101+
assert dest.read_bytes() == b"download"
102+
103+
76104
def test_merge_existing_index_not_found(tmp_path):
77105
existing = tmp_path / "index.json"
78106
try:

tests/test_urlutils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,51 @@ def test_bits_urlretrieve_auth(local_withauth, tmp_path):
421421
assert dest.read_bytes() == b"Basic placeholder:placeholder"
422422

423423

424+
@pytest.mark.parametrize("cancel,cancel_error", [
425+
(False, False),
426+
(True, False),
427+
(True, True),
428+
])
429+
def test_bits_urlretrieve_keyboard_interrupt(
430+
monkeypatch, tmp_path, cancel, cancel_error
431+
):
432+
bits = object()
433+
job = object()
434+
cancelled = []
435+
cancel_requested = []
436+
437+
monkeypatch.setattr(_native, "coinitialize", lambda: None)
438+
monkeypatch.setattr(_native, "bits_connect", lambda: bits)
439+
monkeypatch.setattr(_native, "bits_begin", lambda *a, **k: job)
440+
monkeypatch.setattr(_native, "bits_serialize_job", lambda *a: b"job-id")
441+
442+
def bits_get_progress(*args):
443+
raise KeyboardInterrupt()
444+
445+
def bits_cancel(*args):
446+
cancelled.append(args)
447+
if cancel_error:
448+
raise OSError()
449+
450+
monkeypatch.setattr(_native, "bits_get_progress", bits_get_progress)
451+
monkeypatch.setattr(_native, "bits_cancel", bits_cancel)
452+
453+
request = UU._Request("https://example.com/download")
454+
request.outfile = tmp_path / "download.zip"
455+
progress = []
456+
request._on_progress = progress.append
457+
request._on_cancel = lambda: cancel_requested.append(True) or cancel
458+
jobfile = request.outfile.with_suffix(".job")
459+
460+
with pytest.raises(KeyboardInterrupt):
461+
UU._bits_urlretrieve(request)
462+
463+
assert cancel_requested == [True]
464+
assert progress == [None]
465+
assert bool(cancelled) == cancel
466+
assert jobfile.is_file() == (not cancel or cancel_error)
467+
468+
424469
@pytest.fixture
425470
def inject_error():
426471
try:

0 commit comments

Comments
 (0)