') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); gh-48: Report slow downloads by Punisheroot · Pull Request #391 · python/pymanager · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/manage/urlutils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,6 +148,7 @@ def __init__(self, url, method="GET", headers={}, outfile=None):
self.password = None
self.outfile = Path(outfile) if outfile else None
self.proxy_settings = _proxy_settings_from_env()
self._download_start = time.monotonic()
self._on_progress = None
self._on_auth_request = None
self._on_cancel = None
Expand All@@ -158,6 +159,15 @@ def __str__(self):
def on_progress(self, progress):
if self._on_progress:
self._on_progress(progress)
elif (self._download_start is not None
and progress is not None and progress < 100
and time.monotonic() - self._download_start > 5):
LOGGER.warn(
"Downloading %s is taking some time. Please continue to wait, "
"or press Ctrl+C to abort.",
self,
)
self._download_start = None

def on_auth_request(self, url=None):
if url is None:
Expand DownExpand Up@@ -236,7 +246,7 @@ def _bits_urlretrieve(request):
# Returned HTTP status 404 (0x194)
raise FileNotFoundError() from ex
raise
if progress > last_progress:
if progress > last_progress or not request._on_progress:
request.on_progress(progress)
last_progress = progress
time.sleep(0.1)
Expand DownExpand Up@@ -320,6 +330,8 @@ def _urllib_urlopen(request):
raise FileNotFoundError from ex
else:
raise
if not request._on_progress:
request.on_progress(0)
with r:
data = r.read()
request.on_progress(100)
Expand DownExpand Up@@ -349,6 +361,8 @@ def _urllib_urlretrieve(request):
r = urlopen(req)
else:
raise
if not request._on_progress:
request.on_progress(0)
with r:
progress = 0
try:
Expand DownExpand Up@@ -467,20 +481,23 @@ def _powershell_urlretrieve(request):
stderr=subprocess.STDOUT,
) as p:
request.on_progress(0)
start = time.time()
start = time.monotonic()
timeout = 10.0 if request._on_progress else 1.0
while True:
try:
try:
out = p.communicate(b'', timeout=10.0)[0].decode("utf-8", "replace")
out = p.communicate(b'', timeout=timeout)[0].decode("utf-8", "replace")
if '<S S="Error">Invoke-WebRequest' in out:
raise RuntimeError("Powershell download failed:" + out)
request.on_progress(100)
LOGGER.debug("PowerShell Output: %s", out)
return
except subprocess.TimeoutExpired:
if not request.outfile.exists():
request.on_progress(0)
elapsed = time.monotonic() - start
if not request.outfile.exists() and elapsed >= 10:
# Suppress the original exception to avoid leaking the command
raise subprocess.TimeoutExpired(powershell, int(time.time() - start)) from None
raise subprocess.TimeoutExpired(powershell, int(elapsed)) from None
except:
p.terminate()
out = p.communicate()[0]
Expand Down
139 changes: 139 additions & 0 deletions tests/test_urlutils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -295,6 +295,145 @@ def local_withauth(localserver):
yield req


def test_slow_download_warning(monkeypatch, assert_log):
now = [10.0]
monkeypatch.setattr(UU.time, "monotonic", lambda: now[0])

def urlopen(request):
request.on_progress(0)
now[0] += 5
request.on_progress(50)
assert_log(assert_log.not_logged("Downloading .+ is taking some time.+"))

now[0] += 0.1
request.on_progress(50)
request.on_progress(75)
return b"download"

monkeypatch.setattr(UU, "ENABLE_WINHTTP", True)
monkeypatch.setattr(UU, "_winhttp_urlopen", urlopen)

result = UU.urlopen("https://user:pass@example.com/index.json")

assert result == b"download"
assert_log(
(
"Downloading %s is taking some time. Please continue to wait, "
"or press Ctrl\\+C to abort.",
["https://example.com/index.json"],
),
assert_log.end_of_log(),
)


def test_slow_download_warning_not_emitted_on_completion(monkeypatch, assert_log):
now = [10.0]
monkeypatch.setattr(UU.time, "monotonic", lambda: now[0])
request = UU._Request("https://example.com/index.json")

now[0] += 5.1
request.on_progress(100)

assert_log(assert_log.not_logged("Downloading .+ is taking some time.+"))


def test_slow_download_warning_suppressed_by_progress(monkeypatch, assert_log):
now = [10.0]
monkeypatch.setattr(UU.time, "monotonic", lambda: now[0])
request = UU._Request("https://example.com/index.json")
progress = []
request._on_progress = progress.append

now[0] += 5.1
request.on_progress(50)

assert progress == [50]
assert_log(assert_log.not_logged("Downloading .+ is taking some time.+"))


def test_powershell_slow_download_warning(monkeypatch, assert_log, tmp_path):
import shutil
import subprocess

now = [10.0]
monkeypatch.setattr(UU.time, "monotonic", lambda: now[0])
monkeypatch.setattr(shutil, "which", lambda _: "powershell.exe")

class Process:
def __init__(self, *args, **kwargs):
pass

def __enter__(self):
return self

def __exit__(self, *exc_info):
pass

def communicate(self, _=None, timeout=None):
if timeout is None:
return (b"",)
now[0] += timeout
if now[0] <= 16:
raise subprocess.TimeoutExpired("powershell.exe", timeout)
return (b"",)

def terminate(self):
pass

monkeypatch.setattr(subprocess, "Popen", Process)

request = UU._Request("https://user:pass@example.com/index.json")
request.outfile = tmp_path / "index.json"

UU._powershell_urlretrieve(request)

warning = (
"Downloading %s is taking some time. Please continue to wait, "
"or press Ctrl+C to abort."
)
assert_log(assert_log.skip_until(
warning.replace("+", "\\+"),
["https://example.com/index.json"],
))
assert sum(1 for msg, _ in assert_log if msg == warning) == 1


def test_bits_slow_download_warning(monkeypatch, assert_log, tmp_path):
bits = object()
job = object()
now = [10.0]
progress = iter([0] * 52 + [100])

def sleep(delay):
now[0] += delay

monkeypatch.setattr(UU.time, "monotonic", lambda: now[0])
monkeypatch.setattr(UU.time, "sleep", sleep)
monkeypatch.setattr(_native, "coinitialize", lambda: None, raising=False)
monkeypatch.setattr(_native, "bits_connect", lambda: bits, raising=False)
monkeypatch.setattr(_native, "bits_begin", lambda *a, **k: job, raising=False)
monkeypatch.setattr(_native, "bits_cancel", lambda *a: None, raising=False)
monkeypatch.setattr(_native, "bits_get_progress", lambda *a: next(progress), raising=False)
monkeypatch.setattr(_native, "bits_retry_with_auth", lambda *a: None, raising=False)
monkeypatch.setattr(_native, "bits_find_job", lambda *a: None, raising=False)
monkeypatch.setattr(_native, "bits_serialize_job", lambda *a: b"job-id", raising=False)

request = UU._Request("https://user:pass@example.com/download.zip")
request.outfile = tmp_path / "download.zip"

UU._bits_urlretrieve(request)

warning = (
"Downloading %s is taking some time. Please continue to wait, "
"or press Ctrl+C to abort."
)
assert_log(assert_log.skip_until(
warning.replace("+", "\\+"),
["https://example.com/download.zip"],
))
assert sum(1 for msg, _ in assert_log if msg == warning) == 1


def test_urllib_urlretrieve(local_128kb, tmp_path):
local_128kb.outfile = dest = tmp_path / "read.txt"
progress = local_128kb.progress
Expand Down
Loading