Repository: miurahr/aqtinstall
Pull request #419 (2021-10-03 01:28:01)

Propagate exceptions from `run_installer()`
This change catches exceptions derived from Exception and KeyboardInterrupt raised by `aqt.installer.installer()`, while run by multiple processes, and propagates them back to earlier stack entries. This will prevent any OSError and BrokenPipe exceptions that would otherwise be raised when one process has an exception while the other processes are still running.

This also handles the MemoryError exception we saw in #416, and offers some suggestions for solving the issue.


Patch:
diff --git a/aqt/exceptions.py b/aqt/exceptions.py
index 5774396..ae4a8c1 100644
--- a/aqt/exceptions.py
+++ b/aqt/exceptions.py
@@ -73,3 +73,7 @@ class ArchiveExtractionError(AqtException):
 
 class UpdaterError(AqtException):
     pass
+
+
+class OutOfMemory(AqtException):
+    pass
diff --git a/aqt/installer.py b/aqt/installer.py
index 2059c0e..05867dc 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -45,6 +45,7 @@ from aqt.exceptions import (
     ArchiveListError,
     CliInputError,
     CliKeyboardInterrupt,
+    OutOfMemory,
 )
 from aqt.helper import MyQueueListener, Settings, downloadBinaryFile, getUrl, setup_logging
 from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
@@ -814,7 +815,6 @@ class Cli:
 
 
 def run_installer(archives: List[QtPackage], base_dir: str, sevenzip: Optional[str], keep: bool):
-    key_interrupt = False
     queue = multiprocessing.Manager().Queue(-1)
     listener = MyQueueListener(queue)
     listener.start()
@@ -824,21 +824,38 @@ def run_installer(archives: List[QtPackage], base_dir: str, sevenzip: Optional[s
         tasks.append((arc, base_dir, sevenzip, queue, keep))
     ctx = multiprocessing.get_context("spawn")
     pool = ctx.Pool(Settings.concurrency, init_worker_sh)
+
+    def close_worker_pool_on_exception(exception: BaseException):
+        logger = getLogger("aqt.installer")
+        logger.warning(f"Caught {exception.__class__.__name__}, terminating installer workers")
+        pool.terminate()
+        pool.join()
+
     try:
         pool.starmap(installer, tasks)
         pool.close()
         pool.join()
-    except KeyboardInterrupt:
-        logger = getLogger("aqt.installer")
-        logger.warning("Caught KeyboardInterrupt, terminating installer workers")
-        pool.terminate()
-        pool.join()
-        key_interrupt = True
-    # all done, close logging service for sub-processes
-    listener.enqueue_sentinel()
-    listener.stop()
-    if key_interrupt:
-        raise CliKeyboardInterrupt("Installer halted by keyboard interrupt.")
+    except KeyboardInterrupt as e:
+        close_worker_pool_on_exception(e)
+        raise CliKeyboardInterrupt("Installer halted by keyboard interrupt.") from e
+    except MemoryError as e:
+        close_worker_pool_on_exception(e)
+        if Settings.concurrency > 1:
+            docs_url = "https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration"
+            raise OutOfMemory(
+                "Out of memory when downloading and extracting archives in parallel.",
+                suggested_action=[f"Please reduce your 'concurrency' setting (see {docs_url})"],
+            ) from e
+        raise OutOfMemory(
+            "Out of memory when downloading and extracting archives.", suggested_action=["Please free up more memory."]
+        )
+    except Exception as e:
+        close_worker_pool_on_exception(e)
+        raise e from e
+    finally:
+        # all done, close logging service for sub-processes
+        listener.enqueue_sentinel()
+        listener.stop()
 
 
 def init_worker_sh():


Test patch:
diff --git a/tests/data/settings_no_concurrency.ini b/tests/data/settings_no_concurrency.ini
new file mode 100644
index 0000000..79568bd
--- /dev/null
+++ b/tests/data/settings_no_concurrency.ini
@@ -0,0 +1,222 @@
+[DEFAULTS]
+
+[aqt]
+concurrency: 1
+baseurl: https://download.qt.io
+7zcmd: 7z
+print_stacktrace_on_error: False
+
+[requests]
+connection_timeout: 3.5
+response_timeout: 30
+max_retries: 5
+retry_backoff: 0.1
+
+[mirrors]
+blacklist:
+    http://mirrors.ocf.berkeley.edu
+    http://mirrors.tuna.tsinghua.edu.cn
+    http://mirrors.geekpie.club
+fallbacks:
+    https://ftp.jaist.ac.jp/pub/qtproject
+    http://ftp1.nluug.nl/languages/qt
+    https://mirrors.dotsrc.org/qtproject
+
+[kde_patches]
+patches:
+    0001-toolchain.prf-Use-vswhere-to-obtain-VS-installation-.patch
+    0002-Fix-allocated-memory-of-QByteArray-returned-by-QIODe.patch
+    0003-Update-CLDR-to-v37-adding-Nigerian-Pidgin-as-a-new-l.patch
+    0004-QLayout-docs-explain-better-what-the-QWidget-ctor-ar.patch
+    0005-QMacStyle-fix-tab-rendering.patch
+    0006-QMacStyle-more-pixel-refinements.patch
+    0007-Deprecate-ordering-on-QItemSelectionRange.patch
+    0008-Deprecate-QLocale-Language-entries-that-no-locale-da.patch
+    0009-Deprecate-old-aliases-for-two-countries-and-several-.patch
+    0010-QAbstractItemModelTester-don-t-rely-on-hasChildren.patch
+    0011-Doc-Remove-mentioning-of-old-macos-versions-from-QSe.patch
+    0012-Pass-SameSite-through-QNetworkCookie.patch
+    0013-doc-fix-typo-consise-concise.patch
+    0014-Selftest-copy-XAUTHORITY-environment-variable.patch
+    0015-Fix-delay-first-time-a-font-is-used.patch
+    0016-Fix-QScreen-orientation-not-being-updated-when-setti.patch
+    0017-Android-Request-cursor-to-be-in-proper-position.patch
+    0018-testlib-Let-logger-report-whether-it-is-logging-to-s.patch
+    0019-Android-disable-Gradle-caching-by-default.patch
+    0020-Android-replace-stacktrace-with-debug-message-in-sea.patch
+    0021-Use-void-instead-of-Q_UNUSED.patch
+    0022-Don-t-show-QPushButton-as-hovered-unless-the-mouse-i.patch
+    0023-MinGW-Fix-assert-in-QCoreApplication-arguments-when-.patch
+    0024-QCombobox-propagate-the-palette-to-the-embedded-line.patch
+    0025-QMarginsF-document-that-isNull-operator-operator-are.patch
+    0026-qmake-vcxproj-Fix-handling-of-extra-compiler-outputs.patch
+    0027-Android-fix-documentation-about-ANDROID_EXTRA_LIBS.patch
+    0028-Offscreen-QPA-implement-a-native-interface.patch
+    0029-DropSite-example-support-markdown.patch
+    0030-Do-not-define-dynamic_cast.patch
+    0031-Android-fix-crash-by-passing-the-right-Handle-to-dls.patch
+    0032-testlib-Add-private-API-to-add-test-logger.patch
+    0033-Update-third-party-md4c-to-version-0.4.6.patch
+    0034-moc-Handle-include-in-enum-take-2.patch
+    0035-InputMethod-should-call-reset-function-when-proxywid.patch
+    0036-Add-possibility-to-set-QNX-Screen-pipeline-value.patch
+    0037-qglobal-Only-define-QT_ENSURE_STACK_ALIGNED_FOR_SSE-.patch
+    0038-macOS-FreeType-fix-crash-with-non-printable-unicode.patch
+    0039-Linux-fix-crash-in-AtSpi-adaptor-when-handling-windo.patch
+    0040-Add-_MSC_VER-check-to-MSVC-ARM-compiler-workaround.patch
+    0041-QMap-suppress-warning-about-strict-aliasing-violatio.patch
+    0042-Add-changes-file-for-Qt-5.15.2.patch
+    0043-Set-the-url-to-have-the-AtNx-filename-if-one-is-foun.patch
+    0044-QMap-don-t-tell-everyone-QMapNode-has-no-friends.patch
+    0045-QNAM-Work-around-QObject-finicky-orphan-cleanup-deta.patch
+    0046-xcb-ensure-that-available-glx-version-is-greater-tha.patch
+    0047-Protect-QImage-colorspace-transform-on-shutdown.patch
+    0048-Fix-qstylesheetstyle-clip-border-error.patch
+    0049-Correct-processEvents-documentation.patch
+    0050-Update-CLDR-to-v38.patch
+    0051-Fix-compilation-when-using-no-mimetype-database.patch
+    0052-Reduce-memory-reallocations-in-QTextTablePrivate-upd.patch
+    0053-Fix-regular-expression-initialize-with-incorrect-fil.patch
+    0054-Cocoa-Allow-CMD-H-to-hide-the-application-when-a-too.patch
+    0055-Fix-pcre2-feature-conditions.patch
+    0056-Q_PRIMITIVE_TYPE-improve-the-documentation.patch
+    0057-QAsn1Element-Read-value-in-blocks-to-avoid-oom-at-wr.patch
+    0058-Android-Don-t-use-putIfAbsent-as-that-is-not-availab.patch
+    0059-QMutex-order-reads-from-QMutexPrivate-waiters-and-QB.patch
+    0060-Android-Add-the-QtAndroidBearer.jar-to-the-jar-depen.patch
+    0061-Android-Add-the-required-linker-flags-for-unwinding-.patch
+    0062-Android-recommend-against-using-ANDROID_ABIS-inside-.patch
+    0063-Android-fix-android-java-and-templates-targets-with-.patch
+    0064-QCharRef-properly-disable-assignment-from-char.patch
+    0065-Android-Treat-ACTION_CANCEL-as-TouchPointReleased.patch
+    0066-Fix-misidentification-of-some-shearing-QTransforms-a.patch
+    0067-Fix-QGraphicsItem-crash-if-click-right-button-of-mou.patch
+    0068-Bump-version.patch
+    0069-Android-Ensure-windows-always-have-a-geometry-on-cre.patch
+    0070-macOS-Account-for-Big-Sur-always-enabling-layer-back.patch
+    0071-Fix-shaping-problems-on-iOS-14-macOS-11.patch
+    0072-Link-to-qAlpha-in-qRgb-and-qRgba-docs.patch
+    0073-HTTP2-fix-crash-from-assertion.patch
+    0074-Fuzzing-Add-a-test-for-QDateTime-fromString.patch
+    0075-QSocks5SocketEngine-Fix-out-of-bounds-access-of-QBA.patch
+    0076-Use-QTRY_COMPARE-in-an-attempt-to-make-the-test-less.patch
+    0077-Doc-Document-QGradient-Preset-enum-values.patch
+    0078-Doc-Fix-documentation-warnings-for-Qt-XML.patch
+    0079-Doc-Fix-documentation-warnings-in-Qt-Network.patch
+    0080-Ensure-that-QMenu-is-polished-before-setting-the-scr.patch
+    0081-widgets-Don-t-report-new-focus-object-during-clearFo.patch
+    0082-QDtls-remove-redundant-RAII-struct.patch
+    0083-macOS-Propagate-device-pixel-ratio-of-system-tray-ic.patch
+    0084-tst_qocsp-improve-code-coverage.patch
+    0085-Doc-explain-how-to-create-a-test-touch-device-for-us.patch
+    0086-macOS-Upgrade-supported-SDK-to-11.0.patch
+    0087-Fix-logic-error-in-QString-replace-ch-after-cs.patch
+    0088-Be-more-consistent-when-converting-JSON-values-from-.patch
+    0089-QCoreApplication-add-more-information-to-processEven.patch
+    0090-Fix-QSFPM-not-emitting-dataChanged-when-source-model.patch
+    0091-Android-Fix-android-accessibility-not-being-set-acti.patch
+    0092-Fix-x-height-name-in-stylesheet-docs.patch
+    0093-QMutex-Work-around-ICC-bug-in-dealing-with-constexpr.patch
+    0094-wasm-fix-resizing-of-qwidget-windows.patch
+    0095-Avoid-integer-overflow-and-division-by-zero.patch
+    0096-QPasswordDigestor-improve-code-coverage.patch
+    0097-QStackedLayout-fix-a-memory-leak.patch
+    0098-Limit-value-in-setFontWeightFromValue.patch
+    0099-Doc-Fix-documentation-of-qmake-s-exists-function.patch
+    0100-QVLA-do-not-include-QtTest.patch
+    0101-Clean-up-docs-of-QCalendar-related-QLocale-toString-.patch
+    0102-Change-android-target-SDK-version-to-29.patch
+    0103-QVLA-always-use-new-to-create-new-objects.patch
+    0104-QPushButton-fix-support-of-style-sheet-rule-for-text.patch
+    0105-Limit-pen-width-to-maximal-32767.patch
+    0106-Doc-Consistently-use-book-style-capitalization-for-Q.patch
+    0107-qstring.h-fix-warnings-about-shortening-qsizetype-to.patch
+    0108-Fix-invalid-QSortFilterProxyModel-dataChanged-parame.patch
+    0109-Minor-refactor-of-installMetaFile.patch
+    0110-Return-a-more-useful-date-time-on-parser-failure-in-.patch
+    0111-QCalendar-increase-coverage-by-tests.patch
+    0112-Bounds-check-time-zone-offsets-when-parsing.patch
+    0113-Network-self-test-make-it-work-with-docker-container.patch
+    0114-QSslConfiguration-improve-code-coverage.patch
+    0115-Add-new-way-to-mess-up-projects-with-QMAKE_INSTALL_R.patch
+    0116-Install-3rd-party-headers-and-meta-for-static-builds.patch
+    0117-Create-qtlibjpeg-for-jpeg-image-plugin.patch
+    0118-QStandardPaths-Don-t-change-permissions-of-XDG_RUNTI.patch
+    0119-tst_QSslCertificate-improve-code-coverage.patch
+    0120-Let-QXcbConnection-getTimestamp-properly-exit-when-X.patch
+    0121-QDtls-cookie-verifier-make-sure-a-server-can-re-use-.patch
+    0122-QMacStyle-remove-vertical-adjustment-for-inactive-ta.patch
+    0123-Revert-xcb-add-xcb-util-dependency-for-xcb-image.patch
+    0124-Containers-call-constructors-even-for-primitive-type.patch
+    0125-Android-print-tailored-warning-if-qml-dependency-pat.patch
+    0126-Cosmetic-stroker-avoid-overflows-for-non-finite-coor.patch
+    0127-QSslCipher-improve-its-code-coverage-and-auto-tests.patch
+    0128-tst_qsslkey-handle-QT_NO_SSL-properly.patch
+    0129-Add-the-Qt-6.0-deprecation-macros.patch
+    0130-wasm-fix-mouse-double-click.patch
+    0131-Android-avoid-reflection-with-ClipData-addItem.patch
+    0132-QHeaderView-fix-spurious-sorting.patch
+    0133-Fix-exception-with-Android-5.x.patch
+    0134-Http2-Remove-errored-out-requests-from-queue.patch
+    0135-Http2-don-t-call-ensureConnection-when-there-s-no-re.patch
+    0136-Fix-QTranslator-load-search-order-not-following-uiLa.patch
+    0137-Avoid-signed-overflow-in-moc.patch
+    0138-Android-Kill-calls-to-deprecated-func-in-API-29.patch
+    0139-Doc-Improve-_CAST_FROM_ASCII-documentation.patch
+    0140-Improve-documented-function-argument-names.patch
+    0141-Fix-QImage-setPixelColor-on-RGBA64_Premultiplied.patch
+    0142-macOS-Make-sure-that-the-reserved-characters-are-not.patch
+    0143-Enable-testing-for-whether-a-calendar-registered-its.patch
+    0144-tests-add-a-shortcut-to-quit-app-in-allcursors.patch
+    0145-QSslSocket-Don-t-call-transmit-in-unencrypted-mode.patch
+    0146-QStringView-operator-operator-operator-currently-Qt6.patch
+    0147-QCborStreamReader-move-the-readStringChunk-code-to-t.patch
+    0148-Improve-the-documentation-for-QElapsedTimer-restart-.patch
+    0149-QSslSocket-verify-do-not-alter-the-default-configura.patch
+    0150-Fix-tst_QFontDatabase-aliases-failure-with-ambiguous.patch
+    0151-QStyleAnimation-make-sure-the-last-frame-of-animatio.patch
+    0152-PCRE-update-to-10.36.patch
+    0153-tst_QCborValue-adjust-the-size-of-the-minimum-string.patch
+    0154-macOS-Always-allow-interacting-with-popup-windows-du.patch
+    0155-macOS-Add-missing-QT_MANGLE_NAMESPACE.patch
+    0156-QSplashScreen-draw-pixmap-with-SmoothTransfrom.patch
+    0157-Android-Qml-accessibility-fixes.patch
+    0158-Http2-set-the-reply-s-error-code-and-string-on-error.patch
+    0159-Try-again-to-fix-Clang-s-Wconstant-logical-operand-w.patch
+    0160-Revert-Android-print-tailored-warning-if-qml-depende.patch
+    0161-QUrl-fix-parsing-of-empty-IPv6-addresses.patch
+    0162-tst_QSslError-improve-the-code-coverage-as-pointed-a.patch
+    0163-macOS-Disable-WA_QuitOnClose-on-menu-item-widget-con.patch
+    0164-QString-fix-count-QRegularExpression.patch
+    0165-QString-lastIndexOf-fix-off-by-one-for-zero-length-m.patch
+    0166-secureudpclient-a-speculative-fix-for-non-reproducib.patch
+    0167-Android-don-t-use-avx-and-avx2-when-building-for-And.patch
+    0168-Fuzzing-Provide-link-to-oss-fuzz.patch
+    0169-Blacklist-tst_QMdiArea-updateScrollBars-on-macos.patch
+    0170-Fix-build-with-GCC-11-include-limits.patch
+    0171-Build-fixes-for-GCC-11.patch
+    0172-Partially-revert-813a928c7c3cf98670b6043149880ed5c95.patch
+    0173-Fix-removing-columns-when-QSortFilterProxyModel-has-.patch
+    0174-Fix-get-out-of-bounds-index-in-QSortFilterProxyModel.patch
+    0175-Fix-handling-of-surrogates-in-QBidiAlgorithm.patch
+    0176-Avoid-undefined-color-values-in-corrupt-xpm-image.patch
+    0177-Gracefully-reject-requests-for-absurd-font-sizes.patch
+    0178-Don-t-own-unique-name-for-QDBusTrayIcon.patch
+    0179-QAbstractItemModelTester-fix-false-positive-when-mod.patch
+    0180-Fix-QAbstractItemModelTester-false-positive.patch
+    0181-Deprecate-QMutex-in-recursive-mode.patch
+    0182-Fix-QAbstractItemModelTester-false-positive.patch
+    0183-Fix-crash-on-serializing-default-constructed-QTimeZo.patch
+    0184-Fix-QTreeModel-calling-beginRemoveRows-twice.patch
+    0185-QConcatenateTablesProxyModel-skip-dataChanged-in-hid.patch
+    0186-QComboBox-fix-select-all-columns-in-the-view.patch
+    0187-QTableView-honor-spans-when-calculating-height-width.patch
+    0188-TableView-Trigger-the-resizing-of-editors-resizing-a.patch
+    0189-Fix-no-mapping-for-SysReq-key.patch
+    0190-qdbus-add-support-for-aay-QByteArrayList.patch
+    0191-QRandom-drop-a-usage-of-std-is_literal_type.patch
+    0192-fix-Optimize-the-performance-of-the-inotify-file-sys.patch
+    0193-Remove-the-unnecessary-template-parameter-from-the-c.patch
+    0194-Fix-memory-leak-when-using-small-caps-font.patch
+    0195-Make-sure-_q_printerChanged-is-called-even-if-only-p.patch
+    0196-fix-Alt-shortcut-on-non-US-layouts.patch
diff --git a/tests/test_install.py b/tests/test_install.py
index b9837b9..a237457 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -16,6 +16,7 @@ from pytest_socket import disable_socket
 
 from aqt.archives import QtPackage
 from aqt.exceptions import ArchiveDownloadError, ArchiveExtractionError
+from aqt.helper import Settings
 from aqt.installer import Cli, installer
 
 
@@ -536,9 +537,44 @@ def test_install_nonexistent_archives(monkeypatch, capsys, cmd, xml_file: Option
     assert actual == expected, "{0} != {1}".format(actual, expected)
 
 
-def test_install_keyboard_interrupt(monkeypatch, capsys):
-    def mock_keyboard_interrupt(*args):
-        raise KeyboardInterrupt()
+@pytest.mark.parametrize(
+    "make_exception, expect_end_msg, settings_file",
+    (
+        (
+            "RuntimeError()",
+            "===========================PLEASE FILE A BUG REPORT===========================\n"
+            "You have discovered a bug in aqt.\n"
+            "Please file a bug report at https://github.com/miurahr/aqtinstall/issues.\n"
+            "Please remember to include a copy of this program's output in your report.",
+            "../aqt/settings.ini",
+        ),
+        (
+            "KeyboardInterrupt()",
+            "Caught KeyboardInterrupt, terminating installer workers\n" "Installer halted by keyboard interrupt.",
+            "../aqt/settings.ini",
+        ),
+        (
+            "MemoryError()",
+            "Caught MemoryError, terminating installer workers\n"
+            "Out of memory when downloading and extracting archives in parallel.\n"
+            "==============================Suggested follow-up:==============================\n"
+            "* Please reduce your 'concurrency' setting (see "
+            "https://aqtinstall.readthedocs.io/en/stable/configuration.html#configuration)",
+            "../aqt/settings.ini",
+        ),
+        (
+            "MemoryError()",
+            "Caught MemoryError, terminating installer workers\n"
+            "Out of memory when downloading and extracting archives.\n"
+            "==============================Suggested follow-up:==============================\n"
+            "* Please free up more memory.",
+            "data/settings_no_concurrency.ini",
+        ),
+    ),
+)
+def test_install_pool_exception(monkeypatch, capsys, make_exception, expect_end_msg, settings_file):
+    def mock_installer_func(*args):
+        raise eval(make_exception)
 
     host, target, ver, arch = "windows", "desktop", "6.1.0", "win64_mingw81"
     updates_url = "windows_x86/desktop/qt6_610/Updates.xml"
@@ -548,15 +584,13 @@ def test_install_keyboard_interrupt(monkeypatch, capsys):
     mock_get_url, mock_download_archive = make_mock_geturl_download_archive(archives, arch, host, updates_url)
     monkeypatch.setattr("aqt.archives.getUrl", mock_get_url)
     monkeypatch.setattr("aqt.installer.getUrl", mock_get_url)
-    monkeypatch.setattr("aqt.installer.installer", mock_keyboard_interrupt)
+    monkeypatch.setattr("aqt.installer.installer", mock_installer_func)
 
+    Settings.load_settings(str(Path(__file__).parent / settings_file))
     cli = Cli()
-    cli._setup_settings()
     assert cli.run(cmd) == 1
     out, err = capsys.readouterr()
-    assert err.rstrip().endswith(
-        "Caught KeyboardInterrupt, terminating installer workers\nInstaller halted by keyboard interrupt."
-    )
+    assert err.rstrip().endswith(expect_end_msg)
 
 
 def test_install_installer_archive_extraction_err(monkeypatch):