├── apport ├── py.typed ├── crashdb_impl │ ├── __init__.py │ └── debian.py ├── packaging.py ├── user_group.py ├── packaging_impl │ └── __init__.py ├── com.ubuntu.apport.policy.in ├── logging.py ├── __init__.py ├── REThread.py └── hook_ui.py ├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── test_apport.py │ ├── test_crashdb_debian.py │ ├── test_user_group.py │ ├── test_deprecation.py │ ├── test_packaging_impl.py │ ├── test_hooks_wayland_session.py │ ├── test_apport_retrace.py │ ├── test_rethread.py │ ├── test_hooks_image.py │ ├── test_ui.py │ ├── test_sandboxutils.py │ ├── test_crashdb_launchpad.py │ └── test_helper.py ├── system │ ├── __init__.py │ ├── test_github_query.py │ └── test_apport_valgrind.py ├── integration │ ├── __init__.py │ ├── test_helper.py │ ├── test_packaging.py │ ├── test_ui_kde.py │ ├── test_xml.py │ ├── test_ui_gtk.py │ ├── test_packaging_rpm.py │ ├── test_whoopsie_upload_all.py │ ├── test_apport_checkreports.py │ ├── test_ui_cli.py │ ├── test_unkillable_shutdown.py │ ├── test_java_crashes.py │ └── test_recoverable_problem.py ├── data │ ├── testclick │ │ ├── bin │ │ │ └── pycrash │ │ ├── click │ │ │ ├── CMakeLists.txt │ │ │ └── manifest.json.in │ │ └── CMakeLists.txt │ ├── testclick_0.1_all.click │ ├── LP-PPA-daisy-pluckers-daisy-seeds.asc │ └── LP-PPA-apport-hackers-apport-autopkgtests.asc ├── README.md ├── paths.py └── run-linters ├── problem_report └── py.typed ├── bin ├── apport-collect ├── apport-bug ├── dupdb-admin └── apport-unpack ├── setuptools_apport ├── __init__.py └── java.py ├── data ├── bash-completion │ ├── apport-cli │ ├── apport-unpack │ └── apport-collect ├── icons │ ├── 32x32 │ │ ├── mimetypes │ │ │ └── text-x-apport.png │ │ └── apps │ │ │ └── apport.png │ ├── 48x48 │ │ ├── mimetypes │ │ │ └── text-x-apport.png │ │ └── apps │ │ │ └── apport.png │ ├── 64x64 │ │ ├── mimetypes │ │ │ └── text-x-apport.png │ │ └── apps │ │ │ └── apport.png │ └── scalable │ │ └── mimetypes │ │ └── text-x-apport.svg ├── spinner.gif ├── systemd │ ├── apport.conf │ ├── systemd-coredump@.service.d │ │ └── apport-coredump-hook.conf │ ├── apport-forward@.service │ ├── apport-forward.socket │ ├── apport.service │ └── apport-coredump-hook@.service ├── root_info_wrapper ├── general-hooks │ ├── wayland_session.py │ └── image.py ├── package-hooks │ └── source_apport.py ├── is-enabled ├── gcc_ice_hook ├── kernel_oops ├── apport-checkreports ├── dump_acpi_tables.py ├── iwlwifi_error_dump ├── recoverable_problem ├── java_uncaught_exception ├── package_hook ├── kernel_crashdump ├── apportcheckresume └── unkillable_shutdown ├── setup.cfg ├── etc ├── default │ └── apport ├── apport │ ├── report-ignore │ │ ├── ignore-teamviewer │ │ └── README.denylist │ └── crashdb.conf ├── cron.daily │ └── apport └── init.d │ └── apport ├── .gitignore ├── udev └── 50-apport.rules ├── debhelper ├── apport.pm └── dh_apport ├── java ├── testsuite │ └── crash.java ├── README └── com │ └── ubuntu │ └── apport │ └── ApportUncaughtExceptionHandler.java ├── .github └── workflows │ └── cla.yaml ├── kde ├── apport-kde-mimelnk.desktop.in ├── apport-kde.desktop.in ├── apport-kde-mime.desktop.in ├── choices.ui ├── progress.ui ├── userpass.ui └── error.ui ├── gtk └── apport-gtk.desktop.in ├── use-local ├── pm-utils └── sleep.d │ └── 000record-status ├── TODO ├── do-release ├── man ├── apport-unpack.1 ├── dupdb-admin.1 ├── apport-bug.1 └── apport-valgrind.1 ├── AUTHORS ├── doc └── symptoms.md ├── pyproject.toml └── setup.py /apport/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problem_report/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/apport-collect: -------------------------------------------------------------------------------- 1 | apport-bug -------------------------------------------------------------------------------- /tests/system/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apport/crashdb_impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setuptools_apport/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/bash-completion/apport-cli: -------------------------------------------------------------------------------- 1 | apport-bug -------------------------------------------------------------------------------- /data/icons/32x32/mimetypes/text-x-apport.png: -------------------------------------------------------------------------------- 1 | ../apps/apport.png -------------------------------------------------------------------------------- /data/icons/48x48/mimetypes/text-x-apport.png: -------------------------------------------------------------------------------- 1 | ../apps/apport.png -------------------------------------------------------------------------------- /data/icons/64x64/mimetypes/text-x-apport.png: -------------------------------------------------------------------------------- 1 | ../apps/apport.png -------------------------------------------------------------------------------- /data/icons/scalable/mimetypes/text-x-apport.svg: -------------------------------------------------------------------------------- 1 | ../apps/apport.svg -------------------------------------------------------------------------------- /data/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/apport/main/data/spinner.gif -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_i18n] 2 | domain=apport 3 | 4 | [build_java_subdir] 5 | minimum_java_release=8 6 | -------------------------------------------------------------------------------- /data/systemd/apport.conf: -------------------------------------------------------------------------------- 1 | d /var/lib/apport 0755 root root - 2 | d /var/lib/apport/coredump 0755 root root 3d 3 | -------------------------------------------------------------------------------- /tests/data/testclick/bin/pycrash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | def do_stuff(): 4 | 1/0 5 | 6 | do_stuff() 7 | -------------------------------------------------------------------------------- /data/icons/32x32/apps/apport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/apport/main/data/icons/32x32/apps/apport.png -------------------------------------------------------------------------------- /data/icons/48x48/apps/apport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/apport/main/data/icons/48x48/apps/apport.png -------------------------------------------------------------------------------- /data/icons/64x64/apps/apport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/apport/main/data/icons/64x64/apps/apport.png -------------------------------------------------------------------------------- /data/systemd/systemd-coredump@.service.d/apport-coredump-hook.conf: -------------------------------------------------------------------------------- 1 | [Unit] 2 | OnSuccess=apport-coredump-hook@%i.service 3 | -------------------------------------------------------------------------------- /tests/data/testclick_0.1_all.click: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canonical/apport/main/tests/data/testclick_0.1_all.click -------------------------------------------------------------------------------- /data/root_info_wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # this wrapper just exists so that we can put a polkit .policy around it 3 | exec sh "$@" 4 | -------------------------------------------------------------------------------- /etc/default/apport: -------------------------------------------------------------------------------- 1 | # set this to 0 to disable apport, or to 1 to enable it 2 | # you can temporarily override this with 3 | # sudo service apport start force_start=1 4 | enabled=1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | /.coverage 4 | MANIFEST 5 | __pycache__ 6 | apport/packaging_impl.py 7 | build 8 | dist 9 | doc/*.aux 10 | doc/*.log 11 | doc/*.pdf 12 | doc/*.toc 13 | -------------------------------------------------------------------------------- /udev/50-apport.rules: -------------------------------------------------------------------------------- 1 | # firmware dumps from iwlmvm iwlwifi driver 2 | DRIVER=="iwlwifi", ACTION=="change", ENV{EVENT}=="error_dump", RUN+="/usr/share/apport/iwlwifi_error_dump $env{DEVPATH}" 3 | -------------------------------------------------------------------------------- /data/systemd/apport-forward@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apport crash forwarding receiver 3 | Requires=apport-forward.socket 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/share/apport/apport 8 | -------------------------------------------------------------------------------- /debhelper/apport.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # debhelper sequence file for apport 3 | 4 | use warnings; 5 | use strict; 6 | use Debian::Debhelper::Dh_Lib; 7 | 8 | insert_after("dh_bugfiles", "dh_apport"); 9 | 10 | 1; 11 | -------------------------------------------------------------------------------- /tests/data/testclick/click/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | configure_file(manifest.json.in ${CMAKE_CURRENT_BINARY_DIR}/manifest.json) 2 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/manifest.json 3 | DESTINATION ${CMAKE_INSTALL_PREFIX}) 4 | -------------------------------------------------------------------------------- /etc/apport/report-ignore/ignore-teamviewer: -------------------------------------------------------------------------------- 1 | # Ignore TeamViewer crashes as it causes a crash in GDB 2 | # due to a custom shared library they ship which misses 3 | # the .text section (LP: #2041830) 4 | /opt/teamviewer/tv_bin/TeamViewer 5 | -------------------------------------------------------------------------------- /tests/data/testclick/click/manifest.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testclick", 3 | "title": "apport test click app", 4 | "framework": "ubuntu-sdk-14.04-qml", 5 | "maintainer": "Martin Pitt ", 6 | "version": "0.1" 7 | } 8 | -------------------------------------------------------------------------------- /etc/apport/report-ignore/README.denylist: -------------------------------------------------------------------------------- 1 | # Denylist for apport 2 | # If an executable path appears on any line in any file in 3 | # /etc/apport/report-ignore/, apport will not generate a crash report 4 | # for it. Matches are exact only at the moment (no globbing etc.). 5 | -------------------------------------------------------------------------------- /java/testsuite/crash.java: -------------------------------------------------------------------------------- 1 | import com.ubuntu.apport.*; 2 | 3 | class crash { 4 | public static void main(String[] args) { 5 | com.ubuntu.apport.ApportUncaughtExceptionHandler.install(); 6 | throw new RuntimeException("Can't catch this"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/cla.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CLA 3 | 4 | on: # yamllint disable-line rule:truthy 5 | - pull_request_target 6 | 7 | jobs: 8 | cla-check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check if CLA signed 12 | uses: canonical/has-signed-canonical-cla@v1 13 | -------------------------------------------------------------------------------- /kde/apport-kde-mimelnk.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | # This is a deprecated method for KDE3 to make it recognize this MimeType 3 | Type=MimeType 4 | _Comment=Apport crash file 5 | Hidden=false 6 | Icon=apport 7 | # This must not have a trailing ";" for KDE3 8 | MimeType=text/x-apport 9 | Patterns=*.crash 10 | -------------------------------------------------------------------------------- /data/general-hooks/wayland_session.py: -------------------------------------------------------------------------------- 1 | """Detect if the current session is running under Wayland.""" 2 | 3 | import os 4 | 5 | 6 | def add_info(report, unused_ui): 7 | """Add a tag if current session is running under Wayland.""" 8 | if os.environ.get("WAYLAND_DISPLAY"): 9 | report.add_tags(["wayland-session"]) 10 | -------------------------------------------------------------------------------- /kde/apport-kde.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | _Name=Report a problem... 3 | _Comment=Report a malfunction to the developers 4 | Exec=/usr/share/apport/apport-kde -f 5 | Icon=apport 6 | Terminal=false 7 | Type=Application 8 | Categories=KDE;System; 9 | OnlyShowIn=KDE; 10 | StartupNotify=true 11 | X-Ubuntu-Gettext-Domain=apport 12 | -------------------------------------------------------------------------------- /data/systemd/apport-forward.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Unix socket for apport crash forwarding 3 | ConditionVirtualization=container 4 | 5 | [Socket] 6 | ListenStream=/run/apport.socket 7 | SocketMode=0600 8 | Accept=yes 9 | MaxConnections=10 10 | Backlog=5 11 | PassCredentials=true 12 | 13 | [Install] 14 | WantedBy=sockets.target 15 | -------------------------------------------------------------------------------- /data/systemd/apport.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=automatic crash report generation 3 | After=remote-fs.target 4 | ConditionVirtualization=!container 5 | 6 | [Service] 7 | Type=oneshot 8 | RemainAfterExit=yes 9 | ExecStart=/usr/share/apport/apport --start 10 | ExecStop=/usr/share/apport/apport --stop 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /kde/apport-kde-mime.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | _Name=Report a problem... 3 | _Comment=Report a malfunction to the developers 4 | Exec=/usr/share/apport/apport-kde -c %f 5 | Icon=apport 6 | Terminal=false 7 | Type=Application 8 | MimeType=text/x-apport; 9 | Categories=KDE;System; 10 | NoDisplay=true 11 | StartupNotify=true 12 | X-Ubuntu-Gettext-Domain=apport 13 | -------------------------------------------------------------------------------- /gtk/apport-gtk.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | _Name=Report a problem... 3 | _Comment=Report a malfunction to the developers 4 | Exec=/usr/share/apport/apport-gtk -c %f 5 | Icon=apport 6 | Terminal=false 7 | Type=Application 8 | MimeType=text/x-apport; 9 | Categories=GNOME;GTK;Utility; 10 | NoDisplay=true 11 | StartupNotify=true 12 | X-Ubuntu-Gettext-Domain=apport 13 | -------------------------------------------------------------------------------- /etc/cron.daily/apport: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # clean all crash reports which are older than a week. 3 | [ -d /var/crash ] || exit 0 4 | find /var/crash/. ! -name . -prune -type f \( \( -size 0 -a \! -name '*.upload*' -a \! -name '*.drkonqi*' \) -o -mtime +7 \) -exec rm -f -- '{}' \; 5 | find /var/crash/. ! -name . -prune -type d -regextype posix-extended -regex '.*/[0-9]{12}$' \( -mtime +7 \) -exec rm -Rf -- '{}' \; 6 | -------------------------------------------------------------------------------- /tests/data/testclick/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(com.example.testclick) 2 | cmake_minimum_required(VERSION 2.8.9) 3 | 4 | # Standard install paths 5 | include(GNUInstallDirs) 6 | 7 | set(CMAKE_INSTALL_PREFIX /) 8 | set(CMAKE_INSTALL_BINDIR /) 9 | set(DATA_DIR /) 10 | 11 | install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/bin/pycrash 12 | DESTINATION ${CMAKE_INSTALL_BINDIR}) 13 | 14 | add_subdirectory(click) 15 | -------------------------------------------------------------------------------- /use-local: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # This changes the kernel's core_pattern pipeline to use the local tree's 3 | # apport program instead of the system version. This is useful if you are 4 | # making changes to bin/apport and want to test them without copying them to 5 | # /usr/share/apport/ every time. 6 | 7 | echo "|$(readlink -f $(dirname $0)/data/apport) -p%p -s%s -c%c -d%d -P%P" | tee /proc/sys/kernel/core_pattern 8 | -------------------------------------------------------------------------------- /pm-utils/sleep.d/000record-status: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Record the current operation to allow failure detection. 4 | 5 | STAMPFILE="/var/lib/pm-utils/status" 6 | 7 | case "$1" in 8 | hibernate|suspend) 9 | mkdir -p `dirname $STAMPFILE` 10 | echo "$1" >"$STAMPFILE" 11 | ;; 12 | thaw|resume) 13 | rm -f "$STAMPFILE" 14 | ;; 15 | esac 16 | 17 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | apport: 2 | - check crashes of root processes with dropped privs in test suite 3 | 4 | dup detection: 5 | - add merging of two databases -> needs time stamp of last change 6 | 7 | GUI: 8 | - point out bug privacy and to leave it private by default 9 | 10 | hooks: 11 | - add hooks which run during program crash, to collect more runtime data 12 | 13 | hookutils: 14 | - run hooks for related packages in attach_related_packages 15 | 16 | apt-dpkg backend: 17 | - use python-apt's Version.get_source() instead of apt-get source 18 | -------------------------------------------------------------------------------- /apport/packaging.py: -------------------------------------------------------------------------------- 1 | # You do not want to know why this empty file exist! You have been warned! 2 | # 3 | # This empty module exist to be imported by `apport/__init__.py` before 4 | # `apport/__init__.py` defines the `packaging` object instance. This makes 5 | # following code work: 6 | # 7 | # >>> import apport.packaging 8 | # >>> type(apport.packaging) 9 | # apport.packaging_impl.apt_dpkg._AptDpkgPackageInfo 10 | # 11 | # Importing `apport.packaging` will import `apport` first. This will import 12 | # `apport.packaging` and shadow it. 13 | -------------------------------------------------------------------------------- /data/bash-completion/apport-unpack: -------------------------------------------------------------------------------- 1 | # Apport bash-completion for apport-unpack 2 | 3 | _apport-unpack () 4 | { 5 | local cur prev 6 | 7 | COMPREPLY=() 8 | cur=`_get_cword` 9 | prev=${COMP_WORDS[COMP_CWORD-1]} 10 | 11 | case "$prev" in 12 | apport-unpack) 13 | # only show *.apport *.crash files 14 | COMPREPLY=( $( compgen -G "${cur}*.apport" 15 | compgen -G "${cur}*.crash" ) ) 16 | 17 | ;; 18 | esac 19 | } 20 | 21 | complete -F _apport-unpack -o filenames -o dirnames apport-unpack 22 | -------------------------------------------------------------------------------- /java/README: -------------------------------------------------------------------------------- 1 | apport.jar contains the necessary class(es) for trapping uncaught Java 2 | exceptions. crash.class and crash.jar are used only by the test suite. 3 | 4 | The crash handler, when invoked, opens a pipe to the java_uncaught_exception 5 | script, and feeds it key/value pairs containing the relevant JVM state. 6 | 7 | There is currently no automatic integration of this handler. You have to do 8 | 9 | import com.ubuntu.apport.*; 10 | 11 | and in your main method install the handler with 12 | 13 | com.ubuntu.apport.ApportUncaughtExceptionHandler.install(); 14 | -------------------------------------------------------------------------------- /data/package-hooks/source_apport.py: -------------------------------------------------------------------------------- 1 | """Apport package hook for apport itself. 2 | 3 | This adds /var/log/apport.log and the file listing in /var/crash to the report. 4 | """ 5 | 6 | # Copyright 2007 Canonical Ltd. 7 | # Author: Martin Pitt 8 | 9 | import glob 10 | 11 | import apport.hookutils 12 | 13 | APPORT_LOG = "/var/log/apport.log" 14 | 15 | 16 | def add_info(report): 17 | """Add Apport logs to the problem report.""" 18 | apport.hookutils.attach_file_if_exists(report, APPORT_LOG, "ApportLog") 19 | reports = glob.glob("/var/crash/*") 20 | if reports: 21 | report["CrashReports"] = apport.hookutils.command_output( 22 | ["stat", "-c", "%a:%u:%g:%s:%y:%x:%n"] + reports 23 | ) 24 | -------------------------------------------------------------------------------- /data/is-enabled: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Check if apport reports are enabled. Exit with 0 if so, otherwise with 1. 3 | # 4 | # Copyright (c) 2011 Canonical Ltd. 5 | # Author: Martin Pitt 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU General Public License as published by the 9 | # Free Software Foundation; either version 2 of the License, or (at your 10 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 11 | # the full text of the license. 12 | 13 | set -e 14 | 15 | CONF=/etc/default/apport 16 | 17 | # defaults to enabled if not present 18 | [ -f $CONF ] || exit 0 19 | ! grep -q '^[[:space:]]*enabled[[:space:]]*=[[:space:]]*0[[:space:]]*$' $CONF 20 | -------------------------------------------------------------------------------- /tests/integration/test_helper.py: -------------------------------------------------------------------------------- 1 | """Test test helper functions. Test inception for the win!""" 2 | 3 | import os 4 | import sys 5 | import unittest 6 | 7 | from tests.helper import pids_of, read_shebang 8 | 9 | 10 | class TestHelper(unittest.TestCase): 11 | # pylint: disable=missing-class-docstring,missing-function-docstring 12 | 13 | def test_pids_of_non_existing_program(self) -> None: 14 | self.assertEqual(pids_of("non-existing"), set()) 15 | 16 | def test_pids_of_running_python(self) -> None: 17 | pids = pids_of(sys.executable) 18 | self.assertGreater(len(pids), 0) 19 | self.assertIn(os.getpid(), pids) 20 | 21 | def test_read_shebang_binary(self) -> None: 22 | self.assertIsNone(read_shebang(sys.executable)) 23 | 24 | def test_read_shebang_shell_script(self) -> None: 25 | self.assertEqual(read_shebang("/usr/bin/ldd"), "/bin/bash") 26 | -------------------------------------------------------------------------------- /do-release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # This script does all the steps necessary for doing a new upstream release. It 5 | # should solely be used by upstream developers, distributors do not need to 6 | # worry about it. 7 | 8 | [ -z "$(git status --porcelain)" ] || { 9 | echo "Uncommitted changes, aborting" >&2 10 | exit 1 11 | } 12 | 13 | version=$(grep '(UNRELEASED)' NEWS.md | cut -f1 -d' ') 14 | [ -n "$version" ] || { 15 | echo "no UNRELEASED in NEWS.md" >&2 16 | exit 1 17 | } 18 | 19 | sed -i -r "s/__version__ = '[0-9.]*'/__version__ = '${version}'/" apport/ui.py 20 | sed -i "s/(UNRELEASED)/$(date '+(%Y-%m-%d)')/" NEWS.md 21 | git commit -s -m "Release apport $version" apport/ui.py NEWS.md 22 | git tag "$version" 23 | 24 | git archive --format=tar --prefix="apport-${version}/" "$version" | xz -z -9 > "../apport-${version}.tar.xz" 25 | echo "Release tarball exported to ../apport-${version}.tar.xz" 26 | -------------------------------------------------------------------------------- /data/systemd/apport-coredump-hook@.service: -------------------------------------------------------------------------------- 1 | # This service is responsible for reading the coredump data from systemd journal 2 | # after a crash has occurred, and generating a crash file to /var/crash/. 3 | [Service] 4 | Type=oneshot 5 | ExecStart=/usr/share/apport/apport --from-systemd-coredump %i 6 | Nice=9 7 | OOMScoreAdjust=500 8 | IPAddressDeny=any 9 | LockPersonality=yes 10 | MemoryDenyWriteExecute=yes 11 | NoNewPrivileges=yes 12 | PrivateDevices=yes 13 | PrivateNetwork=yes 14 | PrivateTmp=yes 15 | ProtectControlGroups=yes 16 | ProtectHome=read-only 17 | ProtectHostname=yes 18 | ProtectKernelLogs=yes 19 | ProtectKernelModules=yes 20 | ProtectKernelTunables=yes 21 | ProtectSystem=strict 22 | ReadWritePaths=/var/crash /var/log 23 | RestrictAddressFamilies=AF_UNIX 24 | RestrictRealtime=yes 25 | RestrictSUIDSGID=yes 26 | SystemCallArchitectures=native 27 | SystemCallErrorNumber=EPERM 28 | SystemCallFilter=@system-service @file-system @setuid 29 | -------------------------------------------------------------------------------- /man/apport-unpack.1: -------------------------------------------------------------------------------- 1 | .TH apport\-unpack 1 "September 09, 2006" "Martin Pitt" 2 | 3 | .SH NAME 4 | 5 | apport\-unpack \- extract the fields of a problem report to separate files 6 | 7 | .SH SYNOPSIS 8 | 9 | .B apport\-unpack 10 | .I report target\-directory 11 | 12 | .SH DESCRIPTION 13 | 14 | A problem report, as produced by 15 | .B apport 16 | is a single file with a set of key/value pairs in the RFC822 17 | syntax. This tool disassembles a report such that the value of each entry 18 | is written into a separate file, with the key as file name. This is 19 | particularly useful for large binary data like the core dump. 20 | 21 | .I report 22 | is either a path to an existing apport crash report, or '\-' to read 23 | from stdin. 24 | 25 | The 26 | .I target\-directory 27 | must either be nonexisting or empty. 28 | 29 | .SH AUTHOR 30 | .B apport 31 | and the accompanying tools are developed by Martin Pitt 32 | . 33 | -------------------------------------------------------------------------------- /tests/unit/test_apport.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for functions in the base apport module.""" 11 | 12 | from apport import Report, packaging 13 | 14 | 15 | def test_report() -> None: 16 | """Test using Report imported from the base apport module.""" 17 | report = Report() 18 | assert report["ProblemType"] == "Crash" 19 | 20 | 21 | def test_packaging() -> None: 22 | """Test using packaging imported from the base apport module.""" 23 | assert packaging.get_source("gzip") == "gzip" 24 | -------------------------------------------------------------------------------- /tests/unit/test_crashdb_debian.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for apport.crash_impl.debian""" 11 | 12 | from apport.crashdb_impl.debian import CrashDatabase 13 | from apport.report import Report 14 | 15 | 16 | def test_missing_sender() -> None: 17 | """Test missing sender in CrashDB configuration.""" 18 | crashdb = CrashDatabase(None, {}) 19 | report = Report() 20 | assert crashdb.accepts(report) 21 | assert report["UnreportableReason"] == ( 22 | "Please configure sender settings in /etc/apport/crashdb.conf" 23 | ) 24 | -------------------------------------------------------------------------------- /data/bash-completion/apport-collect: -------------------------------------------------------------------------------- 1 | # Apport bash-completion for apport-collect 2 | 3 | _apport-collect () 4 | { 5 | local cur prev 6 | 7 | COMPREPLY=() 8 | cur=`_get_cword` 9 | prev=${COMP_WORDS[COMP_CWORD-1]} 10 | 11 | case "$prev" in 12 | apport-collect) 13 | COMPREPLY=( $( compgen -W "-p --package --tag" -- $cur) ) 14 | 15 | ;; 16 | -p | --package) 17 | # list package names 18 | COMPREPLY=( $( apt-cache pkgnames $cur 2> /dev/null ) ) 19 | 20 | ;; 21 | --tag) 22 | # standalone parameter 23 | return 0 24 | ;; 25 | *) 26 | # only complete -p/--package once 27 | if [[ "${COMP_WORDS[*]}" =~ .*\ -p.* || "${COMP_WORDS[*]}" =~ .*--package.* ]]; then 28 | COMPREPLY=( $( compgen -W "--tag" -- $cur) ) 29 | else 30 | COMPREPLY=( $( compgen -W "-p --package --tag" -- $cur) ) 31 | fi 32 | 33 | ;; 34 | esac 35 | } 36 | 37 | complete -F _apport-collect apport-collect 38 | -------------------------------------------------------------------------------- /tests/unit/test_user_group.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | """Unit tests for apport.user_group.""" 6 | 7 | import unittest 8 | from unittest.mock import MagicMock, patch 9 | 10 | from apport.user_group import get_process_user_and_group 11 | 12 | 13 | class TestUserGroup(unittest.TestCase): 14 | # pylint: disable=missing-function-docstring 15 | """Unit tests for apport.user_group.""" 16 | 17 | @patch("os.getgid", MagicMock(return_value=0)) 18 | @patch("os.getuid", MagicMock(return_value=0)) 19 | def test_get_process_user_and_group_is_root(self) -> None: 20 | self.assertTrue(get_process_user_and_group().is_root()) 21 | 22 | @patch("os.getgid", MagicMock(return_value=2000)) 23 | @patch("os.getuid", MagicMock(return_value=3000)) 24 | def test_get_process_user_and_group_is_not_root(self) -> None: 25 | self.assertFalse(get_process_user_and_group().is_root()) 26 | -------------------------------------------------------------------------------- /apport/user_group.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # SPDX-License-Identifier: GPL-2.0-or-later 4 | 5 | """Functions around users and groups.""" 6 | 7 | import dataclasses 8 | import os 9 | 10 | 11 | @dataclasses.dataclass() 12 | class UserGroupID: 13 | """Pair of user and group ID.""" 14 | 15 | uid: int 16 | gid: int 17 | 18 | def is_root(self) -> bool: 19 | """Check if the user or group ID is root.""" 20 | return self.uid == 0 or self.gid == 0 21 | 22 | @classmethod 23 | def from_systemd_coredump(cls, coredump): 24 | """Extract user and group from systemd-coredump dictionary.""" 25 | uid = coredump.get("COREDUMP_UID") 26 | assert isinstance(uid, int) 27 | gid = coredump.get("COREDUMP_GID") 28 | assert isinstance(gid, int) 29 | return cls(uid, gid) 30 | 31 | 32 | def get_process_user_and_group() -> UserGroupID: 33 | """Return the current process’s real user and group.""" 34 | return UserGroupID(os.getuid(), os.getgid()) 35 | -------------------------------------------------------------------------------- /apport/packaging_impl/__init__.py: -------------------------------------------------------------------------------- 1 | """Platform-specific apport.packaging implementation.""" 2 | 3 | import importlib 4 | import os 5 | import platform 6 | 7 | from apport.package_info import PackageInfo 8 | 9 | 10 | def determine_packaging_implementation() -> str: 11 | """Determine the packaging implementation for the host.""" 12 | info = platform.freedesktop_os_release() 13 | assert info is not None 14 | ids = set([info["ID"]]) | set(info.get("ID_LIKE", "").split(" ")) 15 | if "debian" in ids: 16 | return "apt_dpkg" 17 | if os.path.exists("/usr/bin/rpm"): 18 | return "rpm" 19 | raise RuntimeError( 20 | "Could not determine system package manager." 21 | " Please file a bug and provide /etc/os-release!" 22 | ) 23 | 24 | 25 | def load_packaging_implementation() -> PackageInfo: 26 | """Return the packaging implementation for the host.""" 27 | module = importlib.import_module( 28 | f"apport.packaging_impl.{determine_packaging_implementation()}" 29 | ) 30 | return module.impl 31 | 32 | 33 | impl = load_packaging_implementation() 34 | -------------------------------------------------------------------------------- /tests/integration/test_packaging.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the apport.packaging module.""" 2 | 3 | import unittest 4 | 5 | import apport.packaging 6 | 7 | 8 | class T(unittest.TestCase): 9 | """Integration tests for the apport.packaging module.""" 10 | 11 | def test_get_uninstalled_package(self) -> None: 12 | """get_uninstalled_package()""" 13 | p = apport.packaging.get_uninstalled_package() 14 | self.assertNotEqual(apport.packaging.get_available_version(p), "") 15 | self.assertRaises(ValueError, apport.packaging.get_version, p) 16 | self.assertTrue(apport.packaging.is_distro_package(p)) 17 | 18 | def test_get_os_version(self) -> None: 19 | """get_os_version()""" 20 | (n, v) = apport.packaging.get_os_version() 21 | self.assertEqual(type(n), str) 22 | self.assertEqual(type(v), str) 23 | self.assertGreater(len(n), 1) 24 | self.assertGreater(len(v), 0) 25 | 26 | # second one uses caching, should be identical 27 | (n2, v2) = apport.packaging.get_os_version() 28 | self.assertEqual((n, v), (n2, v2)) 29 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Copyright: 2 | --------- 3 | General: 4 | Copyright (C) 2006 - 2015 Canonical Ltd. 5 | 6 | apport/packaging_impl/rpm.py: 7 | Copyright (C) 2007 Red Hat Inc. 8 | 9 | Authors and Contributors: 10 | ------------------------- 11 | Martin Pitt : 12 | Lead developer, design, backend, GTK frontend development, 13 | maintenance of other frontends 14 | 15 | Michael Hofmann : 16 | Creation of Qt4 and CLI frontends 17 | 18 | Richard A. Johnson : 19 | Changed Qt4 frontend to KDE frontend 20 | 21 | Robert Collins : 22 | Python crash hook 23 | 24 | Will Woods : 25 | RPM packaging backend 26 | 27 | Matt Zimmerman : 28 | Convenience function library for hooks (apport/hookutils.py) 29 | 30 | Troy James Sobotka : 31 | Apport icon (apport/apport.svg) 32 | 33 | Kees Cook : 34 | Various fixes, additional GDB output, SEGV parser. 35 | 36 | Brian Murray : 37 | Various fixes, installation of packages from Launchpad and PPAs, 38 | utilization of a sandbox for gdb. 39 | -------------------------------------------------------------------------------- /tests/unit/test_deprecation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for deprecated functions in apport.""" 11 | 12 | import unittest 13 | 14 | from apport import fatal, unicode_gettext 15 | 16 | 17 | class TestDeprecation(unittest.TestCase): 18 | """Unit tests for deprecated functions in apport.""" 19 | 20 | def test_unicode_gettext(self) -> None: 21 | """unicode_gettext() throws a deprecation warning.""" 22 | with self.assertWarns(PendingDeprecationWarning): 23 | self.assertEqual(unicode_gettext("untranslated"), "untranslated") 24 | 25 | def test_deprecated_logging_function(self) -> None: 26 | """apport.fatal() throws a deprecation warning.""" 27 | with self.assertRaisesRegex(SystemExit, "^1$"): 28 | with self.assertWarns(DeprecationWarning): 29 | fatal("fatal error") 30 | -------------------------------------------------------------------------------- /tests/integration/test_ui_kde.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the Qt Apport user interface.""" 2 | 3 | # Copyright (C) 2025 Canonical Ltd. 4 | # Author: Benjamin Drung 5 | # SPDX-License-Identifier: GPL-2.0-or-later 6 | 7 | import io 8 | from unittest.mock import patch 9 | 10 | import pytest 11 | 12 | from tests.helper import import_module_from_file 13 | from tests.paths import get_data_directory 14 | 15 | 16 | def test_no_environment_variables() -> None: 17 | """Test launching apport-kde without any environment variables set.""" 18 | apport_kde_path = get_data_directory("kde") / "apport-kde" 19 | with patch("sys.stderr", new_callable=io.StringIO) as stderr: 20 | try: 21 | apport_kde = import_module_from_file(apport_kde_path) 22 | except SystemExit: 23 | pytest.skip(stderr.getvalue().strip()) 24 | with ( 25 | patch.dict("os.environ", {}, clear=True), 26 | patch("sys.stderr", new_callable=io.StringIO) as stderr, 27 | pytest.raises(SystemExit) as error, 28 | ): 29 | apport_kde.main([str(apport_kde_path)]) 30 | 31 | assert ( 32 | stderr.getvalue() == "ERROR: This program needs a running display server" 33 | ' session. Please see "man apport-cli" for a command line version of Apport.\n' 34 | ) 35 | assert error.value.code == 1 36 | -------------------------------------------------------------------------------- /tests/integration/test_xml.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Validate XML files.""" 11 | 12 | import subprocess 13 | 14 | import pytest 15 | 16 | from tests.paths import is_local_source_directory 17 | 18 | 19 | def get_policy_xml() -> str: 20 | """Determine path to Apport PolicyKit XML.""" 21 | if not is_local_source_directory(): 22 | return "/usr/share/polkit-1/actions/com.ubuntu.apport.policy" 23 | policy_xml = "build/share/polkit-1/actions/com.ubuntu.apport.policy" 24 | return policy_xml 25 | 26 | 27 | def test_validate_xml() -> None: 28 | """Validate Apport PolicyKit XML.""" 29 | cmd = [ 30 | "xmllint", 31 | "--noout", 32 | "--nonet", 33 | "--dtdvalid", 34 | "/usr/share/polkit-1/policyconfig-1.dtd", 35 | get_policy_xml(), 36 | ] 37 | try: 38 | subprocess.check_call(cmd) 39 | except FileNotFoundError as error: 40 | pytest.skip(f"{error.filename} not available") 41 | -------------------------------------------------------------------------------- /tests/integration/test_ui_gtk.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the GTK Apport user interface.""" 2 | 3 | # Copyright (C) 2025 Canonical Ltd. 4 | # Author: Benjamin Drung 5 | # SPDX-License-Identifier: GPL-2.0-or-later 6 | 7 | import io 8 | import os 9 | import unittest.mock 10 | 11 | import pytest 12 | 13 | from tests.helper import import_module_from_file, restore_os_environ 14 | from tests.paths import get_data_directory 15 | 16 | 17 | @restore_os_environ() 18 | def test_unusable_display() -> None: 19 | """Test apport-gtk to not crash if no usable display is found (LP: #2006981).""" 20 | os.environ |= {"DISPLAY": ":42", "WAYLAND_DISPLAY": "bogus"} 21 | apport_gtk_path = get_data_directory("gtk") / "apport-gtk" 22 | with unittest.mock.patch("sys.stderr", new_callable=io.StringIO) as stderr: 23 | try: 24 | apport_gtk = import_module_from_file(apport_gtk_path) 25 | except SystemExit: 26 | pytest.skip(stderr.getvalue().strip()) 27 | with unittest.mock.patch("sys.stderr", new_callable=io.StringIO) as stderr: 28 | with pytest.raises(SystemExit) as error: 29 | apport_gtk.main([str(apport_gtk_path)]) 30 | 31 | assert ( 32 | stderr.getvalue() == "ERROR: This program needs a running display server" 33 | ' session. Please see "man apport-cli" for a command line version of Apport.\n' 34 | ) 35 | assert error.value.code == 1 36 | -------------------------------------------------------------------------------- /data/gcc_ice_hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2007 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about a gcc internal compiler exception (ICE).""" 13 | 14 | import sys 15 | 16 | import apport.fileutils 17 | import apport.logging 18 | import apport.report 19 | 20 | # parse command line arguments 21 | if len(sys.argv) != 3: 22 | print(f"Usage: {sys.argv[0]} ") 23 | print( 24 | 'If "-" is specified as second argument,' 25 | " the preprocessed source is read from stdin." 26 | ) 27 | sys.exit(1) 28 | 29 | (exename, sourcefile) = sys.argv[1:3] 30 | 31 | # create report 32 | pr = apport.report.Report() 33 | pr["ExecutablePath"] = exename 34 | if sourcefile == "-": 35 | pr["PreprocessedSource"] = (sys.stdin, False) 36 | else: 37 | pr["PreprocessedSource"] = (sourcefile, False) 38 | 39 | # write report 40 | try: 41 | with apport.fileutils.make_report_file(pr) as f: 42 | pr.write(f) 43 | except OSError as error: 44 | apport.logging.fatal("Cannot create report: %s", str(error)) 45 | -------------------------------------------------------------------------------- /data/general-hooks/image.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Add image build information if available.""" 11 | 12 | import os 13 | 14 | _BUILD_INFO_KEY_MAPPING = {"build_name": "CloudBuildName", "serial": "CloudSerial"} 15 | 16 | 17 | def _add_cloud_build_info(report): 18 | """Add cloud build information if available. 19 | 20 | /etc/cloud/build.info is generated by livecd-rootfs. 21 | """ 22 | if not os.path.isfile("/etc/cloud/build.info"): 23 | return 24 | report.add_tags(["cloud-image"]) 25 | with open("/etc/cloud/build.info", encoding="utf-8") as build_info: 26 | for line in build_info: 27 | try: 28 | key, value = line.split(":", 1) 29 | except ValueError: 30 | continue 31 | if key not in _BUILD_INFO_KEY_MAPPING: 32 | continue 33 | report[_BUILD_INFO_KEY_MAPPING[key]] = value.strip() 34 | 35 | 36 | def add_info(report, unused_ui): 37 | """Add image build information if available.""" 38 | _add_cloud_build_info(report) 39 | -------------------------------------------------------------------------------- /data/kernel_oops: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2008 Canonical Ltd. 4 | # Author: Matt Zimmerman 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about a kernel oops.""" 13 | 14 | # TODO: Address following pylint complaints 15 | # pylint: disable=invalid-name 16 | 17 | import os 18 | import sys 19 | from gettext import gettext as _ 20 | 21 | import apport.fileutils 22 | import apport.report 23 | 24 | checksum = None 25 | if len(sys.argv) > 1: 26 | checksum = sys.argv[1] 27 | 28 | oops = sys.stdin.read() 29 | 30 | pr = apport.report.Report("KernelOops") 31 | pr["Failure"] = "oops" 32 | pr["Tags"] = "kernel-oops" 33 | pr["Annotation"] = _( 34 | "Your system might become unstable now and might need to be restarted." 35 | ) 36 | package = apport.packaging.get_kernel_package() 37 | pr.add_package(package) 38 | pr["SourcePackage"] = "linux" 39 | 40 | pr["OopsText"] = oops 41 | u = os.uname() 42 | pr["Uname"] = f"{u[0]} {u[2]} {u[4]}" 43 | 44 | # write report 45 | try: 46 | with apport.fileutils.make_report_file(pr, uid=checksum) as f: 47 | pr.write(f) 48 | except OSError as error: 49 | apport.fatal("Cannot create report: %s", str(error)) 50 | -------------------------------------------------------------------------------- /tests/unit/test_packaging_impl.py: -------------------------------------------------------------------------------- 1 | """Test functions in apport/packaging_impl/__init__.py.""" 2 | 3 | import unittest 4 | import unittest.mock 5 | from unittest.mock import MagicMock 6 | 7 | from apport.packaging_impl import determine_packaging_implementation 8 | 9 | 10 | class TestPackagingImpl(unittest.TestCase): 11 | # pylint: disable=missing-function-docstring 12 | """Test functions in apport/packaging_impl/__init__.py.""" 13 | 14 | @unittest.mock.patch("platform.freedesktop_os_release") 15 | def test_determine_ubuntu(self, os_release_mock: MagicMock) -> None: 16 | os_release_mock.return_value = { 17 | "PRETTY_NAME": "Ubuntu 22.04.1 LTS", 18 | "NAME": "Ubuntu", 19 | "VERSION_ID": "22.04", 20 | "VERSION": "22.04.1 LTS (Jammy Jellyfish)", 21 | "VERSION_CODENAME": "jammy", 22 | "ID": "ubuntu", 23 | "ID_LIKE": "debian", 24 | } 25 | self.assertEqual(determine_packaging_implementation(), "apt_dpkg") 26 | os_release_mock.assert_called_once_with() 27 | 28 | @unittest.mock.patch("platform.freedesktop_os_release") 29 | def test_determine_debian_unstable(self, os_release_mock: MagicMock) -> None: 30 | os_release_mock.return_value = { 31 | "PRETTY_NAME": "Debian GNU/Linux bookworm/sid", 32 | "NAME": "Debian GNU/Linux", 33 | "ID": "debian", 34 | } 35 | self.assertEqual(determine_packaging_implementation(), "apt_dpkg") 36 | os_release_mock.assert_called_once_with() 37 | -------------------------------------------------------------------------------- /apport/com.ubuntu.apport.policy.in: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Apport 7 | https://wiki.ubuntu.com/Apport 8 | apport 9 | 10 | 11 | <_description>Collect system information 12 | <_message>Authentication is required to collect system information for this problem report 13 | 14 | auth_admin 15 | auth_admin 16 | auth_admin 17 | 18 | /usr/share/apport/root_info_wrapper 19 | 20 | 21 | 22 | 23 | <_description>System problem reports 24 | <_message>Please enter your password to access problem reports of system programs 25 | 26 | auth_admin 27 | auth_admin 28 | auth_admin 29 | 30 | /usr/share/apport/apport-gtk 31 | true 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /etc/apport/crashdb.conf: -------------------------------------------------------------------------------- 1 | # map crash database names to CrashDatabase implementations and URLs 2 | 3 | default = 'debug' 4 | 5 | databases = { 6 | 'ubuntu': { 7 | 'impl': 'launchpad', 8 | 'bug_pattern_url': 'http://people.canonical.com/~ubuntu-archive/bugpatterns/bugpatterns.xml', 9 | 'dupdb_url': 'http://people.canonical.com/~ubuntu-archive/apport-duplicates', 10 | 'distro': 'ubuntu', 11 | 'escalation_tag': 'bugpattern-needed', 12 | 'escalated_tag': 'bugpattern-written', 13 | }, 14 | 'fedora': { 15 | # NOTE this will change Fall '07 when RHT switches to bugzilla 3.x! 16 | 'impl': 'rhbugzilla', 17 | 'bug_pattern_url': 'http://qa.fedoraproject.org/apport/bugpatterns.xml', 18 | 'distro': 'fedora' 19 | }, 20 | 'debian': { 21 | 'impl': 'debian', 22 | 'distro': 'debian', 23 | 'smtphost': 'reportbug.debian.org', 24 | 'recipient': 'submit@bugs.debian.org', 25 | 'sender': '' 26 | }, 27 | 'snap-github': { 28 | 'impl': 'github', 29 | 'repository_owner': None, 30 | 'repository_name': None, 31 | 'github_app_id': 'bb74ee9268c04aeca4fa', 32 | 'labels': ['apport'], 33 | }, 34 | 'ubuntu-wsl': { 35 | 'impl': 'github', 36 | 'repository_owner': 'ubuntu', 37 | 'repository_name': 'WSL', 38 | 'github_app_id': 'bb74ee9268c04aeca4fa', 39 | 'labels': ['apport'], 40 | }, 41 | 'debug': { 42 | # for debugging 43 | 'impl': 'memory', 44 | 'bug_pattern_url': 'file:///tmp/bugpatterns.xml', 45 | 'distro': 'debug' 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /data/apport-checkreports: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2006 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Check if there are new reports for the invoking user. Exit with 0 if new 13 | reports are available, or with 1 if not.""" 14 | 15 | # pylint: disable=invalid-name 16 | # pylint: enable=invalid-name 17 | 18 | import argparse 19 | import sys 20 | 21 | from apport.fileutils import get_new_reports, get_new_system_reports 22 | from apport.packaging_impl import impl as packaging 23 | 24 | 25 | def parse_args(): 26 | """Parse command line options and return arguments.""" 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument( 29 | "-s", 30 | "--system", 31 | action="store_true", 32 | help="Check for crash reports from system users.", 33 | ) 34 | return parser.parse_args() 35 | 36 | 37 | args = parse_args() 38 | if args.system: 39 | reports = get_new_system_reports() 40 | else: 41 | reports = get_new_reports() 42 | 43 | if len(reports) > 0: 44 | for report in reports: 45 | print(report.split(".")[0].split("_")[-1]) 46 | if packaging.enabled(): 47 | sys.exit(0) 48 | else: 49 | print("new reports but apport disabled") 50 | sys.exit(2) 51 | else: 52 | sys.exit(1) 53 | -------------------------------------------------------------------------------- /apport/logging.py: -------------------------------------------------------------------------------- 1 | """Legacy logging functions.""" 2 | 3 | import os 4 | import sys 5 | import time 6 | import typing 7 | 8 | 9 | def log(message: str, timestamp: bool = False) -> None: 10 | """Log the given string to stdout. Prepend timestamp if requested.""" 11 | if timestamp: 12 | sys.stdout.write(f"{time.strftime('%x %X')}: ") 13 | print(message) 14 | 15 | 16 | def fatal(msg: str, *args: typing.Any) -> typing.NoReturn: 17 | """Print out an error message and exit the program.""" 18 | error(msg, *args) 19 | sys.exit(1) 20 | 21 | 22 | def error(msg: str, *args: typing.Any) -> None: 23 | """Print out an error message.""" 24 | if sys.stderr: 25 | sys.stderr.write("ERROR: ") 26 | sys.stderr.write(msg % args) 27 | sys.stderr.write("\n") 28 | 29 | 30 | def warning(msg: str, *args: typing.Any) -> None: 31 | """Print out an warning message.""" 32 | if sys.stderr: 33 | sys.stderr.write("WARNING: ") 34 | sys.stderr.write(msg % args) 35 | sys.stderr.write("\n") 36 | 37 | 38 | def memdbg(checkpoint: str) -> None: 39 | """Print current memory usage. 40 | 41 | This is only done if $APPORT_MEMDEBUG is set. 42 | """ 43 | if "APPORT_MEMDEBUG" not in os.environ or not sys.stderr: 44 | return 45 | 46 | memstat = {} 47 | with open("/proc/self/status", encoding="utf-8") as status_file: 48 | for line in status_file: 49 | if line.startswith("Vm"): 50 | (field, size, _) = line.split() 51 | memstat[field[:-1]] = int(size) / 1024.0 52 | 53 | sys.stderr.write( 54 | f"Size: {memstat['VmSize']:.1f} MB, RSS: {memstat['VmRSS']:.1f} MB," 55 | f" Stk: {memstat['VmStk']:.1f} MB @ {checkpoint}\n" 56 | ) 57 | -------------------------------------------------------------------------------- /tests/data/LP-PPA-daisy-pluckers-daisy-seeds.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGY0f9MBEACUn+75D2AcuoOw2FNPzmZ5bSXfC3hXd4SINbkurHr7jA6L6pQA 4 | 6S9iZu4vc7m47OCfvhXWzEQNdPkDhpHcrXxlpWs0PNgBCg4zhqDF69iL9oCVLhh1 5 | QbzfZkw7ikofNcnrzfh/OewKqkrMxF+MxsXNlYitXA4Vlura3u7JBcsMyvXdqoPP 6 | hMlsbyCEFeiHE8vHplwvWflljTb9Pe3AbJmSrcWnUrDkYqp2M0ryq9M/PrYEDLJ8 7 | yA4wcvDQuS1nWfRgGpxUtpmzZTDZB+fSpdOEh3YeBlP/WZUBRvmrbsy0fF6KhDVS 8 | ydXrAXWnEnwPnhXejYbJpfL5QFW79nUoYkN0luTrRN6qoxl9cDPLeFkLn5dCiIZs 9 | mF38hiwgoPrGSd1sodbGXfYdBulsgJW9Ng1a8fBn/U0v/kYcQEwspWpp3zcL+urh 10 | v7qAsMTu9RGIXCNH/amXr2Yd3dYwyQC7el2BKRmRRxhJ7VwZpFrl+6nmJoeAMAKh 11 | 2ka/Qf+yoTRAcQzuhEo/zo6GKFUwt68Axw30WAsTzo1uQa/swa1RC6ltJ7KE49/A 12 | uiy6gwyINE6i/DZ77oOAa1zkiIZqLbs6CRyygTFPBxbBICZ1wwDhXL5EvQrnHkBQ 13 | zkKEYZPbo9qF8OqFUqwXLDzq5lNecS1S6aHhNFkPcC38Z7V2mPXcq+44QwARAQAB 14 | tCBMYXVuY2hwYWQgUFBBIGZvciBEYWlzeSBQbHVja2Vyc4kCTgQTAQoAOBYhBO5M 15 | GD+PaRoy6vc/3CagP4ita0wzBQJmNH/TAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B 16 | AheAAAoJECagP4ita0wzVU8P/1T6+0Zr7WwAx+chhPxvKb9L9BcDopy/GrZeDGqm 17 | u/M+XEAgeHju9NwFGHmnDRY4O+O/aGite/2xSpG7dxedV+lp+Dan8R1aM+jZWFJO 18 | VZQE3n3hAsfkKs25N23PnAIkqwBdamuprHjGFj4L2b3uoG67Ty3RK5ITcBlHznMI 19 | jjnVvFOjbCCRtMBq8dxCvdpeU6YdO9WLVOUKhthy4yCGv96w4eThjiFS9hqn65eN 20 | zMcXrth9kLoRbK0Y+O0PCCT0djMk8mVKj6tme9ZKVwRUrSMavYn8dlxaLE6bDPYT 21 | DKIBiVAQZRkK0As1hsbI/1JYGz2X08R+zUHvhse6uOllSYYiZZSpKMq3LVS2IgiX 22 | ArTbbLffllad9ehiM/nqP+bWgNDpuHwFfXRaYd/u5uC1Ij0X3MREwmhn40s8rhos 23 | 6J0/M4THbl6r6KtnNzaVuy4bxM2ifsHoY+XpnT1wTxxu0Nzl6NWWWqifqHVg43/c 24 | trVOxB9hFIfPe5LZyiOxzqh2yJGyELFFYTScmXvM/W6WS6zzxmSUWuGlgF3AKLJX 25 | EbFH9D6vLd8KH+rw6MQ5gWHvhMj/2u558UsamYGGVPpKs8F7vyMECmVqeObBssf4 26 | WKFr56jjcykLd+xcQMbE2qG87SttRk2ui8XKXMqaMkCJ/36JVhqXhiLevqqDqy4D 27 | mSnT 28 | =dMaH 29 | -----END PGP PUBLIC KEY BLOCK----- 30 | -------------------------------------------------------------------------------- /tests/data/LP-PPA-apport-hackers-apport-autopkgtests.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBGALDfwBEACqAFoa9HpvexNGUlPfDs+VgKgksprT+nlpZUAaQzfAFVQinQW4 4 | yXn4uxaBVybtvQS723leswf/zpP8zg690MeD/crFi123BpeIqVLISmZCDWKvJfHO 5 | NoXwik+PbDFL4sVBESEO5vkKGwdTsRxmPzldQL70oQrkLnUOslBPl3XUfFAbUYtg 6 | MYRJ60PtelGLoJWsqhxyJGBajomwgE6Vd9O7LgkAmFh26UN48Uc9hiGf6f4Az3yD 7 | UdsFSMgDwdqkQWqgDQ8ULVkrCt1ZfwUUEvRHUHvSxe7v/+8oCTp8C1f+AzUrFRql 8 | aWa/2dhQBxBY71qlZapBgnVpK/1qpxwImxpH6QL2XhZlkaZbvEiTWFPbidTHRnB3 9 | LVfPM0ebyWR2fOMeVH9hx09kasocWMZYLpDXWe+snTbb3/Uqw9LDpx/9y7UkDN6A 10 | x6njy84chc3BIls9xHZKEgLd8DgxflXHoa1K3i4GTqwioRE2On624f6VxpGUSihF 11 | y4PzCKe0/L42chR2UawzE5WG6g0qxU8Cng6fuKlHCX3KGpTXrPs2CGaNTxhnj2Im 12 | 90KzAZXkGKfOLedYYsfpdHZT0ORoD3/mJKjZOeq2cpGDo7S53coe5rkSvnjNeEsJ 13 | d1M+p6Q29HbBbPJujedCAgDAJ7XjSUPgDs8oCcpNarll2RMmVYPXkSjKkQARAQAB 14 | tCxMYXVuY2hwYWQgUFBBIGZvciBBcHBvcnQgdXBzdHJlYW0gZGV2ZWxvcGVyc4kC 15 | TgQTAQoAOBYhBPUDj/fj4rA0bfGnp6rSzngTXv/1BQJgCw38AhsDBQsJCAcCBhUK 16 | CQgLAgQWAgMBAh4BAheAAAoJEKrSzngTXv/1/Q4P/iyNkW+/D0YzpXLm2Sb3ezjv 17 | ChB3Co1GN3dSI4hd5qjTho0uHpzZPNcjJYuGCysB78M8ZYRRoO6e+WrV6z/HQcVR 18 | uLFeYV6yJ6sVqQuOQ+SflwaFvWu9rXHtMbh/+NepA1D/FDKsD08/rIC/ni15eCLX 19 | WXleUxPGRz9GYWkrTeZzM/wrSyaG5pEQhhAc8lLgsVDy7BgcehWaSRwcAb//USPn 20 | 3eYsX6oqIXCqE/eyoSMKjE/FJ3yaTR4fJd61HzGotxRhLNK8TrVuxxfi8Cut4Ffx 21 | yOPj3WHPOkIT0qPVmLDWD/faJ6mu3FmhwiD03m3O7nsIcGHm+Yr4tpnmJcBaRHlP 22 | OvVrywjh6abcqnXLEJIBi4q2f+ffVV5E6luWLOOBFJ0PJmiLX4P0sWzH2OPCrREB 23 | 9YrdJwnW1dDg84LYWoUitxqGql1B2jjkC+PdG4+YN6bsH+chGfG/LOVWo6kpY1uA 24 | 4dojbxn63FZbjtfhQLn5Rc0J3kjTPOaEOCc9fCA+AGe9exX2K88W0YgOR44ZynUN 25 | 4OxvIckgIKxvnZrTrX2v0xbXRebt1RhhGdOoQ259G8vl75tEEMcGphcJ8w9rsIIm 26 | l74l5kGQARisjUNEMer+D+5In8CB95IZKh8eKBZerVmDg6osH7p8r/KentOOh8Xp 27 | gif/3n7CvdD4dg+8dk4q 28 | =vU9u 29 | -----END PGP PUBLIC KEY BLOCK----- 30 | -------------------------------------------------------------------------------- /tests/unit/test_hooks_wayland_session.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for data/general-hooks/wayland_session.py.""" 11 | 12 | import unittest 13 | import unittest.mock 14 | 15 | import problem_report 16 | from tests.helper import import_module_from_file 17 | from tests.paths import get_data_directory 18 | 19 | wayland_session = import_module_from_file( 20 | get_data_directory() / "general-hooks" / "wayland_session.py" 21 | ) 22 | 23 | 24 | class TestGeneralHookWaylandSession(unittest.TestCase): 25 | """Unit tests for data/general-hooks/wayland_session.py.""" 26 | 27 | @unittest.mock.patch.dict("os.environ", {"WAYLAND_DISPLAY": "wayland-0"}) 28 | def test_is_wayland_session(self) -> None: 29 | """Test add_info() for a Wayland session.""" 30 | report = problem_report.ProblemReport() 31 | wayland_session.add_info(report, None) 32 | self.assertEqual(set(report.keys()), {"Date", "ProblemType", "Tags"}) 33 | self.assertEqual(report["Tags"], "wayland-session") 34 | 35 | @unittest.mock.patch.dict("os.environ", {}, clear=True) 36 | def test_is_no_wayland_session(self) -> None: 37 | """Test add_info() for a session that isn't using Wayland.""" 38 | report = problem_report.ProblemReport() 39 | wayland_session.add_info(report, None) 40 | self.assertEqual(set(report.keys()), {"Date", "ProblemType"}) 41 | -------------------------------------------------------------------------------- /tests/integration/test_packaging_rpm.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the apport.packaging_impl.rpm module.""" 2 | 3 | import unittest 4 | 5 | from tests.helper import skip_if_command_is_missing 6 | 7 | try: 8 | from apport.packaging_impl.rpm import impl 9 | 10 | HAS_RPM = True 11 | except ImportError: 12 | HAS_RPM = False 13 | 14 | 15 | @unittest.skipUnless(HAS_RPM, "rpm module not available") 16 | @skip_if_command_is_missing("rpm") 17 | class TestPackagingRpm(unittest.TestCase): 18 | """Integration tests for the apport.packaging_impl.rpm module.""" 19 | 20 | def test_get_dependencies(self) -> None: 21 | """get_dependencies().""" 22 | deps = impl.get_dependencies("bash") 23 | self.assertNotEqual(deps, []) 24 | 25 | def test_get_header(self) -> None: 26 | """_get_header().""" 27 | # pylint: disable=protected-access 28 | hdr = impl._get_header("alsa-utils") 29 | self.assertEqual(hdr["n"], "alsa-utils") 30 | 31 | def test_get_headers_by_tag(self) -> None: 32 | """_get_headers_by_tag().""" 33 | # pylint: disable=protected-access 34 | headers_by_tag = impl._get_headers_by_tag("basenames", "/bin/bash") 35 | self.assertEqual(len(headers_by_tag), 1) 36 | self.assertTrue(headers_by_tag[0]["n"].startswith("bash")) 37 | 38 | def test_get_system_architecture(self) -> None: 39 | """get_system_architecture().""" 40 | arch = impl.get_system_architecture() 41 | # must be nonempty without line breaks 42 | self.assertNotEqual(arch, "") 43 | self.assertNotIn("\n", arch) 44 | 45 | def test_get_version(self) -> None: 46 | """get_version().""" 47 | ver = impl.get_version("bash") 48 | self.assertIsNotNone(ver) 49 | ver = impl.get_version("alsa-utils") 50 | self.assertIsNotNone(ver) 51 | -------------------------------------------------------------------------------- /data/dump_acpi_tables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """Dump ACPI tables.""" 4 | 5 | import os 6 | import stat 7 | import sys 8 | 9 | 10 | def dump_acpi_table(filename, tablename, out): 11 | """Dump a single ACPI table.""" 12 | if not os.access(filename, os.R_OK): 13 | return 14 | 15 | out.write(f"{tablename[0:4]} @ 0x0000000000000000\n") 16 | n = 0 17 | with open(filename, "rb") as f: 18 | hex_str = "" 19 | try: 20 | byte = f.read(1) 21 | while byte != b"": 22 | val = ord(byte) 23 | if (n & 15) == 0: 24 | if n > 65535: 25 | hex_str = f" {n:04X}: " 26 | else: 27 | hex_str = f" {n:04X}: " 28 | ascii_str = "" 29 | 30 | hex_str = f"{hex_str}{val:02X} " 31 | 32 | if (val < 32) or (val > 126): 33 | ascii_str = f"{ascii_str}." 34 | else: 35 | ascii_str = ascii_str + chr(val) 36 | n = n + 1 37 | if (n & 15) == 0: 38 | out.write(f"{hex_str} {ascii_str}\n") 39 | byte = f.read(1) 40 | finally: 41 | if (n % 16) != 0: 42 | hex_str += " " * (16 - n % 16) 43 | out.write(f"{hex_str} {ascii_str}\n") 44 | 45 | out.write("\n") 46 | 47 | 48 | def dump_acpi_tables(path, out): 49 | """Dump ACPI tables.""" 50 | tables = os.listdir(path) 51 | for tablename in tables: 52 | pathname = os.path.join(path, tablename) 53 | mode = os.stat(pathname).st_mode 54 | if stat.S_ISDIR(mode): 55 | dump_acpi_tables(pathname, out) 56 | else: 57 | dump_acpi_table(pathname, tablename, out) 58 | 59 | 60 | if os.path.isdir("/sys/firmware/acpi/tables"): 61 | dump_acpi_tables("/sys/firmware/acpi/tables", sys.stdout) 62 | -------------------------------------------------------------------------------- /man/dupdb-admin.1: -------------------------------------------------------------------------------- 1 | .TH dupdp\-admin 1 "August 01, 2007" "Martin Pitt" 2 | 3 | .SH NAME 4 | 5 | dupdb\-admin \- Manage the duplicate database for apport\-retrace. 6 | 7 | .SH SYNOPSIS 8 | 9 | .B dupdb\-admin \-f 10 | .I dbpath 11 | .B dump 12 | 13 | .B dupdb\-admin \-f 14 | .I dbpath 15 | .B changeid 16 | .I oldid newid 17 | 18 | .B dupdb\-admin \-f 19 | .I dbpath 20 | .B publish 21 | .I path 22 | 23 | .SH DESCRIPTION 24 | 25 | .BR apport\-retrace (1) 26 | has the capability of checking for duplicate bugs (amongst other 27 | things). It uses an SQLite database for keeping track of master bugs. 28 | .B dupdb\-admin 29 | is a small tool to manage that database. 30 | 31 | The central concept in that database is a "crash signature", a string 32 | that uniquely identifies a particular crash. It is built from the 33 | executable path name, the signal number or exception name, and the 34 | topmost functions of the stack trace. 35 | 36 | The database maps crash signatures to the 'master' crash id and thus 37 | can close duplicate crash reports with a reference to that master ID. 38 | It also tracks the status of crashes (open/fixed in a particular 39 | version) to be able to identify regressions. 40 | 41 | .SH MODES 42 | 43 | .TP 44 | .B dump 45 | Print a list of all database entries. 46 | 47 | .TP 48 | .B changeid 49 | Change the associated crash ID for a particular crash. 50 | 51 | .TP 52 | .B publish 53 | Export the duplicate database into a set of text files in the given directory 54 | which is suitable for WWW publishing. 55 | If the directory already exists, it will be updated. The new content is built 56 | in a new directory which is the given one with ".new" appended, then moved to 57 | the given name in an almost atomic way. 58 | 59 | .SH OPTIONS 60 | 61 | .TP 62 | .B \-f \fIpath\fR, \fB\-\-database-file\fR=\fIpath 63 | Path to the duplicate database SQLite file. 64 | 65 | .SH AUTHOR 66 | .B apport 67 | and the accompanying tools are developed by Martin Pitt 68 | . 69 | -------------------------------------------------------------------------------- /tests/integration/test_whoopsie_upload_all.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Integration tests for whoopsie-upload-all.""" 11 | 12 | import io 13 | import os 14 | import shutil 15 | import tempfile 16 | import unittest 17 | import unittest.mock 18 | from unittest.mock import MagicMock 19 | 20 | from tests.helper import import_module_from_file 21 | from tests.paths import get_data_directory 22 | 23 | whoopsie_upload_all = import_module_from_file( 24 | get_data_directory() / "whoopsie-upload-all" 25 | ) 26 | 27 | 28 | class TestWhoopsieUploadAll(unittest.TestCase): 29 | """Integration tests for whoopsie-upload-all.""" 30 | 31 | def setUp(self) -> None: 32 | self.report_dir = tempfile.mkdtemp() 33 | self.addCleanup(shutil.rmtree, self.report_dir) 34 | 35 | def _write_report(self, content: bytes) -> str: 36 | report = os.path.join(self.report_dir, "testcase.crash") 37 | with open(report, "wb") as report_file: 38 | report_file.write(content) 39 | return report 40 | 41 | @unittest.mock.patch("sys.stderr", new_callable=io.StringIO) 42 | def test_process_report_malformed_report(self, stderr_mock: MagicMock) -> None: 43 | """Test process_report() raises MalformedProblemReport.""" 44 | report = self._write_report(b"AB\xfc:CD\n") 45 | self.assertIsNone(whoopsie_upload_all.process_report(report)) 46 | self.assertIn( 47 | "Malformed problem report: 'ascii' codec can't decode byte 0xfc" 48 | " in position 2: ordinal not in range(128).", 49 | stderr_mock.getvalue(), 50 | ) 51 | -------------------------------------------------------------------------------- /tests/unit/test_apport_retrace.py: -------------------------------------------------------------------------------- 1 | """Unit tests for apport-retrace.""" 2 | 3 | import io 4 | import tempfile 5 | import unittest 6 | import unittest.mock 7 | from unittest.mock import MagicMock 8 | 9 | from tests.helper import import_module_from_file 10 | from tests.paths import get_bin_directory 11 | 12 | apport_retrace = import_module_from_file(get_bin_directory() / "apport-retrace") 13 | 14 | 15 | @unittest.mock.patch.object(apport_retrace, "get_crashdb") 16 | def test_malformed_crash_report(get_crashdb_mock: MagicMock) -> None: 17 | """Test apport-retrace to fail on malformed crash report.""" 18 | with ( 19 | tempfile.NamedTemporaryFile(mode="w+", suffix=".crash") as crash_file, 20 | unittest.mock.patch("sys.stderr", new_callable=io.StringIO) as stderr, 21 | ): 22 | crash_file.write( 23 | "ProblemType: Crash\nArchitecture: amd64\nPackage: gedit 46.2-2\n" 24 | ) 25 | crash_file.flush() 26 | return_code = apport_retrace.main(["-x", "/usr/bin/gedit", crash_file.name]) 27 | 28 | assert return_code == 2 29 | assert ( 30 | stderr.getvalue() 31 | == "ERROR: report file does not contain one of the required fields:" 32 | " CoreDump DistroRelease\n" 33 | ) 34 | get_crashdb_mock.assert_called_once_with(None) 35 | 36 | 37 | @unittest.mock.patch.object(apport_retrace, "get_crashdb") 38 | def test_malformed_kernel_crash_report(get_crashdb_mock: MagicMock) -> None: 39 | """Test apport-retrace to fail on malformed kernel crash report.""" 40 | with ( 41 | tempfile.NamedTemporaryFile(mode="w+", suffix=".crash") as crash_file, 42 | unittest.mock.patch("sys.stderr", new_callable=io.StringIO) as stderr, 43 | ): 44 | crash_file.write("ProblemType: KernelCrash\n") 45 | crash_file.flush() 46 | return_code = apport_retrace.main([crash_file.name]) 47 | 48 | assert return_code == 2 49 | assert ( 50 | stderr.getvalue() == "ERROR: report file does not contain the required fields\n" 51 | ) 52 | get_crashdb_mock.assert_called_once_with(None) 53 | -------------------------------------------------------------------------------- /debhelper/dh_apport: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | =head1 NAME 4 | 5 | dh_installapport - install apport package hooks 6 | 7 | =cut 8 | 9 | use strict; 10 | 11 | use Debian::Debhelper::Dh_Lib; 12 | 13 | =head1 SYNOPSIS 14 | 15 | B [S>] 16 | 17 | =head1 DESCRIPTION 18 | 19 | dh_apport is a debhelper program that installs apport package hooks into 20 | package build directories. 21 | 22 | =head1 FILES 23 | 24 | =over 4 25 | 26 | =item debian/I.apport 27 | 28 | Installed into /usr/share/apport/package-hooks/I.py in the package 29 | build directory. This file is used to control apport's bug filing for this 30 | package. 31 | 32 | =item debian/source.apport 33 | 34 | Installed into /usr/share/apport/package-hooks/source_I.py (where 35 | I is the current source package name) in the package build directory of 36 | the first package dh_apport is told to act on. By default, this is the first 37 | binary package in debian/control, but if you use -p, -i, or -a flags, it 38 | will be the first package specified by those flags. This file is used to 39 | control apport's bug filing for all binary packages built by this source 40 | package. 41 | 42 | =back 43 | 44 | =cut 45 | 46 | init(); 47 | 48 | foreach my $package (@{$dh{DOPACKAGES}}) { 49 | next if is_udeb($package); 50 | 51 | my $tmp=tmpdir($package); 52 | my $hooksdir="$tmp/usr/share/apport/package-hooks"; 53 | my $file=pkgfile($package,"apport"); 54 | 55 | if ($file ne '') { 56 | if (! -d $hooksdir) { 57 | doit("install","-d",$hooksdir); 58 | } 59 | doit("install","-p","-m644",$file,"$hooksdir/$package.py"); 60 | } 61 | 62 | if (-e "debian/source.apport" && $package eq $dh{FIRSTPACKAGE}) { 63 | if (! -d $hooksdir) { 64 | doit("install","-d",$hooksdir); 65 | } 66 | my $src=sourcepackage(); 67 | doit("install","-p","-m644","debian/source.apport","$hooksdir/source_$src.py"); 68 | } 69 | } 70 | 71 | =head1 SEE ALSO 72 | 73 | L 74 | 75 | This program is a part of apport. 76 | 77 | =head1 AUTHOR 78 | 79 | Colin Watson 80 | 81 | Copyright (C) 2009 Canonical Ltd., licensed under the GNU GPL v2 or later. 82 | 83 | =cut 84 | -------------------------------------------------------------------------------- /tests/system/test_github_query.py: -------------------------------------------------------------------------------- 1 | """System tests for the apport.crashdb_impl.github module.""" 2 | 3 | import unittest 4 | from unittest.mock import Mock 5 | 6 | import apport.crashdb_impl.github 7 | 8 | SOME_ID = "a654870577ad2a2ab5b1" 9 | 10 | 11 | class TestGitHubQuery(unittest.TestCase): 12 | """System tests for the apport.crashdb_impl.github module.""" 13 | 14 | def setUp(self) -> None: 15 | self.crashdb = self._get_gh_database("Lorem", "Ipsum") 16 | self.message_cb = Mock() 17 | self.github = apport.crashdb_impl.github.Github( 18 | self.crashdb.app_id, self.message_cb 19 | ) 20 | 21 | def test_api_authentication(self) -> None: 22 | """Test if we can contact Github authentication service.""" 23 | with self.github as github: 24 | data = {"client_id": SOME_ID, "scope": "public_repo"} 25 | url = "https://github.com/login/device/code" 26 | response = github.api_authentication(url, data) 27 | # Sample response: 28 | # { 29 | # 'device_code': '35fe1f072913d46c00ad3e4a83e57facfb758f67', 30 | # 'user_code': '5A5D-7210', 31 | # 'verification_uri': 'https://github.com/login/device', 32 | # 'expires_in': 899, 33 | # 'interval': 5 34 | # } 35 | self.assertIsInstance(response["device_code"], str) 36 | self.assertIsInstance(response["user_code"], str) 37 | self.assertIsInstance(response["device_code"], str) 38 | self.assertIsInstance(response["expires_in"], int) 39 | self.assertIsInstance(response["interval"], int) 40 | 41 | @staticmethod 42 | def _get_gh_database( 43 | repository_owner: str, repository_name: str 44 | ) -> apport.crashdb_impl.github.CrashDatabase: 45 | return apport.crashdb_impl.github.CrashDatabase( 46 | None, 47 | { 48 | "impl": "github", 49 | "repository_owner": repository_owner, 50 | "repository_name": repository_name, 51 | "github_app_id": SOME_ID, 52 | "labels": ["apport"], 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Test suite for Apport 2 | ===================== 3 | 4 | The test suite for Apport is grouped into different groups described below. If 5 | the tests are run in place, they test the code in place. If the `tests` 6 | directory is copied outside, the tests will run against the installation. 7 | 8 | Linter tests 9 | ------------ 10 | 11 | The script [run-linters](./run-linters) runs following linters on the source 12 | code: 13 | 14 | * black 15 | * isort 16 | * mypy 17 | * pycodestyle 18 | * pydocstyle 19 | * pylint 20 | 21 | Unit tests 22 | ---------- 23 | 24 | The [unit directory](./unit) contains unit tests. These test cases test 25 | individual functions or methods. They execute fast (a few milliseconds per test 26 | at most) and do not interact with the outside system. All outside access is 27 | mocked. The unit tests can be run from the top directory by calling: 28 | 29 | ``` 30 | python3 -m pytest tests/unit/ 31 | ``` 32 | 33 | Integration tests 34 | ----------------- 35 | 36 | The [integration directory](./integration) contains integration tests. These 37 | test cases test full scripts but also individual functions or methods. They 38 | execute relatively quickly (a few seconds per test at most) and interact with 39 | the outside system in a non invasive manner. Temporary directories are created 40 | in case the test needs to write to them. The integration tests can be run from 41 | the top directory by calling: 42 | 43 | ``` 44 | python3 -m pytest tests/integration/ 45 | ``` 46 | 47 | System tests 48 | ------------ 49 | 50 | The [system directory](./system) contains system tests. It also contains 51 | integration tests that need special environment setup or have a long execution 52 | time. The GTK and KDE UI integration tests need a window system, which can be 53 | provided by `xvfb-run`. Some integration tests query https://launchpad.net/. 54 | These tests can be skipped by setting the environment variable 55 | `SKIP_ONLINE_TESTS` to something non empty. The test in 56 | [test_python_crashes.py](./system/test_python_crashes.py) need a running D-Bus 57 | daemon. Whit a D-Bus daemon running, the system tests can be run from the top 58 | directory by calling: 59 | 60 | ``` 61 | GDK_BACKEND=x11 xvfb-run python3 -m pytest tests/system/ 62 | ``` 63 | -------------------------------------------------------------------------------- /tests/system/test_apport_valgrind.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Canonical Ltd. 2 | # Author: Kyle Nitzsche 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """System tests for bin/apport-valgrind.""" 11 | 12 | import os 13 | import shutil 14 | import subprocess 15 | import tempfile 16 | import unittest 17 | 18 | from tests.helper import get_gnu_coreutils_cmd, skip_if_command_is_missing 19 | from tests.paths import local_test_environment 20 | 21 | with open("/proc/meminfo", encoding="utf-8") as f: 22 | for line in f.readlines(): 23 | if line.startswith("MemTotal"): 24 | MEM_TOTAL_MiB = int(line.split()[1]) // 1024 25 | break 26 | 27 | 28 | @skip_if_command_is_missing("valgrind") 29 | class TestApportValgrind(unittest.TestCase): 30 | """System tests for bin/apport-valgrind.""" 31 | 32 | env: dict[str, str] 33 | 34 | @classmethod 35 | def setUpClass(cls) -> None: 36 | cls.env = os.environ | local_test_environment() 37 | 38 | def setUp(self) -> None: 39 | self.workdir = tempfile.mkdtemp() 40 | self.pwd = os.getcwd() 41 | 42 | def tearDown(self) -> None: 43 | shutil.rmtree(self.workdir) 44 | os.chdir(self.pwd) 45 | 46 | @unittest.skipIf(MEM_TOTAL_MiB < 2000, f"{MEM_TOTAL_MiB} MiB is not enough memory") 47 | def test_sandbox_cache_options(self) -> None: 48 | """apport-valgrind creates a user specified sandbox and cache""" 49 | sandbox = os.path.join(self.workdir, "test-sandbox") 50 | cache = os.path.join(self.workdir, "test-cache") 51 | 52 | cmd = [ 53 | "apport-valgrind", 54 | "--sandbox-dir", 55 | sandbox, 56 | "--cache", 57 | cache, 58 | get_gnu_coreutils_cmd("true"), 59 | ] 60 | subprocess.check_call(cmd, env=self.env) 61 | 62 | self.assertTrue( 63 | os.path.exists(sandbox), 64 | f"A sandbox directory {sandbox} was specified but was not created", 65 | ) 66 | 67 | self.assertTrue( 68 | os.path.exists(cache), 69 | f"A cache directory {cache} was specified but was not created", 70 | ) 71 | -------------------------------------------------------------------------------- /apport/__init__.py: -------------------------------------------------------------------------------- 1 | """Apport Python module.""" 2 | 3 | # for faster module loading and avoiding circular dependencies 4 | # pylint: disable=import-outside-toplevel 5 | 6 | import typing 7 | 8 | # Import apport.packaging to shadow it afterwards. 9 | import apport.packaging as _ # noqa: F401 10 | 11 | __all__ = [ 12 | "Report", 13 | "error", 14 | "fatal", 15 | "log", 16 | "memdbg", 17 | "packaging", 18 | "unicode_gettext", 19 | "warning", 20 | ] 21 | 22 | if typing.TYPE_CHECKING: # pragma: no cover 23 | from apport.packaging_impl import impl as packaging 24 | from apport.report import Report 25 | else: 26 | # wraps an object; pylint: disable-next=invalid-name 27 | def Report(*args, **kwargs): 28 | """Lazy loading of apport.report.Report().""" 29 | from apport.report import Report as cls 30 | 31 | return cls(*args, **kwargs) 32 | 33 | # wrapper object; pylint: disable-next=too-few-public-methods 34 | class _LazyLoadingPackaging: 35 | def __getattribute__(self, name): 36 | # The packaging object will be replaced by the imported object. 37 | # pylint: disable-next=global-statement 38 | global packaging 39 | from apport.packaging_impl import impl as packaging 40 | 41 | return packaging.__getattribute__(name) 42 | 43 | packaging = _LazyLoadingPackaging() 44 | 45 | 46 | def _logging_function(function_name): 47 | def _wrapped_logging_function(*args, **kwargs): 48 | import warnings 49 | 50 | import apport.logging 51 | 52 | warnings.warn( 53 | f"apport.{function_name}() is deprecated." 54 | f" Please use apport.logging.{function_name}() directly instead.", 55 | DeprecationWarning, 56 | stacklevel=2, 57 | ) 58 | return getattr(apport.logging, function_name)(*args, **kwargs) 59 | 60 | return _wrapped_logging_function 61 | 62 | 63 | error = _logging_function("error") 64 | fatal = _logging_function("fatal") 65 | log = _logging_function("log") 66 | memdbg = _logging_function("memdbg") 67 | warning = _logging_function("warning") 68 | 69 | 70 | def unicode_gettext(message): 71 | """Return the localized translation of message.""" 72 | import gettext 73 | import warnings 74 | 75 | warnings.warn( 76 | "apport.unicode_gettext() is deprecated." 77 | " Please use gettext.gettext() directly instead.", 78 | PendingDeprecationWarning, 79 | stacklevel=2, 80 | ) 81 | return gettext.gettext(message) 82 | -------------------------------------------------------------------------------- /apport/REThread.py: -------------------------------------------------------------------------------- 1 | """Enhanced Thread with support for return values and exception propagation.""" 2 | 3 | # Copyright (C) 2007 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | # pylint: disable=invalid-name 13 | # pylint: enable=invalid-name 14 | 15 | import sys 16 | import threading 17 | 18 | 19 | class REThread(threading.Thread): 20 | """Thread with return values and exception propagation. 21 | 22 | The thread is marked as daemon thread. The entire Python program exits 23 | when no alive non-daemon threads are left. 24 | """ 25 | 26 | def __init__(self, group=None, target=None, name=None, args=(), kwargs=None): 27 | """Initialize Thread, identical to threading.Thread.__init__().""" 28 | if kwargs is None: 29 | kwargs = {} 30 | 31 | threading.Thread.__init__(self, group, target, name, args, kwargs, daemon=True) 32 | self.__target = target 33 | self.__args = args 34 | self.__kwargs = kwargs 35 | self._retval = None 36 | self._exception = None 37 | 38 | def run(self): 39 | """Run target function, identical to threading.Thread.run().""" 40 | if self.__target: 41 | try: 42 | self._retval = self.__target(*self.__args, **self.__kwargs) 43 | except BaseException: # pylint: disable=broad-except 44 | if sys: # pylint: disable=using-constant-test 45 | self._exception = sys.exc_info() 46 | 47 | def return_value(self): 48 | """Return value from target function. 49 | 50 | This can only be called after the thread has finished, i. e. when 51 | is_alive() is False and did not terminate with an exception. 52 | """ 53 | assert not self.is_alive() 54 | assert not self._exception 55 | return self._retval 56 | 57 | def exc_info(self): 58 | """Return (type, value, traceback) of the exception caught in run().""" 59 | return self._exception 60 | 61 | def exc_raise(self): 62 | """Raise the exception caught in the thread. 63 | 64 | Do nothing if no exception was caught. 65 | """ 66 | if self._exception: 67 | raise self._exception[1].with_traceback(self._exception[2]) 68 | -------------------------------------------------------------------------------- /bin/apport-bug: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Determine the most appropriate Apport user interface (GTK/KDE/CLI) and file a 3 | # bug with it. 4 | # 5 | # Copyright (C) 2009 Canonical Ltd. 6 | # Author: Martin Pitt 7 | # 8 | # This program is free software; you can redistribute it and/or modify it 9 | # under the terms of the GNU General Public License as published by the 10 | # Free Software Foundation; either version 2 of the License, or (at your 11 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 12 | # the full text of the license. 13 | 14 | # Explicitly set the PATH to that of ENV_SUPATH in /etc/login.defs. We need do 15 | # this so that confined applications using ubuntu-browsers.d/ubuntu-integration 16 | # cannot abuse the environment to escape AppArmor confinement via this script 17 | # (LP: #1045986). This can be removed once AppArmor supports environment 18 | # filtering (LP: #1045985) 19 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin 20 | 21 | # locate path of a particular program 22 | find_program() { 23 | for p in /usr/local/bin /usr/bin /usr/local/share/apport /usr/share/apport; do 24 | if [ -x $p/$1 ]; then 25 | RET="$p/$1" 26 | return 27 | fi 28 | done 29 | unset RET 30 | } 31 | 32 | # determine which UIs are available, and where 33 | find_programs() { 34 | find_program "apport-cli" 35 | CLI="$RET" 36 | find_program "apport-gtk" 37 | GTK="$RET" 38 | find_program "apport-kde" 39 | KDE="$RET" 40 | } 41 | 42 | # 43 | # main 44 | # 45 | 46 | find_programs 47 | 48 | export APPORT_INVOKED_AS="$0" 49 | 50 | # check for X 51 | if [ -z "$DISPLAY" -a -z "$WAYLAND_DISPLAY" ]; then 52 | if [ -n "$CLI" ] ; then 53 | $CLI "$@" 54 | else 55 | echo "Neither \$DISPLAY nor \$WAYLAND_DISPLAY is set. You need apport-cli to make this program work." >&2 56 | exit 1 57 | fi 58 | 59 | # do we have a running Gnome/KDE session? 60 | elif pgrep -u `id -u` -x gnome-session >/dev/null && \ 61 | [ -n "$GTK" ]; then 62 | $GTK "$@" 63 | elif pgrep -u `id -u` -x ksmserver >/dev/null && \ 64 | [ -n "$KDE" ]; then 65 | $KDE "$@" 66 | 67 | # fall back to calling whichever is available 68 | elif [ -n "$GTK" ]; then 69 | $GTK "$@" 70 | elif [ -n "$KDE" ]; then 71 | $KDE "$@" 72 | elif [ -n "$CLI" ]; then 73 | if [ -z "$TERM" ] && [ -x "$XTERM" ]; then 74 | "$XTERM" -e "$CLI" "$@" 75 | else 76 | $CLI "$@" 77 | fi 78 | 79 | else 80 | echo "Neither apport-gtk, apport-kde, apport-cli, or whoopsie-upload-all are installed. Install either to make this program work." >&2 81 | exit 1 82 | fi 83 | -------------------------------------------------------------------------------- /data/iwlwifi_error_dump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2014 Canonical Ltd. 4 | # Author: Seth Forshee 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about an iwlwifi firmware error dump.""" 13 | 14 | import os 15 | import re 16 | import sys 17 | 18 | import apport.fileutils 19 | import apport.logging 20 | import apport.report 21 | from apport.hookutils import command_output 22 | from apport.packaging_impl import impl as packaging 23 | 24 | 25 | def main() -> None: 26 | """Collect information about an iwlwifi firmware error dump.""" 27 | if len(sys.argv) != 2: 28 | sys.exit(1) 29 | 30 | phy = os.path.basename(sys.argv[1]) 31 | sysfs_path = f"/sys/kernel/debug/ieee80211/{phy}/iwlwifi/iwlmvm/fw_error_dump" 32 | if not os.path.exists(sysfs_path): 33 | sys.exit(1) 34 | 35 | pr = apport.report.Report("KernelCrash") 36 | pr.add_package(packaging.get_kernel_package()) 37 | pr["Title"] = "iwlwifi firmware error" 38 | pr.add_os_info() 39 | 40 | # Get iwl firmware version and error code from dmesg 41 | dmesg = command_output(["dmesg"]) 42 | regex = re.compile( 43 | "^.*iwlwifi [0-9a-fA-F:]{10}\\.[0-9a-fA-F]: Loaded firmware version:" 44 | " ([0-9\\.]+).*\\n.*iwlwifi [0-9a-fA-F:]{10}\\.[0-9a-fA-F]:" 45 | " (0x[0-9A-F]{8} \\| [A-Z_]+)", 46 | re.MULTILINE, 47 | ) 48 | m = regex.findall(dmesg) 49 | if m: 50 | v = m[len(m) - 1] 51 | fw_version = v[0] 52 | error_code = v[1] 53 | 54 | pr["IwlFwVersion"] = fw_version 55 | pr["IwlErrorCode"] = error_code 56 | pr["DuplicateSignature"] = f"iwlwifi:{fw_version}:{error_code}" 57 | pr["Title"] += f": {error_code}" 58 | 59 | # Get iwl firmware dump file from debugfs 60 | try: 61 | with open(sysfs_path, "rb") as f: 62 | pr["IwlFwDump"] = f.read() 63 | # Firmware dump could contain sensitive information 64 | pr["LaunchpadPrivate"] = "yes" 65 | pr["LaunchpadSubscribe"] = "canonical-kernel-team" 66 | except OSError: 67 | pass 68 | 69 | try: 70 | with apport.fileutils.make_report_file(pr) as f: 71 | pr.write(f) 72 | except OSError as error: 73 | apport.logging.fatal("Cannot create report: %s", str(error)) 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /kde/choices.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DialogChoices 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 182 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | text 27 | 28 | 29 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 30 | 31 | 32 | true 33 | 34 | 35 | 1 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | buttons 64 | accepted() 65 | DialogChoices 66 | accept() 67 | 68 | 69 | 199 70 | 164 71 | 72 | 73 | 199 74 | 90 75 | 76 | 77 | 78 | 79 | buttons 80 | rejected() 81 | DialogChoices 82 | reject() 83 | 84 | 85 | 199 86 | 164 87 | 88 | 89 | 199 90 | 90 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /data/recoverable_problem: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """Report an error that can be recovered from. 4 | 5 | This application should be called with its standard input pipe fed a 6 | nul-separated list of key-value pairs. 7 | """ 8 | 9 | # Copyright (C) 2012 Canonical Ltd. 10 | # Author: Evan Dandrea 11 | # 12 | # This program is free software; you can redistribute it and/or modify it 13 | # under the terms of the GNU General Public License as published by the 14 | # Free Software Foundation; either version 2 of the License, or (at your 15 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 16 | # the full text of the license. 17 | 18 | import argparse 19 | import os 20 | import sys 21 | 22 | import apport.report 23 | 24 | 25 | # pylint: disable-next=missing-function-docstring 26 | def main() -> None: 27 | # Check parameters 28 | argparser = argparse.ArgumentParser("%(prog) [options]") 29 | argparser.add_argument("-p", "--pid", action="store", type=int, dest="optpid") 30 | args = argparser.parse_args() 31 | 32 | # Build the base report 33 | report = apport.report.Report("RecoverableProblem") 34 | 35 | # If we have a parameter pid, use that, otherwise look to our parent 36 | if args.optpid: 37 | report.pid = args.optpid 38 | else: 39 | report.pid = os.getppid() 40 | 41 | # Grab PID info right away, as we don't know how long it'll stick around 42 | try: 43 | report.add_proc_info(report.pid) 44 | except ValueError as error: 45 | # The process may have gone away before we could get to it. 46 | if str(error) == "invalid process": 47 | return 48 | 49 | # Get the info on the bug 50 | items = sys.stdin.read().split("\0") 51 | if len(items) % 2 != 0: 52 | sys.stderr.write( 53 | "Expect even number of fields in stdin," 54 | " needs to have pairs of key and value.\n" 55 | ) 56 | sys.exit(1) 57 | 58 | while items: 59 | key = items.pop(0) 60 | if not items: 61 | break 62 | value = items.pop(0) 63 | report[key] = value 64 | 65 | # Put in the more general stuff 66 | report.add_os_info() 67 | report.add_user_info() 68 | 69 | duplicate_signature = report.get("DuplicateSignature", "") 70 | exec_path = report.get("ExecutablePath", "") 71 | if exec_path and duplicate_signature: 72 | report["DuplicateSignature"] = f"{exec_path}:{duplicate_signature}" 73 | 74 | # Write the final report 75 | try: 76 | with apport.fileutils.make_report_file(report) as report_file: 77 | report.write(report_file) 78 | except OSError as error: 79 | apport.fatal("Cannot create report: %s", str(error)) 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /etc/init.d/apport: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: apport 5 | # Required-Start: $local_fs $remote_fs 6 | # Required-Stop: $local_fs $remote_fs 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 9 | # Short-Description: automatic crash report generation 10 | ### END INIT INFO 11 | 12 | DESC="automatic crash report generation" 13 | NAME=apport 14 | AGENT=/usr/share/apport/apport 15 | SCRIPTNAME=/etc/init.d/$NAME 16 | 17 | # Exit if the package is not installed 18 | [ -x "$AGENT" ] || exit 0 19 | 20 | # read default file 21 | enabled=1 22 | [ -e /etc/default/$NAME ] && . /etc/default/$NAME || true 23 | 24 | # Define LSB log_* functions. 25 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 26 | . /lib/lsb/init-functions 27 | 28 | # 29 | # Function that starts the daemon/service 30 | # 31 | do_start() 32 | { 33 | # Return 34 | # 0 if daemon has been started 35 | # 1 if daemon was already running 36 | # 2 if daemon could not be started 37 | 38 | $AGENT --start 39 | 40 | # check for incomplete suspend/resume or hibernate 41 | if [ -e /var/lib/pm-utils/status ]; then 42 | /usr/share/apport/apportcheckresume || true 43 | rm -f /var/lib/pm-utils/status 44 | rm -f /var/lib/pm-utils/resume-hang.log 45 | fi 46 | } 47 | 48 | # 49 | # Function that stops the daemon/service 50 | # 51 | do_stop() 52 | { 53 | # Return 54 | # 0 if daemon has been stopped 55 | # 1 if daemon was already stopped 56 | # 2 if daemon could not be stopped 57 | # other if a failure occurred 58 | 59 | # Check for a hung resume. If we find one try and grab everything 60 | # we can to aid in its discovery. 61 | if [ -e /var/lib/pm-utils/status ]; then 62 | ps -wwef >/var/lib/pm-utils/resume-hang.log 63 | fi 64 | 65 | $AGENT --stop 66 | } 67 | 68 | case "$1" in 69 | start) 70 | # don't start in containers 71 | grep -zqs '^container=' /proc/1/environ && exit 0 72 | 73 | [ "$enabled" = "1" ] || [ "$force_start" = "1" ] || exit 0 74 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC:" "$NAME" 75 | do_start 76 | case "$?" in 77 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 78 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 79 | esac 80 | ;; 81 | stop) 82 | # don't stop in containers 83 | grep -zqs '^container=' /proc/1/environ && exit 0 84 | 85 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC:" "$NAME" 86 | do_stop 87 | case "$?" in 88 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 89 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 90 | esac 91 | ;; 92 | restart|force-reload) 93 | $0 stop || true 94 | $0 start 95 | ;; 96 | *) 97 | echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 98 | exit 3 99 | ;; 100 | esac 101 | 102 | : 103 | -------------------------------------------------------------------------------- /doc/symptoms.md: -------------------------------------------------------------------------------- 1 | Apport symptom scripts 2 | ====================== 3 | 4 | In some cases it is quite hard for a bug reporter to figure out which package to 5 | file a bug against, especially for functionality which spans multiple packages. 6 | For example, sound problems are divided between the kernel, alsa, pulseaudio, 7 | and gstreamer. 8 | 9 | Apport supports an extension of the notion of package hooks to do an 10 | interactive "symptom based" bug reporting. Calling the UI with just `-f` and 11 | not specifying any package name shows the available symptoms, the user selects 12 | the matching category, and the symptom scripts can do some question & answer 13 | game to finally figure out which package to file it against and which 14 | information to collect. Alternatively, the UIs can be invoked with 15 | `-s symptom-name`. 16 | 17 | Structure 18 | ========= 19 | 20 | Symptom scripts go into `/usr/share/apport/symptoms/symptomname.py`, and have 21 | the following structure: 22 | 23 | ```python 24 | description = "One-line description" 25 | 26 | 27 | def run(report, ui): 28 | problem = ui.choice( 29 | "What particular problem do you observe?", 30 | ["Thing 1", "Thing 2", ...], 31 | ) 32 | 33 | # collect debugging information here, ask further questions, and figure out 34 | # package name 35 | return "packagename" 36 | ``` 37 | 38 | They need to define a `run()` method which can use the passed `HookUI` object 39 | for interactive questions (see [package-hooks.md](./package-hooks.md) for 40 | details about this). 41 | 42 | `run()` can optionally add information to the passed report object, such as 43 | tags. Before `run()` is called, Apport already added the OS and user information 44 | to the report object. 45 | 46 | After the symptom `run()` method, Apport adds package related information and 47 | calls the package hooks as usual. 48 | 49 | `run()` has to return the (binary) package name to file the bug against. 50 | 51 | Just as package hooks, if the user canceled an interactive question for which 52 | the script requires an answer, `run()` should raise `StopIteration`, which will 53 | stop the bug reporting process. 54 | 55 | Example 56 | ======= 57 | 58 | ```python 59 | import apport 60 | 61 | description = "External or internal storage devices (e. g. USB sticks)" 62 | 63 | 64 | def run(report, ui): 65 | problem = ui.choice( 66 | "What particular problem do you observe?", 67 | [ 68 | "Removable storage device is not mounted automatically", 69 | "Internal hard disk partition cannot be mounted manually", 70 | # ... 71 | ], 72 | ) 73 | 74 | # collect debugging information here, ask further questions 75 | 76 | if not kernel_detected: 77 | return apport.packaging.get_kernel_package() 78 | if not udev_detected: 79 | return "udev" 80 | return "devicekit-disks" 81 | ``` 82 | -------------------------------------------------------------------------------- /tests/integration/test_apport_checkreports.py: -------------------------------------------------------------------------------- 1 | """Test apport-checkreports""" 2 | 3 | # Copyright (C) 2022 Canonical Ltd. 4 | # Author: Benjamin Drung 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | import os 13 | import shutil 14 | import subprocess 15 | import tempfile 16 | import unittest 17 | 18 | import apport.report 19 | from tests.paths import get_data_directory, local_test_environment 20 | 21 | 22 | class TestApportCheckreports(unittest.TestCase): 23 | # pylint: disable=missing-function-docstring 24 | """Test apport-checkreports""" 25 | 26 | def setUp(self) -> None: 27 | self.data_dir = get_data_directory() 28 | self.env = os.environ | local_test_environment() 29 | 30 | self.report_dir = tempfile.mkdtemp() 31 | self.env["APPORT_REPORT_DIR"] = self.report_dir 32 | 33 | def tearDown(self) -> None: 34 | shutil.rmtree(self.report_dir) 35 | 36 | def _call( 37 | self, 38 | args: list | None = None, 39 | expected_returncode: int = 0, 40 | expected_stdout: str = "", 41 | ) -> None: 42 | cmd = [str(self.data_dir / "apport-checkreports")] 43 | if args: 44 | cmd += args 45 | process = subprocess.run( 46 | cmd, 47 | check=False, 48 | env=self.env, 49 | stdout=subprocess.PIPE, 50 | stderr=subprocess.PIPE, 51 | text=True, 52 | ) 53 | self.assertEqual(process.returncode, expected_returncode) 54 | self.assertEqual(process.stdout, expected_stdout) 55 | self.assertEqual(process.stderr, "") 56 | 57 | def _write_report(self, filename: str, user: bool = True) -> None: 58 | path = f"{self.report_dir}/{filename}" 59 | report = apport.report.Report() 60 | with open(path, "wb") as report_file: 61 | report.write(report_file) 62 | 63 | if user and os.geteuid() == 0: 64 | os.chown(path, 1000, -1) 65 | 66 | def test_has_no_system_report(self) -> None: 67 | self._write_report("_bin_sleep.1000.crash") 68 | self._call(args=["--system"], expected_returncode=1) 69 | 70 | @unittest.skipIf(os.geteuid() != 0, "this test needs to be run as root") 71 | def test_has_system_report(self) -> None: 72 | self._write_report("_usr_bin_yes.0.crash", user=False) 73 | self._call(args=["-s"], expected_returncode=0, expected_stdout="yes\n") 74 | 75 | def test_has_user_report(self) -> None: 76 | self._write_report("_bin_sleep.1000.crash") 77 | self._call(expected_returncode=0, expected_stdout="sleep\n") 78 | 79 | def test_no_report(self) -> None: 80 | self._call(expected_returncode=1) 81 | -------------------------------------------------------------------------------- /data/java_uncaught_exception: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """Receive details from ApportUncaughtExceptionHandler. 4 | 5 | This generates and saves a problem report. 6 | """ 7 | 8 | # Copyright 2010 Canonical Ltd. 9 | # Author: Matt Zimmerman 10 | # 11 | # This program is free software; you can redistribute it and/or modify it 12 | # under the terms of the GNU General Public License as published by the 13 | # Free Software Foundation; either version 2 of the License, or (at your 14 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 15 | # the full text of the license. 16 | 17 | import os 18 | import sys 19 | import urllib.parse 20 | 21 | import apport.report 22 | from apport.packaging_impl import impl as packaging 23 | 24 | 25 | def make_title(report): 26 | """Construct crash title from stack trace.""" 27 | lines = report["StackTrace"].split("\n") 28 | message = lines[0].strip() 29 | stackframe = lines[1].strip() 30 | return f"{message} in {stackframe}" 31 | 32 | 33 | # pylint: disable-next=missing-function-docstring 34 | def main() -> int: 35 | if not packaging.enabled(): 36 | return 1 37 | 38 | # read from the JVM process a sequence of key, value delimited by null 39 | # bytes 40 | items = sys.stdin.read().split("\0") 41 | data = {} 42 | while items: 43 | key = items.pop(0) 44 | if not items: 45 | break 46 | value = items.pop(0) 47 | data[key] = value 48 | 49 | # create report 50 | report = apport.report.Report(problem_type="Crash") 51 | # assume our parent is the JVM process 52 | report.pid = os.getppid() 53 | 54 | report.add_os_info() 55 | report.add_proc_info() 56 | # these aren't relevant because the crash was in bytecode 57 | del report["ProcMaps"] 58 | del report["ProcStatus"] 59 | report.add_user_info() 60 | 61 | # add in data which was fed to us from the JVM process 62 | for key, value in data.items(): 63 | report[key] = value 64 | 65 | # Add an ExecutablePath pointing to the file where the main class resides 66 | if "MainClassUrl" in report: 67 | url = report["MainClassUrl"] 68 | 69 | url_parts = urllib.parse.urlparse(url) 70 | path = url_parts.path 71 | 72 | if url_parts.scheme == "jar": 73 | # path is then a URL to the jar file 74 | url_parts = urllib.parse.urlparse(path) 75 | path = url_parts.path.split("!/", 1)[0] 76 | 77 | if url_parts.scheme == "file": 78 | report["ExecutablePath"] = path 79 | else: 80 | # Program at some non-file URL crashed. Give up. 81 | return 1 82 | 83 | report["Title"] = make_title(report) 84 | 85 | try: 86 | with apport.fileutils.make_report_file(report) as report_file: 87 | report.write(report_file) 88 | except OSError as error: 89 | apport.fatal("Cannot create report: %s", str(error)) 90 | return 0 91 | 92 | 93 | if __name__ == "__main__": 94 | sys.exit(main()) 95 | -------------------------------------------------------------------------------- /kde/progress.ui: -------------------------------------------------------------------------------- 1 | 2 | ProgressDialog 3 | 4 | 5 | 6 | 0 7 | 0 8 | 422 9 | 191 10 | 11 | 12 | 13 | Dialog 14 | 15 | 16 | 17 | 9 18 | 19 | 20 | 6 21 | 22 | 23 | 24 | 25 | 26 | 1 27 | 0 28 | 0 29 | 0 30 | 31 | 32 | 33 | heading 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 5 42 | 1 43 | 0 44 | 0 45 | 46 | 47 | 48 | text 49 | 50 | 51 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | false 62 | 63 | 64 | false 65 | 66 | 67 | 68 | 69 | 70 | 71 | Qt::Horizontal 72 | 73 | 74 | QDialogButtonBox::Cancel 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | buttons 84 | accepted() 85 | ProgressDialog 86 | accept() 87 | 88 | 89 | 248 90 | 254 91 | 92 | 93 | 157 94 | 274 95 | 96 | 97 | 98 | 99 | buttons 100 | rejected() 101 | ProgressDialog 102 | reject() 103 | 104 | 105 | 316 106 | 260 107 | 108 | 109 | 286 110 | 274 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/unit/test_rethread.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the apport.REThread module.""" 2 | 3 | import sys 4 | import time 5 | import traceback 6 | import unittest 7 | 8 | import apport.REThread 9 | 10 | 11 | def idle(seconds: float) -> None: 12 | """Test thread to just wait a bit.""" 13 | time.sleep(seconds) 14 | 15 | 16 | def div(x: int, y: int) -> float: 17 | """Test thread to divide two numbers.""" 18 | return x / y 19 | 20 | 21 | class T(unittest.TestCase): 22 | """Unit tests for the apport.REThread module.""" 23 | 24 | def test_return_value(self) -> None: 25 | """Return value works properly.""" 26 | t = apport.REThread.REThread(target=div, args=(42, 2)) 27 | t.start() 28 | t.join() 29 | # exc_raise() should be a no-op on successful functions 30 | t.exc_raise() 31 | self.assertEqual(t.return_value(), 21) 32 | self.assertIsNone(t.exc_info()) 33 | 34 | def test_no_return_value(self) -> None: 35 | """apport.REThread.REThread works if run() does not return anything.""" 36 | t = apport.REThread.REThread(target=idle, args=(0.5,)) 37 | t.start() 38 | # thread must be joined first 39 | self.assertRaises(AssertionError, t.return_value) 40 | t.join() 41 | self.assertIsNone(t.return_value()) 42 | self.assertIsNone(t.exc_info()) 43 | 44 | def test_exception(self) -> None: 45 | """Exception in thread is caught and passed.""" 46 | t = apport.REThread.REThread(target=div, args=(1, 0)) 47 | t.start() 48 | t.join() 49 | # thread did not terminate normally, no return value 50 | self.assertRaises(AssertionError, t.return_value) 51 | self.assertIs(t.exc_info()[0], ZeroDivisionError) 52 | exc = traceback.format_exception( 53 | t.exc_info()[0], t.exc_info()[1], t.exc_info()[2] 54 | ) 55 | self.assertTrue( 56 | exc[-1].startswith("ZeroDivisionError"), 57 | f"not a ZeroDivisionError:{str(exc)}", 58 | ) 59 | self.assertIn("\n return x / y\n", exc[-2]) 60 | 61 | def test_exc_raise(self) -> None: 62 | """exc_raise() raises caught thread exception.""" 63 | t = apport.REThread.REThread(target=div, args=(1, 0)) 64 | t.start() 65 | t.join() 66 | # thread did not terminate normally, no return value 67 | self.assertRaises(AssertionError, t.return_value) 68 | raised = False 69 | try: 70 | t.exc_raise() 71 | except ZeroDivisionError: 72 | raised = True 73 | error = sys.exc_info() 74 | exc = traceback.format_exception(error[0], error[1], error[2]) 75 | self.assertIn("\n return x / y\n", exc[-2]) 76 | self.assertTrue(raised) 77 | 78 | def test_exc_raise_complex(self) -> None: 79 | """Exceptions that can't be simply created are reraised correctly. 80 | 81 | A unicode error takes several arguments on construction, so trying to 82 | recreate it by just passing an instance to the class, as the Python 3 83 | reraise expression did, will fail. See lp:1024836 for details. 84 | """ 85 | t = apport.REThread.REThread(target=str.encode, args=("\xff", "ascii")) 86 | t.start() 87 | t.join() 88 | self.assertRaises(UnicodeError, t.exc_raise) 89 | -------------------------------------------------------------------------------- /data/package_hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2007 - 2009 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about a package installation/upgrade failure.""" 13 | 14 | import argparse 15 | import contextlib 16 | import os 17 | import sys 18 | 19 | import apport.fileutils 20 | import apport.logging 21 | import apport.report 22 | from apport.packaging_impl import impl as packaging 23 | 24 | 25 | def mkattrname(path): 26 | """Convert a file path to a problem report attribute name.""" 27 | 28 | name = "" 29 | for directory in path.split(os.sep): 30 | if not directory: 31 | continue 32 | name += "".join( 33 | [c for c in directory[0].upper() + directory[1:] if c.isalnum()] 34 | ) 35 | return name 36 | 37 | 38 | def parse_args(): 39 | """Parse command line options and return arguments.""" 40 | parser = argparse.ArgumentParser() 41 | parser.add_argument( 42 | "-p", 43 | "--package", 44 | required=True, 45 | help="Specify the package name which failed to upgrade (mandatory)", 46 | ) 47 | parser.add_argument( 48 | "-l", 49 | "--log", 50 | action="append", 51 | dest="logs", 52 | help="Append given log file, or, if it is a directory," 53 | " all files in it (can be specified multiple times)", 54 | ) 55 | parser.add_argument( 56 | "-t", 57 | "--tags", 58 | help="Add the following tags to the bug report (comma separated)", 59 | ) 60 | args = parser.parse_args() 61 | if args.tags: 62 | args.tags = args.tags.split(",") 63 | return args 64 | 65 | 66 | # pylint: disable-next=missing-function-docstring 67 | def main(): 68 | # parse command line arguments 69 | options = parse_args() 70 | 71 | # create report 72 | report = apport.report.Report("Package") 73 | report.add_package(options.package) 74 | # get_source can fail on distribution upgrades where the package in question has 75 | # been removed from the newer release. See https://launchpad.net/bugs/2078695 76 | with contextlib.suppress(ValueError): 77 | report["SourcePackage"] = packaging.get_source(options.package) 78 | report["ErrorMessage"] = (sys.stdin, False) 79 | 80 | if options.tags: 81 | report.add_tags(options.tags) 82 | 83 | for line in options.logs or []: 84 | if os.path.isfile(line): 85 | report[mkattrname(line)] = (line,) 86 | elif os.path.isdir(line): 87 | for log_file in os.listdir(line): 88 | path = os.path.join(line, log_file) 89 | if os.path.isfile(path): 90 | report[mkattrname(path)] = (path,) 91 | 92 | # write report 93 | try: 94 | with apport.fileutils.make_report_file(report) as report_file: 95 | report.write(report_file) 96 | except OSError as error: 97 | apport.logging.fatal("Cannot create report: %s", str(error)) 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /tests/paths.py: -------------------------------------------------------------------------------- 1 | """Test helper functions around modifying paths.""" 2 | 3 | import os 4 | import pathlib 5 | from collections.abc import Mapping 6 | from typing import Any 7 | 8 | _SRCDIR = pathlib.Path(__file__).absolute().parent.parent 9 | _BINDIR = _SRCDIR / "bin" 10 | _CRASHDB_CONF = _SRCDIR / "etc" / "apport" / "crashdb.conf" 11 | _DATADIR = _SRCDIR / "data" 12 | 13 | 14 | def get_bin_directory() -> pathlib.Path: 15 | """Return absolute path for the scripts directory.""" 16 | if is_local_source_directory(): 17 | return _BINDIR 18 | return pathlib.Path("/usr/bin") 19 | 20 | 21 | def get_data_directory(local_path: str | None = None) -> pathlib.Path: 22 | """Return absolute path for apport's data directory. 23 | 24 | If the tests are executed in the local source code directory, 25 | return the absolute path to the local data directory or to the 26 | given local path (if specified). Otherwise return the path to the 27 | system installed data directory. The returned data directory can be 28 | overridden by setting the environment variable APPORT_DATA_DIR. 29 | """ 30 | if "APPORT_DATA_DIR" in os.environ: 31 | return pathlib.Path(os.environ["APPORT_DATA_DIR"]) 32 | if is_local_source_directory(): 33 | if local_path is None: 34 | return _DATADIR 35 | return _SRCDIR / local_path 36 | return pathlib.Path("/usr/share/apport") 37 | 38 | 39 | def get_test_data_directory() -> pathlib.Path: 40 | """Return absolute path for apport's tests/data directory.""" 41 | return pathlib.Path(__file__).absolute().parent / "data" 42 | 43 | 44 | def is_local_source_directory() -> bool: 45 | """Return True if the current working directory is the source directory. 46 | 47 | The local source directory is expected to have a tests directory 48 | and a setup.py file. 49 | """ 50 | return os.path.isdir("tests") and os.path.exists("setup.py") 51 | 52 | 53 | def local_test_environment() -> dict[str, str]: 54 | """Return needed environment variables when running tests locally.""" 55 | if not is_local_source_directory(): 56 | return {} 57 | return { 58 | "APPORT_CRASHDB_CONF": str(_CRASHDB_CONF), 59 | "APPORT_DATA_DIR": str(_DATADIR), 60 | "PATH": f"{_BINDIR}:{os.environ.get('PATH', os.defpath)}", 61 | "PYTHONPATH": str(_SRCDIR), 62 | } 63 | 64 | 65 | def patch_data_dir(report: Any) -> dict[str, str] | None: 66 | """Patch APPORT_DATA_DIR in apport.report for local tests.""" 67 | if not is_local_source_directory(): 68 | return None 69 | 70 | # pylint: disable=protected-access 71 | orig = { 72 | "data_dir": report._data_dir, 73 | "general_hook_dir": report.GENERAL_HOOK_DIR, 74 | "package_hook_dir": report.PACKAGE_HOOK_DIR, 75 | } 76 | 77 | data_dir = get_data_directory() 78 | report._data_dir = data_dir 79 | report.GENERAL_HOOK_DIR = f"{data_dir}/general-hooks/" 80 | report.PACKAGE_HOOK_DIR = f"{data_dir}/package-hooks/" 81 | 82 | return orig 83 | 84 | 85 | def restore_data_dir(report: Any, orig: Mapping[str, str] | None) -> None: 86 | """Restore APPORT_DATA_DIR in apport.report from local tests. 87 | 88 | The parameter orig is the result from the patch_data_dir() call. 89 | """ 90 | if not orig: 91 | return 92 | 93 | # pylint: disable=protected-access 94 | report._data_dir = orig["data_dir"] 95 | report.GENERAL_HOOK_DIR = orig["general_hook_dir"] 96 | report.PACKAGE_HOOK_DIR = orig["package_hook_dir"] 97 | -------------------------------------------------------------------------------- /data/kernel_crashdump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2007 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about a kernel oops.""" 13 | 14 | import glob 15 | import os 16 | import re 17 | 18 | import apport.fileutils 19 | import apport.logging 20 | import apport.report 21 | from apport.packaging_impl import impl as packaging 22 | 23 | 24 | # TODO: Split into smaller functions/methods 25 | # pylint: disable-next=too-complex 26 | def main() -> None: 27 | """Collect information about a kernel oops.""" 28 | pr = apport.report.Report("KernelCrash") 29 | package = packaging.get_kernel_package() 30 | pr.add_package(package) 31 | 32 | pr.add_os_info() 33 | 34 | vmcore_path = os.path.join(apport.fileutils.report_dir, "vmcore") 35 | # only accept plain files here, not symlinks; otherwise we might recursively 36 | # include the report, or similar DoS attacks 37 | if os.path.exists(f"{vmcore_path}.log"): 38 | try: 39 | log_fd = os.open(f"{vmcore_path}.log", os.O_RDONLY | os.O_NOFOLLOW) 40 | pr["VmCoreLog"] = (os.fdopen(log_fd, "rb"),) 41 | os.unlink(f"{vmcore_path}.log") 42 | except OSError as error: 43 | apport.logging.fatal("Cannot open vmcore log: %s", str(error)) 44 | 45 | if os.path.exists(vmcore_path): 46 | try: 47 | core_fd = os.open(vmcore_path, os.O_RDONLY | os.O_NOFOLLOW) 48 | pr["VmCore"] = (os.fdopen(core_fd, "rb"),) 49 | with apport.fileutils.make_report_file(pr) as f: 50 | pr.write(f) 51 | except OSError as error: 52 | apport.logging.fatal("Cannot create report: %s", str(error)) 53 | 54 | try: 55 | os.unlink(vmcore_path) 56 | except OSError: 57 | pass # huh, already gone? 58 | else: 59 | # check for kdump-tools generated dmesg in timestamped dir 60 | for dmesg_file in glob.glob( 61 | os.path.join(apport.fileutils.report_dir, "*", "dmesg.*") 62 | ): 63 | timedir = os.path.dirname(dmesg_file) 64 | timestamp = os.path.basename(timedir) 65 | if re.match("^[0-9]{12}$", timestamp): 66 | # we require the containing dir to be owned by root, to avoid users 67 | # creating a symlink to someplace else and disclosing data; we just 68 | # compare against euid here so that we can test this as non-root 69 | if os.lstat(timedir).st_uid != os.geteuid(): 70 | apport.logging.fatal("%s has unsafe permissions, ignoring", timedir) 71 | report_name = f"{package}-{timestamp}.crash" 72 | try: 73 | crash_report = os.path.join( 74 | apport.fileutils.report_dir, report_name 75 | ) 76 | dmesg_fd = os.open(dmesg_file, os.O_RDONLY | os.O_NOFOLLOW) 77 | pr["VmCoreDmesg"] = (os.fdopen(dmesg_fd, "rb"),) 78 | with open(crash_report, "xb") as f: 79 | pr.write(f) 80 | except OSError as error: 81 | apport.logging.fatal("Cannot create report: %s", str(error)) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /java/com/ubuntu/apport/ApportUncaughtExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ubuntu.apport; 2 | 3 | /* 4 | * Apport handler for uncaught Java exceptions 5 | * 6 | * Copyright: 2010 Canonical Ltd. 7 | * Author: Matt Zimmerman 8 | * 9 | * This program is free software; you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by the 11 | * Free Software Foundation; either version 2 of the License, or (at your 12 | * option) any later version. See http://www.gnu.org/copyleft/gpl.html for 13 | * the full text of the license. 14 | */ 15 | 16 | import java.io.*; 17 | import java.util.HashMap; 18 | 19 | public class ApportUncaughtExceptionHandler 20 | implements java.lang.Thread.UncaughtExceptionHandler { 21 | 22 | /* Write out an apport problem report with details of the 23 | * exception, then print it in the usual canonical format */ 24 | public void uncaughtException(Thread t, Throwable e) { 25 | //System.out.println("uncaughtException"); 26 | if (e instanceof ThreadDeath) 27 | return; 28 | 29 | HashMap problemReport = getProblemReport(t, e); 30 | //System.out.println("got problem report"); 31 | 32 | try { 33 | String handler_path = System.getenv("APPORT_JAVA_EXCEPTION_HANDLER"); 34 | if (handler_path == null) 35 | handler_path = "/usr/share/apport/java_uncaught_exception"; 36 | Process p = new ProcessBuilder(handler_path).start(); 37 | //System.out.println("started process"); 38 | 39 | OutputStream os = p.getOutputStream(); 40 | writeProblemReport(os, problemReport); 41 | //System.out.println("wrote problem report"); 42 | 43 | os.close(); 44 | 45 | try { 46 | p.waitFor(); 47 | } catch (InterruptedException ignore) { 48 | // ignored 49 | } 50 | 51 | } catch (java.io.IOException ioe) { 52 | System.out.println("could not call java_uncaught_exception"); 53 | } 54 | 55 | System.err.print("Exception in thread \"" 56 | + t.getName() + "\" "); 57 | e.printStackTrace(System.err); 58 | } 59 | 60 | public HashMap getProblemReport(Thread t, Throwable e) { 61 | HashMap problemReport = new HashMap(); 62 | 63 | StringWriter sw = new StringWriter(); 64 | PrintWriter pw = new PrintWriter(sw); 65 | e.printStackTrace(pw); 66 | problemReport.put("StackTrace", sw.toString()); 67 | 68 | problemReport.put("MainClassUrl", mainClassUrl(e)); 69 | 70 | return problemReport; 71 | } 72 | 73 | public void writeProblemReport(OutputStream os, HashMap pr) 74 | throws IOException { 75 | 76 | StringWriter sw = new StringWriter(); 77 | for(Object o : pr.keySet()) { 78 | String key = (String)o; 79 | String value = (String)pr.get(o); 80 | sw.write(key); 81 | sw.write("\0"); 82 | sw.write(value); 83 | sw.write("\0"); 84 | } 85 | os.write(sw.toString().getBytes()); 86 | } 87 | 88 | public static String mainClassUrl(Throwable e) { 89 | StackTraceElement[] stacktrace = e.getStackTrace(); 90 | String className = stacktrace[stacktrace.length-1].getClassName(); 91 | 92 | if (!className.startsWith("/")) { 93 | className = "/" + className; 94 | } 95 | className = className.replace('.', '/'); 96 | className = className + ".class"; 97 | 98 | java.net.URL classUrl = 99 | new ApportUncaughtExceptionHandler().getClass().getResource(className); 100 | 101 | return classUrl.toString(); 102 | } 103 | 104 | /* Install this handler as the default uncaught exception handler */ 105 | public static void install() { 106 | Thread.setDefaultUncaughtExceptionHandler(new ApportUncaughtExceptionHandler()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /kde/userpass.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 398 10 | 159 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 10 20 | 10 21 | 381 22 | 139 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 0 31 | 0 32 | 33 | 34 | 35 | text 36 | 37 | 38 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 39 | 40 | 41 | true 42 | 43 | 44 | 1 45 | 46 | 47 | 48 | 49 | 50 | 51 | QFormLayout::AllNonFixedFieldsGrow 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | user 60 | 61 | 62 | 63 | 64 | 65 | 66 | pass 67 | 68 | 69 | 70 | 71 | 72 | 73 | QLineEdit::Password 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | Qt::Horizontal 83 | 84 | 85 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | buttonBox 96 | accepted() 97 | Dialog 98 | accept() 99 | 100 | 101 | 248 102 | 254 103 | 104 | 105 | 157 106 | 274 107 | 108 | 109 | 110 | 111 | buttonBox 112 | rejected() 113 | Dialog 114 | reject() 115 | 116 | 117 | 316 118 | 260 119 | 120 | 121 | 286 122 | 274 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /tests/unit/test_hooks_image.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for data/general-hooks/image.py.""" 11 | 12 | import unittest 13 | import unittest.mock 14 | from unittest.mock import MagicMock 15 | 16 | import problem_report 17 | from tests.helper import import_module_from_file 18 | from tests.paths import get_data_directory 19 | 20 | image = import_module_from_file(get_data_directory() / "general-hooks" / "image.py") 21 | 22 | 23 | class TestGeneralHookImage(unittest.TestCase): 24 | """Unit tests for data/general-hooks/image.py.""" 25 | 26 | @unittest.mock.patch("os.path.isfile", MagicMock(return_value=True)) 27 | def test_add_info(self) -> None: 28 | """Test add_info() for Ubuntu 22.04 server cloud image.""" 29 | report = problem_report.ProblemReport() 30 | open_mock = unittest.mock.mock_open( 31 | read_data="build_name: server\nserial: 20221214\n" 32 | ) 33 | with unittest.mock.patch("builtins.open", open_mock): 34 | image.add_info(report, None) 35 | self.assertEqual( 36 | set(report.keys()), 37 | {"CloudBuildName", "CloudSerial", "Date", "ProblemType", "Tags"}, 38 | ) 39 | self.assertEqual(report["CloudBuildName"], "server") 40 | self.assertEqual(report["CloudSerial"], "20221214") 41 | self.assertEqual(report["Tags"], "cloud-image") 42 | open_mock.assert_called_with("/etc/cloud/build.info", encoding="utf-8") 43 | 44 | @unittest.mock.patch("os.path.isfile", MagicMock(return_value=True)) 45 | def test_add_info_empty_build_info(self) -> None: 46 | """Test add_info() with empty /etc/cloud/build.info.""" 47 | report = problem_report.ProblemReport() 48 | open_mock = unittest.mock.mock_open(read_data="\n") 49 | with unittest.mock.patch("builtins.open", open_mock): 50 | image.add_info(report, None) 51 | self.assertEqual(set(report.keys()), {"Date", "ProblemType", "Tags"}) 52 | self.assertEqual(report["Tags"], "cloud-image") 53 | open_mock.assert_called_with("/etc/cloud/build.info", encoding="utf-8") 54 | 55 | @unittest.mock.patch("os.path.isfile", MagicMock(return_value=True)) 56 | def test_add_info_unknown_field(self) -> None: 57 | """Test add_info() with unknown field in /etc/cloud/build.info.""" 58 | report = problem_report.ProblemReport() 59 | open_mock = unittest.mock.mock_open( 60 | read_data="unknown: value\nserial: 20221214\n" 61 | ) 62 | with unittest.mock.patch("builtins.open", open_mock): 63 | image.add_info(report, None) 64 | self.assertEqual( 65 | set(report.keys()), {"CloudSerial", "Date", "ProblemType", "Tags"} 66 | ) 67 | self.assertEqual(report["CloudSerial"], "20221214") 68 | self.assertEqual(report["Tags"], "cloud-image") 69 | open_mock.assert_called_with("/etc/cloud/build.info", encoding="utf-8") 70 | 71 | @unittest.mock.patch("os.path.isfile") 72 | def test_no_cloud_build_info(self, isfile_mock: MagicMock) -> None: 73 | """Test add_info() with no /etc/cloud/build.info.""" 74 | isfile_mock.return_value = False 75 | report = problem_report.ProblemReport() 76 | image.add_info(report, None) 77 | self.assertEqual(set(report.keys()), {"Date", "ProblemType"}) 78 | isfile_mock.assert_called_once_with("/etc/cloud/build.info") 79 | -------------------------------------------------------------------------------- /man/apport-bug.1: -------------------------------------------------------------------------------- 1 | .TH apport\-bug 1 "September 08, 2009" "Martin Pitt" 2 | 3 | .SH NAME 4 | 5 | apport\-bug, apport\-collect \- file a bug report using Apport, or update an existing report 6 | 7 | .SH SYNOPSIS 8 | 9 | .B apport\-bug 10 | 11 | .B apport\-bug 12 | .I symptom \fR|\fI pid \fR|\fI package \fR|\fI program path \fR|\fI .apport/.crash file 13 | 14 | .B apport\-collect 15 | .I report-number 16 | 17 | .SH DESCRIPTION 18 | 19 | .B apport\-bug 20 | reports problems to your distribution's bug tracking system, 21 | using Apport to collect a lot of local information about your system to help 22 | the developers to fix the problem and avoid unnecessary question/answer 23 | turnarounds. 24 | 25 | You should always start with running 26 | .B apport\-bug 27 | without arguments, which will present a list of known symptoms. This will 28 | generate the most useful bug reports. 29 | 30 | If there is no matching symptom, you need to determine the affected program or 31 | package yourself. You can provide a package name or program name to 32 | .B apport\-bug\fR, 33 | e. g.: 34 | 35 | .RS 4 36 | .nf 37 | apport\-bug firefox 38 | apport\-bug /usr/bin/unzip 39 | .fi 40 | .RE 41 | 42 | In order to add more information to the bug report that could 43 | help the developers to fix the problem, you can also specify a process 44 | ID instead: 45 | 46 | .RS 4 47 | .nf 48 | $ pidof gnome-terminal 49 | 5139 50 | $ apport\-bug 5139 51 | .fi 52 | .RE 53 | 54 | As a special case, to report a bug against the Linux kernel, you do not need to 55 | use the full package name (such as linux-image-2.6.28-4-generic); you can just use 56 | 57 | .RS 4 58 | .nf 59 | apport\-bug linux 60 | .fi 61 | .RE 62 | 63 | to report a bug against the currently running kernel. 64 | 65 | Finally, you can use this program to report a previously stored crash or bug report: 66 | 67 | .RS 4 68 | .nf 69 | apport\-bug /var/crash/_bin_bash.1000.crash 70 | apport\-bug /tmp/apport.firefox.332G9t.apport 71 | .fi 72 | .RE 73 | 74 | Bug reports can be written to a file by using the 75 | .B \-\-save 76 | option or by using 77 | .B apport\-cli\fR. 78 | 79 | .B apport\-bug 80 | detects whether KDE or Gnome is running and calls 81 | .B apport\-gtk 82 | or 83 | .B apport\-kde 84 | accordingly. If neither is available, or the session does not run 85 | under X11, it calls 86 | .B apport\-cli 87 | for a command-line client. 88 | 89 | .SH UPDATING EXISTING REPORTS 90 | 91 | .B apport\-collect 92 | collects the same information as apport\-bug, but adds it to an already 93 | reported problem you have submitted. This is useful if the report was 94 | not originally filed through Apport, and the developers ask you to attach 95 | information from your system. 96 | 97 | .SH OPTIONS 98 | Please see the 99 | .BR apport\-cli(1) 100 | manpage for possible options. 101 | 102 | .SH ENVIRONMENT 103 | 104 | .TP 105 | .B APPORT_IGNORE_OBSOLETE_PACKAGES 106 | Apport refuses to create bug reports if the package or any dependency is not 107 | current. If this environment variable is set, this check is waived. Experts who 108 | will thoroughly check the situation before filing a bug report can define this 109 | in their 110 | .B ~/.bashrc 111 | or temporarily on the command line when calling 112 | .B apport\-bug\fR. 113 | 114 | .SH FILES 115 | apport crash files are written in to 116 | .B /var/crash 117 | by default, named uniquely per binary name and user id. They are not deleted 118 | after being sent to the bug tracker (but from cron when they get older than 7 119 | days). You can extract the core file (if any) and other information using 120 | .B apport-unpack. 121 | 122 | .SH "SEE ALSO" 123 | .BR apport\-cli (1), 124 | .BR apport\-unpack (1) 125 | 126 | .SH AUTHOR 127 | .B apport 128 | and the accompanying tools are developed by Martin Pitt 129 | . 130 | -------------------------------------------------------------------------------- /bin/dupdb-admin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2007 - 2012 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """CLI for maintaining the duplicate database""" 13 | 14 | # pylint: disable=invalid-name 15 | # pylint: enable=invalid-name 16 | 17 | import argparse 18 | import os.path 19 | import sys 20 | 21 | import apport.crashdb_impl.memory 22 | import apport.logging 23 | 24 | 25 | def command_dump(crashdb, _): 26 | """Print out all entries.""" 27 | 28 | for sig, (crash_id, version, lastchange) in crashdb.duplicate_db_dump(True).items(): 29 | sys.stdout.write(f"{crash_id:7d}: {sig} ") 30 | if version == "": 31 | sys.stdout.write("[fixed] ") 32 | elif version: 33 | sys.stdout.write(f"[fixed in: {version}] ") 34 | else: 35 | sys.stdout.write("[open] ") 36 | print(f"last change: {str(lastchange)}") 37 | 38 | 39 | def command_changeid(crashdb, args): 40 | """Change the master ID of a crash.""" 41 | crashdb.duplicate_db_change_master_id(args.old_id, args.new_id) 42 | 43 | 44 | def command_removeid(crashdb, args): 45 | """Remove a crash.""" 46 | crashdb.duplicate_db_remove(args.id) 47 | 48 | 49 | def command_publish(crashdb, args): 50 | """Publish crash database to a directory.""" 51 | crashdb.duplicate_db_publish(args.path) 52 | 53 | 54 | def parse_args(): 55 | """Parse command line options and return arguments.""" 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument( 58 | "-f", 59 | "--database-file", 60 | dest="db_file", 61 | metavar="PATH", 62 | default="apport_duplicates.db", 63 | help="Location of the database file (default: %(default)s)", 64 | ) 65 | subparsers = parser.add_subparsers(metavar="command", required=True) 66 | 67 | parser_dump = subparsers.add_parser( 68 | "dump", help="Print a list of all database entries" 69 | ) 70 | parser_dump.set_defaults(command=command_dump) 71 | 72 | parser_changeid = subparsers.add_parser( 73 | "changeid", help="Change the associated crash ID for a particular crash" 74 | ) 75 | parser_changeid.set_defaults(command=command_changeid) 76 | parser_changeid.add_argument("old_id") 77 | parser_changeid.add_argument("new_id") 78 | 79 | parser_removeid = subparsers.add_parser( 80 | "removeid", help="Remove the associated crash ID for a particular crash" 81 | ) 82 | parser_removeid.set_defaults(command=command_removeid) 83 | parser_removeid.add_argument("id") 84 | 85 | parser_publish = subparsers.add_parser( 86 | "publish", 87 | help="Export the duplicate database into a set of text files" 88 | " in the given directory which is suitable for WWW publishing", 89 | ) 90 | parser_publish.set_defaults(command=command_publish) 91 | parser_publish.add_argument("path") 92 | 93 | return parser.parse_args() 94 | 95 | 96 | # pylint: disable-next=missing-function-docstring 97 | def main(): 98 | args = parse_args() 99 | 100 | if not os.path.exists(args.db_file): 101 | apport.logging.fatal("file does not exist: %s", args.db_file) 102 | 103 | # pure DB operations don't need a real backend, and thus no crashdb.conf 104 | crashdb = apport.crashdb_impl.memory.CrashDatabase(None, {}) 105 | crashdb.init_duplicate_db(args.db_file) 106 | args.command(crashdb, args) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /kde/error.ui: -------------------------------------------------------------------------------- 1 | 2 | ErrorDialog 3 | 4 | 5 | 6 | 0 7 | 0 8 | 270 9 | 191 10 | 11 | 12 | 13 | Dialog 14 | 15 | 16 | 17 | 9 18 | 19 | 20 | 6 21 | 22 | 23 | 24 | 25 | checker 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 1 35 | 0 36 | 0 37 | 38 | 39 | 40 | 41 | 42 | 43 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 1 52 | 0 53 | 0 54 | 0 55 | 56 | 57 | 58 | heading 59 | 60 | 61 | 6 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 5 70 | 1 71 | 0 72 | 0 73 | 74 | 75 | 76 | text 77 | 78 | 79 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 80 | 81 | 82 | true 83 | 84 | 85 | 6 86 | 87 | 88 | 89 | 90 | 91 | 92 | Qt::Horizontal 93 | 94 | 95 | QDialogButtonBox::Close 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | buttons 105 | accepted() 106 | ErrorDialog 107 | accept() 108 | 109 | 110 | 248 111 | 254 112 | 113 | 114 | 157 115 | 274 116 | 117 | 118 | 119 | 120 | buttons 121 | rejected() 122 | ErrorDialog 123 | reject() 124 | 125 | 126 | 316 127 | 260 128 | 129 | 130 | 286 131 | 274 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /setuptools_apport/java.py: -------------------------------------------------------------------------------- 1 | """Setuptools extension to build the Java subdirectory.""" 2 | 3 | import functools 4 | import glob 5 | import logging 6 | import os 7 | import pathlib 8 | import subprocess 9 | import typing 10 | 11 | from setuptools import Command, Distribution 12 | 13 | 14 | # pylint: disable-next=invalid-name 15 | class build_java(Command): 16 | """Compile Java components of Apport""" 17 | 18 | description = __doc__ 19 | user_options = [("minimum_java_release=", "r", "Specify minimum Java release.")] 20 | 21 | def __init__(self, dist: Distribution, **kwargs: dict[str, typing.Any]) -> None: 22 | Command.__init__(self, dist, **kwargs) 23 | self.initialize_options() 24 | 25 | def initialize_options(self) -> None: 26 | """Set or (reset) all options/attributes/caches to their default values""" 27 | self.minimum_java_release = "7" 28 | 29 | def finalize_options(self) -> None: 30 | """Set final values for all options/attributes""" 31 | 32 | def run(self) -> None: 33 | """Build the Java .class and .jar files.""" 34 | oldwd = os.getcwd() 35 | os.chdir("java") 36 | javac = [ 37 | "javac", 38 | "-source", 39 | self.minimum_java_release, 40 | "-target", 41 | self.minimum_java_release, 42 | ] 43 | subprocess.check_call(javac + glob.glob("com/ubuntu/apport/*.java")) 44 | subprocess.check_call( 45 | ["jar", "cvf", "apport.jar"] + glob.glob("com/ubuntu/apport/*.class") 46 | ) 47 | subprocess.check_call(javac + ["testsuite/crash.java"]) 48 | subprocess.check_call( 49 | ["jar", "cvf", "crash.jar", "crash.class"], cwd="testsuite" 50 | ) 51 | 52 | os.chdir(oldwd) 53 | 54 | 55 | # pylint: disable-next=invalid-name 56 | class install_java(Command): 57 | """Install Java components of Apport.""" 58 | 59 | def __init__(self, dist: Distribution, **kwargs: dict[str, typing.Any]) -> None: 60 | super().__init__(dist, **kwargs) 61 | self.initialize_options() 62 | 63 | def initialize_options(self) -> None: 64 | """Set default values for all the options that this command supports.""" 65 | self.install_dir: str | None = None 66 | 67 | def finalize_options(self) -> None: 68 | """Set final values for all the options that this command supports.""" 69 | self.set_undefined_options("install_data", ("install_dir", "install_dir")) 70 | 71 | def _install_data_files(self, dst_path: str, src_files: list[pathlib.Path]) -> None: 72 | assert self.install_dir 73 | for src_file in src_files: 74 | target = pathlib.Path(self.install_dir) / dst_path / src_file.name 75 | self.mkpath(str(target.parent)) 76 | self.copy_file(str(src_file), str(target), preserve_mode=False) 77 | 78 | def run(self) -> None: 79 | """Install the Java .jar files.""" 80 | self._install_data_files("share/java", [pathlib.Path("java/apport.jar")]) 81 | 82 | 83 | @functools.cache 84 | def has_java(unused_command: Command) -> bool: 85 | """Check if the Java compiler is available.""" 86 | try: 87 | subprocess.run(["javac", "-version"], capture_output=True, check=True) 88 | except (OSError, subprocess.CalledProcessError): 89 | logging.getLogger(__name__).warning( 90 | "Java support: Java not available, not building Java crash handler" 91 | ) 92 | return False 93 | return True 94 | 95 | 96 | def register_java_sub_commands( 97 | build: type[Command], install: type[Command] 98 | ) -> dict[str, type[Command]]: 99 | """Plug the Java extension into setuptools. 100 | 101 | Return a dictionary with the added command classes which needs to 102 | be passed to the `setup` call as `cmdclass` parameter. 103 | """ 104 | build.sub_commands.append(("build_java_subdir", has_java)) 105 | install.sub_commands.append(("install_java", has_java)) 106 | return {"build_java_subdir": build_java, "install_java": install_java} 107 | -------------------------------------------------------------------------------- /data/apportcheckresume: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (C) 2009 Canonical Ltd. 4 | # Author: Andy Whitcroft 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Detect and report suspend/hibernate/resume failures. 13 | 14 | If a suspend/hibernate is marked as still in progress during a normal 15 | system boot we know that that operation has failed. Use that to 16 | generate an apport bug report. 17 | """ 18 | 19 | import datetime 20 | import os 21 | import sys 22 | from gettext import gettext as _ 23 | 24 | import apport.report 25 | from apport.hookutils import attach_file_if_exists 26 | from apport.packaging_impl import impl as packaging 27 | 28 | 29 | # pylint: disable-next=missing-function-docstring 30 | def main(argv: list[str] | None = None) -> int: 31 | if argv is None: 32 | argv = sys.argv 33 | 34 | try: 35 | if not packaging.enabled(): 36 | return -1 37 | 38 | report = apport.report.Report(problem_type="KernelOops") 39 | 40 | libdir = "/var/lib/pm-utils" 41 | flagfile = f"{libdir}/status" 42 | stresslog = f"{libdir}/stress.log" 43 | hanglog = f"{libdir}/resume-hang.log" 44 | 45 | report.add_os_info() 46 | report.add_proc_info() 47 | report.add_user_info() 48 | report.add_package(apport.packaging.get_kernel_package()) 49 | 50 | # grab the contents of the suspend/resume flag file 51 | attach_file_if_exists(report, flagfile, "Failure") 52 | 53 | # grab the contents of the suspend/hibernate log file 54 | attach_file_if_exists(report, "/var/log/pm-suspend.log", "SleepLog") 55 | 56 | # grab the contents of the suspend/resume stress test log if present. 57 | attach_file_if_exists(report, stresslog, "StressLog") 58 | 59 | # Ensure we are appropriately tagged. 60 | if "Failure" in report: 61 | report.add_tags(["resume ", report["Failure"]]) 62 | 63 | # Record the failure mode. 64 | report["Failure"] += "/resume" 65 | 66 | # If we had a late hang pull in the resume-hang logfile. Also 67 | # add an additional tag so we can pick these out. 68 | if os.path.exists(hanglog): 69 | attach_file_if_exists(report, hanglog, "ResumeHangLog") 70 | report.add_tags(["resume-late-hang"]) 71 | 72 | # Generate a sensible report message. 73 | if report.get("Failure") == "suspend/resume": 74 | report["Annotation"] = _( 75 | "This occurred during a previous suspend," 76 | " and prevented the system from resuming properly." 77 | ) 78 | else: 79 | report["Annotation"] = _( 80 | "This occurred during a previous hibernation," 81 | " and prevented the system from resuming properly." 82 | ) 83 | 84 | # If we had a late hang make sure the dialog is clear that they may 85 | # not have noticed. Also update the bug title so we notice. 86 | if os.path.exists(hanglog): 87 | report["Annotation"] += " " + _( 88 | "The resume processing hung very near the end" 89 | " and will have appeared to have completed normally." 90 | ) 91 | report["Failure"] = "late resume" 92 | 93 | if report.check_ignored(): 94 | return 0 95 | 96 | nowtime = datetime.datetime.now() 97 | pr_filename = f"/var/crash/susres.{str(nowtime).replace(' ', '_')}.crash" 98 | with os.fdopen( 99 | os.open(pr_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640), "wb" 100 | ) as report_file: 101 | report.write(report_file) 102 | return 0 103 | except Exception: 104 | print("apportcheckresume failed") 105 | raise 106 | 107 | 108 | if __name__ == "__main__": 109 | sys.exit(main()) 110 | -------------------------------------------------------------------------------- /bin/apport-unpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2006 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Extract the fields of a problem report into separate files into a new or 13 | empty directory.""" 14 | 15 | # pylint: disable=invalid-name 16 | # pylint: enable=invalid-name 17 | 18 | import argparse 19 | import contextlib 20 | import gettext 21 | import gzip 22 | import io 23 | import os 24 | import sys 25 | from collections.abc import Iterator 26 | from gettext import gettext as _ 27 | from typing import BinaryIO 28 | 29 | import problem_report 30 | from apport.logging import fatal 31 | 32 | 33 | def parse_args() -> argparse.Namespace: 34 | """Parse command line arguments.""" 35 | parser = argparse.ArgumentParser(usage=_("%(prog)s ")) 36 | parser.add_argument("report", help=_("Report file to unpack")) 37 | parser.add_argument("target_directory", help=_("directory to unpack report to")) 38 | return parser.parse_args() 39 | 40 | 41 | @contextlib.contextmanager 42 | def open_report(report_filename: str) -> Iterator[(BinaryIO | gzip.GzipFile)]: 43 | """Open a problem report from given filename.""" 44 | if report_filename == "-": 45 | # sys.stdin has type io.TextIOWrapper, not the claimed io.TextIO. 46 | # See https://github.com/python/typeshed/issues/10093 47 | assert isinstance(sys.stdin, io.TextIOWrapper) 48 | yield sys.stdin.detach() 49 | elif report_filename.endswith(".gz"): 50 | with gzip.open(report_filename, "rb") as report_file: 51 | yield report_file 52 | else: 53 | with open(report_filename, "rb") as report_file: 54 | yield report_file 55 | 56 | 57 | def unpack_report_to_directory( 58 | report: problem_report.ProblemReport, target_directory: str 59 | ) -> list[str]: 60 | """Write each report entry into a separate file. 61 | 62 | Return a list of keys that were not loaded. 63 | """ 64 | missing_keys = [] 65 | for key, value in report.items(): 66 | if value is None: 67 | missing_keys.append(key) 68 | continue 69 | with open(os.path.join(target_directory, key), "wb") as key_file: 70 | if isinstance(value, str): 71 | key_file.write(value.encode("UTF-8")) 72 | else: 73 | key_file.write(value) 74 | return missing_keys 75 | 76 | 77 | # pylint: disable-next=missing-function-docstring 78 | def main() -> None: 79 | gettext.textdomain("apport") 80 | args = parse_args() 81 | 82 | # ensure that the directory does not yet exist or is empty 83 | try: 84 | if os.path.isdir(args.target_directory): 85 | if os.listdir(args.target_directory): 86 | fatal(_("Destination directory exists and is not empty.")) 87 | else: 88 | os.mkdir(args.target_directory) 89 | except OSError as error: 90 | fatal("%s", str(error)) 91 | 92 | report = problem_report.ProblemReport() 93 | try: 94 | with open_report(args.report) as report_file: 95 | # In case of passing the report to stdin, 96 | # the report needs to be loaded in one go. 97 | # The current implementation loads the whole report into memory. 98 | report.load(report_file, binary=args.report == "-") 99 | except (OSError, problem_report.MalformedProblemReport) as error: 100 | fatal("%s", str(error)) 101 | bin_keys = unpack_report_to_directory(report, args.target_directory) 102 | if bin_keys: 103 | try: 104 | with open_report(args.report) as report_file: 105 | report.extract_keys(report_file, bin_keys, args.target_directory) 106 | except (OSError, problem_report.MalformedProblemReport) as error: 107 | fatal("%s", str(error)) 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | -------------------------------------------------------------------------------- /tests/integration/test_ui_cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Command line Apport user interface tests.""" 11 | 12 | import gzip 13 | import io 14 | import os 15 | import pathlib 16 | import tempfile 17 | import unittest 18 | import unittest.mock 19 | from gettext import gettext as _ 20 | 21 | import apport.report 22 | from problem_report import CompressedFile 23 | from tests.helper import import_module_from_file, skip_if_command_is_missing 24 | from tests.paths import is_local_source_directory, local_test_environment 25 | 26 | if is_local_source_directory(): 27 | APPORT_CLI_PATH = "bin/apport-cli" 28 | else: 29 | APPORT_CLI_PATH = "/usr/bin/apport-cli" 30 | apport_cli = import_module_from_file(pathlib.Path(APPORT_CLI_PATH)) 31 | 32 | 33 | class TestApportCli(unittest.TestCase): 34 | # pylint: disable=missing-function-docstring 35 | """Test apport-cli.""" 36 | 37 | orig_environ: dict[str, str] 38 | 39 | @classmethod 40 | def setUpClass(cls) -> None: 41 | cls.orig_environ = os.environ.copy() 42 | os.environ |= local_test_environment() 43 | 44 | @classmethod 45 | def tearDownClass(cls) -> None: 46 | os.environ.clear() 47 | os.environ.update(cls.orig_environ) 48 | 49 | def setUp(self) -> None: 50 | self.app = apport_cli.CLIUserInterface([APPORT_CLI_PATH]) 51 | self.app.report = apport.report.Report() 52 | self.app.report.add_os_info() 53 | self.app.report["ExecutablePath"] = "/bin/bash" 54 | self.app.report["Signal"] = "11" 55 | self.app.report["CoreDump"] = b"\x01\x02" 56 | self.app.report["LongString"] = f"l{'o' * 1_042_000}ng" 57 | 58 | @skip_if_command_is_missing("/usr/bin/sensible-pager") 59 | def test_ui_update_view(self) -> None: 60 | with tempfile.NamedTemporaryFile(prefix="apport_") as temp: 61 | with gzip.open(temp.name, "wb") as f_out: 62 | f_out.write(b"some uncompressed data") 63 | self.app.report["CompressedFile"] = CompressedFile(temp.name) 64 | 65 | read_fd, write_fd = os.pipe() 66 | with os.fdopen(write_fd, "w", buffering=1) as stdout: 67 | self.app.ui_update_view(stdout=stdout) 68 | with os.fdopen(read_fd, "r") as pipe: 69 | report = pipe.read() 70 | self.assertRegex( 71 | report, 72 | "^== ExecutablePath =================================\n" 73 | "/bin/bash\n\n" 74 | "== ProblemType =================================\n" 75 | "Crash\n\n" 76 | "== Architecture =================================\n" 77 | "[^\n]+\n\n" 78 | "== CompressedFile =================================\n" 79 | "[^\n]+\n\n" 80 | "== CoreDump =================================\n" 81 | "[^\n]+\n\n" 82 | "== Date =================================\n" 83 | "[^\n]+\n\n" 84 | "== DistroRelease =================================\n" 85 | "[^\n]+\n\n" 86 | "== LongString =================================\n" 87 | "[^\n]+1042003[^\n]+\n\n" 88 | "== Signal =================================\n" 89 | "11\n\n" 90 | "== Uname =================================\n" 91 | "[^\n]+\n\n$", 92 | ) 93 | 94 | @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) 95 | def test_save_report_in_temp_directory(self, stdout_mock: io.StringIO) -> None: 96 | self.app.report["Package"] = "bash" 97 | with unittest.mock.patch.object(apport_cli.CLIDialog, "run") as run_mock: 98 | run_mock.return_value = 4 99 | self.app.ui_present_report_details() 100 | self.assertIn(_("Problem report file:"), stdout_mock.getvalue()) 101 | -------------------------------------------------------------------------------- /tests/integration/test_unkillable_shutdown.py: -------------------------------------------------------------------------------- 1 | """Test unkillable_shutdown""" 2 | 3 | # Copyright (C) 2022 Canonical Ltd. 4 | # Author: Benjamin Drung 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | import contextlib 13 | import multiprocessing 14 | import os 15 | import shutil 16 | import signal 17 | import subprocess 18 | import tempfile 19 | import unittest 20 | from collections.abc import Generator 21 | 22 | from tests.helper import get_gnu_coreutils_cmd 23 | from tests.paths import get_data_directory, local_test_environment 24 | 25 | 26 | class TestUnkillableShutdown(unittest.TestCase): 27 | """Test unkillable_shutdown""" 28 | 29 | maxDiff = None 30 | TEST_EXECUTABLE = get_gnu_coreutils_cmd("sleep") 31 | TEST_ARGS = ["86400"] 32 | 33 | def setUp(self) -> None: 34 | self.data_dir = get_data_directory() 35 | self.env = os.environ | local_test_environment() 36 | 37 | self.report_dir = tempfile.mkdtemp() 38 | self.env["APPORT_REPORT_DIR"] = self.report_dir 39 | 40 | def tearDown(self) -> None: 41 | shutil.rmtree(self.report_dir) 42 | 43 | def _call(self, omit: list | None = None) -> None: 44 | cmd = [str(self.data_dir / "unkillable_shutdown")] 45 | if omit: 46 | cmd += [arg for pid in omit for arg in ("-o", str(pid))] 47 | process = subprocess.run( 48 | cmd, 49 | check=False, 50 | env=self.env, 51 | stdout=subprocess.PIPE, 52 | stderr=subprocess.PIPE, 53 | text=True, 54 | ) 55 | self.assertEqual(process.returncode, 0, process.stderr) 56 | self.assertEqual(process.stdout, "") 57 | 58 | @staticmethod 59 | def _get_all_pids() -> list[int]: 60 | return [int(pid) for pid in os.listdir("/proc") if pid.isdigit()] 61 | 62 | @contextlib.contextmanager 63 | def _launch_process_with_different_session_id( 64 | self, 65 | ) -> Generator[multiprocessing.Process]: 66 | """Launch test executable with different session ID. 67 | 68 | getsid() will return a different ID than the current process. 69 | """ 70 | 71 | def _run_test_executable(queue: multiprocessing.Queue) -> None: 72 | os.setsid() 73 | cmd = [self.TEST_EXECUTABLE] + self.TEST_ARGS 74 | with subprocess.Popen(cmd) as test_process: 75 | queue.put(test_process.pid) 76 | 77 | queue: multiprocessing.Queue = multiprocessing.Queue() 78 | runner = multiprocessing.Process(target=_run_test_executable, args=(queue,)) 79 | runner.start() 80 | try: 81 | pid = queue.get(timeout=60) 82 | try: 83 | yield runner 84 | finally: 85 | os.kill(pid, signal.SIGHUP) 86 | runner.join(60) 87 | finally: 88 | runner.kill() 89 | 90 | def test_omit_all_processes(self) -> None: 91 | """unkillable_shutdown will write no reports.""" 92 | self._call(omit=self._get_all_pids()) 93 | self.assertEqual(os.listdir(self.report_dir), []) 94 | 95 | def test_omit_all_processes_except_one(self) -> None: 96 | """unkillable_shutdown will write exactly one report.""" 97 | existing_pids = self._get_all_pids() 98 | with self._launch_process_with_different_session_id() as runner: 99 | self._call(omit=existing_pids + [runner.pid]) 100 | self.assertEqual( 101 | os.listdir(self.report_dir), 102 | [f"{self.TEST_EXECUTABLE.replace('/', '_')}.{os.geteuid()}.crash"], 103 | ) 104 | 105 | def test_write_reports(self) -> None: 106 | """unkillable_shutdown will write reports.""" 107 | # Ensure that at least one process is honoured by unkillable_shutdown. 108 | with self._launch_process_with_different_session_id(): 109 | self._call() 110 | reports = os.listdir(self.report_dir) 111 | self.assertGreater(len(reports), 0, reports) 112 | -------------------------------------------------------------------------------- /man/apport-valgrind.1: -------------------------------------------------------------------------------- 1 | .TH apport\-valgrind 1 "February 12, 2013" "Kyle Nitzsche" 2 | 3 | .SH NAME 4 | 5 | apport\-valgrind \- valgrind wrapper that first downloads debug symbols 6 | 7 | .SH SYNOPSIS 8 | 9 | .B apport\-valgrind 10 | [ 11 | .I OPTIONS 12 | ] 13 | .I EXECUTABLE 14 | 15 | .SH DESCRIPTION 16 | 17 | .B apport\-valgrind 18 | is a valgrind wrapper that automatically downloads related available debug 19 | symbols and provides them to valgrind's memcheck tool, which is executed. The 20 | output is a valgrind log file ("valgrind.log") that contains stack traces (with 21 | as many symbols resolved as available) and that shows memory leaks. 22 | 23 | By default, a temporary cache directory is created to hold the latest debug 24 | symbol packages. These are unpacked into a temporary sandbox directory. The 25 | path to the sandbox directory is provided to valgrind as an additional location 26 | for symbol files. 27 | 28 | You may create and use persistent cache and sandbox directories to save time 29 | across multiple executions, thus preventing the need to recreate them each 30 | time. Downloading all packages into the the cache directory each time is 31 | particularly time consuming. 32 | 33 | It is recommended to update your system before execution. This ensures your 34 | runtime environment is consistent with the latest downloaded symbol packages 35 | and therefore results in a more complete stack trace from valgrind. 36 | 37 | .I EXECUTABLE 38 | is the program to run under valgrind. Always terminate the 39 | .I EXECUTABLE 40 | in its usual way. Exit it from the GUI if there is one. If not, use the most 41 | appropriate method. 42 | 43 | Different techniques are used to determine which packages should be unpacked 44 | into the sandbox depending on whether 45 | .I EXECUTABLE 46 | is packaged (installed by a debian package) or not (for example something 47 | created for development or testing). A packaged 48 | .I EXECUTABLE 49 | has debian dependencies that are used. For an unpackaged 50 | .I EXECUTABLE\fR, 51 | the shared object files are found with ldd and the packages for these are 52 | used. 53 | 54 | .SH OPTIONS 55 | 56 | .TP 57 | .B \-C \fICDIR\fR, \-\-cache=\fICDIR\fR 58 | Reuse a previously created cache dir (\fICDIR\fR) or, if it does not exist, 59 | create it. 60 | 61 | .TP 62 | .B \-\-sandbox\-dir=\fISDIR\fR 63 | Reuse a previously created sandbox dir (\fISDIR\fR) or, if it does not exist, 64 | create it 65 | 66 | .TP 67 | .B \-\-no\-sandbox 68 | Do not create or reuse a sandbox directory for additional debug symbols but 69 | rely only on installed debug symbols. This speeds execution time but may result 70 | in an incomplete and less useful valgrind log if you do not have all 71 | appropriate debug symbol packages installed. 72 | 73 | .TP 74 | .B \-p, \-\-extra-package 75 | Specify an extra package (or packages) to unpack in the sandbox. Useful to add 76 | additional debug symbol packages that result in more complete valgrind logs. 77 | 78 | .TP 79 | .B \-v, \-\-verbose 80 | Report download/install progress when installing packages in sandbox mode. 81 | 82 | .TP 83 | .B \-l \fILOGFILE\fR, \-\-log=\fILOGFILE\fR 84 | Specify the file name for the generated valgrind log file. Default is: 85 | valgrind.log 86 | 87 | .TP 88 | .B \-h, \-\-help 89 | Display short help that documents all options. 90 | 91 | .SH EXAMPLES 92 | 93 | Create and use temporary cache and sandbox directories: 94 | .RS 4 95 | apport\-valgrind 96 | .I EXECUTABLE 97 | .RE 98 | 99 | Reuse or create cache dir: 100 | .RS 4 101 | apport\-valgrind \-C 102 | .I CDIR 103 | .I EXECUTABLE 104 | .RE 105 | 106 | Reuse or create sandbox dir: 107 | .RS 4 108 | apport\-valgrind \-\-sandbox\-dir 109 | .I SDIR 110 | .I EXECUTABLE 111 | .RE 112 | 113 | .SH KNOWN ISSUES 114 | 115 | If you abnormally terminate the executable you are running under valgrind, 116 | temporary directories may not be deleted and processes may not all terminate. 117 | For example, if the executable does not normally terminate on ctrl+c, pressing 118 | ctrl+c in the terminal may cause apport-valgrind and valgrind to terminate, but 119 | may not terminate the executable and may not delete the temporary directories. 120 | 121 | .SH AUTHORS 122 | 123 | Developed by Martin Pitt , Alex Chiang 124 | and Kyle Nitzsche 125 | 126 | -------------------------------------------------------------------------------- /tests/run-linters: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run code linters on Apport. 3 | # 4 | # Test against the source tree when run in the source tree root. 5 | 6 | # Copyright (C) 2007 - 2012 Canonical Ltd. 7 | # Author: Martin Pitt 8 | # 9 | # This program is free software; you can redistribute it and/or modify it 10 | # under the terms of the GNU General Public License as published by the 11 | # Free Software Foundation; either version 2 of the License, or (at your 12 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 13 | # the full text of the license. 14 | 15 | set -eu 16 | 17 | PYTHON_FILES="apport apport_python_hook.py data problem_report.py setup.py setuptools_apport tests" 18 | test ! -d debian || PYTHON_FILES="$PYTHON_FILES debian" 19 | PYTHON_SCRIPTS_WITHOUT_APPORT=$(find bin data gtk kde -type f -executable ! -name apport-bug ! -name apport-collect ! -name is-enabled ! -name root_info_wrapper ! -name apport) 20 | PYTHON_SCRIPTS="$PYTHON_SCRIPTS_WITHOUT_APPORT data/apport" 21 | 22 | check_hardcoded_names() { 23 | # assert that there are no hardcoded "Ubuntu" names 24 | out=$(grep -rw Ubuntu apport/*.py gtk/apport-gtk* kde/* bin/* | grep -v Debian | grep -v X-Ubuntu-Gettext-Domain | grep -v '#.*Ubuntu') || : 25 | if [ -n "$out" ]; then 26 | echo "Found hardcoded 'Ubuntu' names, use DistroRelease: field or lsb_release instead:\n\n$out" >&2 27 | exit 1 28 | fi 29 | } 30 | 31 | run_black() { 32 | if ! type black >/dev/null 2>&1; then 33 | echo "Skipping black tests, black is not installed" 34 | return 35 | fi 36 | black_version=$(black --version | head -n1 | cut -d' ' -f 2) 37 | if test "${black_version%%.*}" -lt 25; then 38 | echo "Skipping black tests, black $black_version is installed but version >= 25 is needed" 39 | return 40 | fi 41 | echo "Running black..." 42 | black -C --check --diff ${PYTHON_FILES} ${PYTHON_SCRIPTS} 43 | } 44 | 45 | run_isort() { 46 | if ! type isort >/dev/null 2>&1; then 47 | echo "Skipping isort tests, isort is not installed" 48 | return 49 | fi 50 | echo "Running isort..." 51 | isort --check-only --diff ${PYTHON_FILES} ${PYTHON_SCRIPTS} 52 | } 53 | 54 | run_mypy() { 55 | if ! type mypy >/dev/null 2>&1; then 56 | echo "Skipping mypy tests, mypy is not installed" 57 | return 58 | fi 59 | echo "Running mypy..." 60 | mypy ${PYTHON_FILES} data/apport 61 | mypy --scripts-are-modules ${PYTHON_SCRIPTS_WITHOUT_APPORT} 62 | } 63 | 64 | run_pycodestyle() { 65 | if ! type pycodestyle >/dev/null 2>&1; then 66 | echo "Skipping pycodestyle tests, pycodestyle is not installed" 67 | return 68 | fi 69 | echo "Running pycodestyle..." 70 | # E101 causes false positive on tests/unit/test_hookutils.py, 71 | # see https://github.com/PyCQA/pycodestyle/issues/376 72 | # E704 complains about "def f(): ..." 73 | # . catches all *.py modules; we explicitly need to specify the programs 74 | pycodestyle --max-line-length=88 -r --ignore=E101,E203,E704,W503 ${PYTHON_FILES} ${PYTHON_SCRIPTS} 75 | } 76 | 77 | run_pydocstyle() { 78 | if ! type pydocstyle >/dev/null 2>&1; then 79 | echo "Skipping pydocstyle tests, pydocstyle is not installed" 80 | return 81 | fi 82 | pydocstyle_version=$(pydocstyle --version) 83 | pydocstyle_major_version=$(echo "$pydocstyle_version" | cut -d. -f1) 84 | if test "$pydocstyle_major_version" -lt 6; then 85 | echo "Skipping pydocstyle tests, pydocstyle $pydocstyle_version is too old" 86 | return 87 | fi 88 | echo "Running pydocstyle..." 89 | pydocstyle ${PYTHON_FILES} ${PYTHON_SCRIPTS} 90 | } 91 | 92 | run_pylint() { 93 | if ! type pylint >/dev/null 2>&1; then 94 | echo "Skipping pylint tests, pylint is not installed" 95 | return 96 | fi 97 | echo "Running pylint..." 98 | pylint -j 0 "$@" ${PYTHON_FILES} ${PYTHON_SCRIPTS} 99 | } 100 | 101 | if test "${1-}" = "--errors-only"; then 102 | # Run only linters that can detect real errors (ignore formatting) 103 | run_mypy 104 | run_pylint --errors-only 105 | else 106 | run_black 107 | run_isort 108 | run_pycodestyle 109 | run_pydocstyle 110 | run_mypy 111 | run_pylint 112 | check_hardcoded_names 113 | fi 114 | -------------------------------------------------------------------------------- /data/unkillable_shutdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (c) 2010 Canonical Ltd. 4 | # Author: Martin Pitt 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | """Collect information about processes which are still running after sending 13 | SIGTERM to them (which happens during computer shutdown in 14 | /etc/init.d/sendsigs in Debian/Ubuntu)""" 15 | 16 | import argparse 17 | import errno 18 | import os 19 | from collections.abc import Container, Iterable 20 | 21 | import apport.fileutils 22 | import apport.hookutils 23 | import apport.logging 24 | import apport.report 25 | 26 | 27 | def parse_argv(): 28 | """Parse command line and return arguments.""" 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument( 31 | "-o", 32 | "--omit", 33 | metavar="PID", 34 | action="append", 35 | default=[], 36 | dest="omit_pids", 37 | help="Ignore a particular process ID (can be specified multiple times)", 38 | ) 39 | return parser.parse_args() 40 | 41 | 42 | def orphaned_processes(omit_pids: Container[str]) -> Iterable[int]: 43 | """Yield an iterator of running process IDs. 44 | 45 | This excludes PIDs which do not have a valid /proc/pid/exe symlink (e. g. 46 | kernel processes), the PID of our own process, and everything that is 47 | contained in the omit_pids argument. 48 | """ 49 | my_pid = os.getpid() 50 | my_sid = os.getsid(0) 51 | for process in os.listdir("/proc"): 52 | try: 53 | pid = int(process) 54 | except ValueError: 55 | continue 56 | if pid == 1 or pid == my_pid or process in omit_pids: 57 | apport.logging.warning("ignoring: %s", process) 58 | continue 59 | 60 | try: 61 | sid = os.getsid(pid) 62 | except OSError: 63 | # os.getsid() can fail with "No such process" if the process died 64 | # in the meantime 65 | continue 66 | 67 | if sid == my_sid: 68 | apport.logging.warning("ignoring same sid: %s", process) 69 | continue 70 | 71 | try: 72 | os.readlink(os.path.join("/proc", process, "exe")) 73 | except OSError as error: 74 | if error.errno == errno.ENOENT: 75 | # kernel thread or similar, silently ignore 76 | continue 77 | apport.logging.warning( 78 | "Could not read information about pid %s: %s", process, str(error) 79 | ) 80 | continue 81 | 82 | yield pid 83 | 84 | 85 | def do_report(pid: int, omit_pids: Iterable[str]) -> None: 86 | """Create a report for a particular PID.""" 87 | 88 | report = apport.report.Report("Bug") 89 | try: 90 | report.add_proc_info(pid) 91 | except (ValueError, AssertionError): 92 | # happens if ExecutablePath doesn't exist (any more?), ignore 93 | return 94 | 95 | report["Tags"] = "shutdown-hang" 96 | report["Title"] = "does not terminate at computer shutdown" 97 | if "ExecutablePath" in report: 98 | report["Title"] = ( 99 | f"{os.path.basename(report['ExecutablePath'])} {report['Title']}" 100 | ) 101 | report["Processes"] = apport.hookutils.command_output(["ps", "aux"]) 102 | report["InitctlList"] = apport.hookutils.command_output(["initctl", "list"]) 103 | if omit_pids: 104 | report["OmitPids"] = " ".join(omit_pids) 105 | 106 | try: 107 | with apport.fileutils.make_report_file(report) as report_file: 108 | report.write(report_file) 109 | except FileExistsError as error: 110 | apport.logging.warning( 111 | "Cannot create report: %s already exists", error.filename 112 | ) 113 | except OSError as error: 114 | apport.logging.fatal("Cannot create report: %s", str(error)) 115 | 116 | 117 | # 118 | # main 119 | # 120 | 121 | args = parse_argv() 122 | 123 | for p in orphaned_processes(args.omit_pids): 124 | do_report(p, args.omit_pids) 125 | -------------------------------------------------------------------------------- /tests/unit/test_ui.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the apport.ui module.""" 2 | 3 | import os 4 | import tempfile 5 | import textwrap 6 | import unittest 7 | import unittest.mock 8 | from gettext import gettext as _ 9 | from unittest.mock import MagicMock 10 | 11 | import apport.ui 12 | 13 | 14 | class TestUI(unittest.TestCase): 15 | """Unit tests for apport.ui.""" 16 | 17 | crashdb_conf: str 18 | 19 | @classmethod 20 | def setUpClass(cls) -> None: 21 | # pylint: disable-next=consider-using-with 22 | crashdb_conf_file = tempfile.NamedTemporaryFile("w+") 23 | cls.addClassCleanup(crashdb_conf_file.close) 24 | crashdb_conf_file.write( 25 | textwrap.dedent( 26 | """\ 27 | default = 'testsuite' 28 | databases = { 29 | 'testsuite': { 30 | 'impl': 'memory', 31 | 'bug_pattern_url': None, 32 | }, 33 | 'debug': { 34 | 'impl': 'memory', 35 | 'distro': 'debug', 36 | }, 37 | } 38 | """ 39 | ) 40 | ) 41 | crashdb_conf_file.flush() 42 | cls.crashdb_conf = crashdb_conf_file.name 43 | 44 | def setUp(self) -> None: 45 | self.orig_environ = os.environ.copy() 46 | os.environ["APPORT_CRASHDB_CONF"] = self.crashdb_conf 47 | self.ui = apport.ui.UserInterface([]) 48 | 49 | # Mock environment for run_as_real_user() to not being called via sudo/pkexec 50 | os.environ.pop("SUDO_UID", None) 51 | os.environ.pop("PKEXEC_UID", None) 52 | 53 | def tearDown(self) -> None: 54 | os.environ.clear() 55 | os.environ.update(self.orig_environ) 56 | 57 | @unittest.mock.patch("subprocess.run") 58 | @unittest.mock.patch("webbrowser.open") 59 | def test_open_url(self, open_mock: MagicMock, run_mock: MagicMock) -> None: 60 | """Test successful UserInterface.open_url() without pkexec/sudo.""" 61 | self.ui.open_url("https://example.com") 62 | 63 | run_mock.assert_called_once_with( 64 | ["xdg-open", "https://example.com"], check=False 65 | ) 66 | open_mock.assert_not_called() 67 | 68 | @unittest.mock.patch("subprocess.run") 69 | @unittest.mock.patch("webbrowser.open") 70 | def test_open_url_webbrowser_fallback( 71 | self, open_mock: MagicMock, run_mock: MagicMock 72 | ) -> None: 73 | """Test UserInterface.open_url() to fall back to webbrowser.open().""" 74 | run_mock.side_effect = FileNotFoundError 75 | open_mock.return_value = True 76 | 77 | self.ui.open_url("https://example.net") 78 | 79 | run_mock.assert_called_once_with( 80 | ["xdg-open", "https://example.net"], check=False 81 | ) 82 | open_mock.assert_called_once_with("https://example.net", new=1, autoraise=True) 83 | 84 | @unittest.mock.patch("subprocess.run") 85 | @unittest.mock.patch("webbrowser.open") 86 | @unittest.mock.patch("apport.ui.UserInterface.ui_error_message") 87 | def test_open_url_webbrowser_fails( 88 | self, error_message_mock: MagicMock, open_mock: MagicMock, run_mock: MagicMock 89 | ) -> None: 90 | """Test UserInterface.open_url() with webbrowser.open() returning False.""" 91 | run_mock.side_effect = FileNotFoundError 92 | open_mock.return_value = False 93 | 94 | self.ui.open_url("https://example.org") 95 | 96 | run_mock.assert_called_once_with( 97 | ["xdg-open", "https://example.org"], check=False 98 | ) 99 | open_mock.assert_called_once_with("https://example.org", new=1, autoraise=True) 100 | error_message_mock.assert_called_once_with( 101 | _("Unable to start web browser"), 102 | _("Unable to start web browser to open %s.") % ("https://example.org"), 103 | ) 104 | 105 | @unittest.mock.patch("apport.ui.UserInterface.ui_error_message") 106 | def test_hanging_without_pid(self, error_message_mock: MagicMock) -> None: 107 | """Test calling apport --hanging without providing a process ID.""" 108 | ui = apport.ui.UserInterface(["ui-test", "--hanging"]) 109 | self.assertFalse(ui.run_argv()) 110 | error_message_mock.assert_called_once_with( 111 | _("No PID specified"), 112 | _("You need to specify a PID. See --help for more information."), 113 | ) 114 | -------------------------------------------------------------------------------- /tests/unit/test_sandboxutils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Canonical Ltd. 2 | # Author: Benjamin Drung 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for apport.sandboxutils.""" 11 | 12 | import os 13 | import tempfile 14 | import unittest 15 | import unittest.mock 16 | from unittest.mock import MagicMock 17 | 18 | from apport.package_info import PackageInfo 19 | from apport.report import Report 20 | from apport.sandboxutils import _move_base_files_first, make_sandbox 21 | 22 | 23 | class TestSandboxutils(unittest.TestCase): 24 | """Unit tests for apport.sandboxutils.""" 25 | 26 | @staticmethod 27 | def _get_sample_report() -> Report: 28 | report = Report() 29 | report["Architecture"] = "amd64" 30 | report["DistroRelease"] = "Ubuntu 22.04" 31 | report["ExecutablePath"] = "/bin/bash" 32 | report["Signal"] = "11" 33 | return report 34 | 35 | @unittest.mock.patch("apport.sandboxutils.packaging", spec=PackageInfo) 36 | def test_make_sandbox(self, packaging_mock: MagicMock) -> None: 37 | """make_sandbox() for a sample report.""" 38 | packaging_mock.install_packages.return_value = "obsolete\n" 39 | report = self._get_sample_report() 40 | sandbox, cache, outdated_msg = make_sandbox(report, "system") 41 | self.assertTrue(os.path.exists(sandbox), f"'{sandbox}' does not exist") 42 | self.assertTrue(os.path.exists(cache), f"'{cache}' does not exist") 43 | self.assertEqual(outdated_msg, "obsolete\nobsolete\n") 44 | self.assertEqual(packaging_mock.install_packages.call_count, 2) 45 | 46 | @unittest.mock.patch("apport.sandboxutils.packaging", spec=PackageInfo) 47 | def test_make_sandbox_install_packages_failure( 48 | self, packaging_mock: MagicMock 49 | ) -> None: 50 | """make_sandbox() where packaging.install_packages fails.""" 51 | packaging_mock.install_packages.side_effect = SystemError("100% fail") 52 | with self.assertRaises(SystemExit): 53 | make_sandbox(self._get_sample_report(), "system") 54 | packaging_mock.install_packages.assert_called_once() 55 | 56 | @unittest.mock.patch("apport.sandboxutils.packaging", spec=PackageInfo) 57 | def test_make_sandbox_with_sandbox_dir(self, packaging_mock: MagicMock) -> None: 58 | """make_sandbox() with sandbox_dir set.""" 59 | packaging_mock.install_packages.return_value = "obsolete\n" 60 | with tempfile.TemporaryDirectory(dir="/var/tmp") as tmpdir: 61 | config_dir = os.path.join(tmpdir, "config") 62 | cache_dir = os.path.join(tmpdir, "cache") 63 | sandbox_dir = os.path.join(tmpdir, "sandbox") 64 | sandbox, cache, outdated_msg = make_sandbox( 65 | self._get_sample_report(), 66 | config_dir, 67 | cache_dir=cache_dir, 68 | sandbox_dir=sandbox_dir, 69 | ) 70 | self.assertEqual(sandbox, sandbox_dir) 71 | self.assertEqual(cache, cache_dir) 72 | self.assertEqual(outdated_msg, "obsolete\nobsolete\n") 73 | self.assertEqual(packaging_mock.install_packages.call_count, 2) 74 | 75 | def test_move_base_files_first_existing(self) -> None: 76 | """_move_base_files_first() with base-files in list.""" 77 | pkgs: list[tuple[str, None | str]] = [ 78 | ("chaos-marmosets", "0.1.2-2"), 79 | ("base-files", "13ubuntu9"), 80 | ("libc6", "2.39-0ubuntu8.2"), 81 | ] 82 | _move_base_files_first(pkgs) 83 | self.assertEqual( 84 | pkgs, 85 | [ 86 | ("base-files", "13ubuntu9"), 87 | ("chaos-marmosets", "0.1.2-2"), 88 | ("libc6", "2.39-0ubuntu8.2"), 89 | ], 90 | ) 91 | 92 | def test_move_base_files_first_missing(self) -> None: 93 | """_move_base_files_first() without base-files in list.""" 94 | pkgs: list[tuple[str, None | str]] = [ 95 | ("chaos-marmosets", "0.1.2-2"), 96 | ("libc6", "2.39-0ubuntu8.2"), 97 | ] 98 | _move_base_files_first(pkgs) 99 | self.assertEqual( 100 | pkgs, [("chaos-marmosets", "0.1.2-2"), ("libc6", "2.39-0ubuntu8.2")] 101 | ) 102 | -------------------------------------------------------------------------------- /tests/integration/test_java_crashes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Canonical Ltd. 2 | # Author: Martin Pitt 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Integration tests for the Java crash collection support.""" 11 | 12 | import os 13 | import shutil 14 | import subprocess 15 | import tempfile 16 | import unittest 17 | 18 | import apport.fileutils 19 | import apport.report 20 | from tests.helper import skip_if_command_is_missing 21 | from tests.paths import get_data_directory, local_test_environment 22 | 23 | 24 | @skip_if_command_is_missing("java") 25 | class TestJavaCrashes(unittest.TestCase): 26 | """Integration tests for the Java crash collection support.""" 27 | 28 | def setUp(self) -> None: 29 | self.env = os.environ | local_test_environment() 30 | datadir = get_data_directory() 31 | self.orig_report_dir = apport.fileutils.report_dir 32 | apport.fileutils.report_dir = tempfile.mkdtemp() 33 | self.env["APPORT_REPORT_DIR"] = apport.fileutils.report_dir 34 | self.env["APPORT_JAVA_EXCEPTION_HANDLER"] = str( 35 | datadir / "java_uncaught_exception" 36 | ) 37 | java_dir = get_data_directory("java") 38 | self.apport_jar_path = java_dir / "apport.jar" 39 | self.crash_jar_path = java_dir / "testsuite" / "crash.jar" 40 | if not self.apport_jar_path.exists(): 41 | self.skipTest(f"{self.apport_jar_path} missing") 42 | 43 | def tearDown(self) -> None: 44 | shutil.rmtree(apport.fileutils.report_dir) 45 | apport.fileutils.report_dir = self.orig_report_dir 46 | 47 | def test_crash_class(self) -> None: 48 | """Crash in a .class file.""" 49 | crash_class = self.crash_jar_path.with_suffix(".class") 50 | self.assertTrue(crash_class.exists(), f"{crash_class} missing") 51 | java = subprocess.run( 52 | [ 53 | "java", 54 | "-classpath", 55 | f"{self.apport_jar_path}:{self.crash_jar_path.parent}", 56 | "crash", 57 | ], 58 | check=False, 59 | env=self.env, 60 | stdout=subprocess.PIPE, 61 | stderr=subprocess.PIPE, 62 | ) 63 | self.assertNotEqual(java.returncode, 0, "crash must exit with nonzero code") 64 | self.assertIn("Can't catch this", java.stderr.decode()) 65 | 66 | self._check_crash_report(str(crash_class)) 67 | 68 | def test_crash_jar(self) -> None: 69 | """Crash in a .jar file.""" 70 | self.assertTrue(self.crash_jar_path.exists(), f"{self.crash_jar_path} missing") 71 | java = subprocess.run( 72 | [ 73 | "java", 74 | "-classpath", 75 | f"{self.apport_jar_path}:{self.crash_jar_path}", 76 | "crash", 77 | ], 78 | check=False, 79 | env=self.env, 80 | stdout=subprocess.PIPE, 81 | stderr=subprocess.PIPE, 82 | ) 83 | self.assertNotEqual(java.returncode, 0, "crash must exit with nonzero code") 84 | self.assertIn("Can't catch this", java.stderr.decode()) 85 | 86 | self._check_crash_report(f"{self.crash_jar_path}!/crash.class") 87 | 88 | def _check_crash_report(self, main_file: str) -> None: 89 | """Check that we have one crash report, and verify its contents.""" 90 | reports = apport.fileutils.get_new_reports() 91 | self.assertEqual(len(reports), 1, "did not create a crash report") 92 | report = apport.report.Report() 93 | with open(reports[0], "rb") as report_file: 94 | report.load(report_file) 95 | self.assertEqual(report["ProblemType"], "Crash") 96 | self.assertTrue(report["ProcCmdline"].startswith("java -classpath"), report) 97 | self.assertTrue( 98 | report["StackTrace"].startswith( 99 | "java.lang.RuntimeException: Can't catch this" 100 | ) 101 | ) 102 | if ".jar!" in main_file: 103 | self.assertEqual(report["MainClassUrl"], f"jar:file:{main_file}") 104 | else: 105 | self.assertEqual(report["MainClassUrl"], f"file:{main_file}") 106 | self.assertIn("DistroRelease", report) 107 | self.assertIn("ProcCwd", report) 108 | -------------------------------------------------------------------------------- /tests/unit/test_crashdb_launchpad.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Canonical Ltd. 2 | # Author: Simon Chopin 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Unit tests for apport.crash_impl.launchpad""" 11 | 12 | import io 13 | import re 14 | import unittest.mock 15 | 16 | from apport.crashdb_impl.launchpad import CrashDatabase, upload_blob 17 | from apport.report import Report 18 | 19 | 20 | def python_crash() -> Report: 21 | """Generate a report that looks like a Python crash""" 22 | report = Report("Crash") 23 | report["Package"] = "python-goo 3epsilon1" 24 | report["SourcePackage"] = "pygoo" 25 | report["PackageArchitecture"] = "all" 26 | report["DistroRelease"] = "Ubuntu 24.04" 27 | report["ExecutablePath"] = "/usr/bin/pygoo" 28 | report[ 29 | "Traceback" 30 | ] = """Traceback (most recent call last): 31 | File "test.py", line 7, in 32 | print(_f(5)) 33 | File "test.py", line 5, in _f 34 | return g_foo00(x+1) 35 | File "test.py", line 2, in g_foo00 36 | return x/0 37 | ZeroDivisionError: integer division or modulo by zero""" 38 | return report 39 | 40 | 41 | def native_crash() -> Report: 42 | """Generate a report that looks like a native binary crash""" 43 | report = Report("Crash") 44 | report["Signal"] = "6" 45 | report["SignalName"] = "SIGABRT" 46 | report["Package"] = "bash" 47 | report["SourcePackage"] = "bash" 48 | report["DistroRelease"] = "Ubuntu 24.04" 49 | report["PackageArchitecture"] = "i386" 50 | report["Architecture"] = "amd64" 51 | report["ExecutablePath"] = "/bin/bash" 52 | report["CoreDump"] = "/var/lib/apport/coredump/core.bash" 53 | report["AssertionMessage"] = "foo.c:42 main: i > 0" 54 | return report 55 | 56 | 57 | def test_python_crash_headers() -> None: 58 | # pylint: disable=protected-access 59 | """Test _generate_upload_headers in case of a Python crash""" 60 | crashdb = CrashDatabase(None, {"distro": "ubuntu"}) 61 | report = python_crash() 62 | headers = crashdb._generate_upload_headers(report) 63 | 64 | assert "need-duplicate-check" in headers["Tags"].split(" ") 65 | assert headers.get("Private") == "yes" 66 | 67 | 68 | def test_native_crash_headers() -> None: 69 | # pylint: disable=protected-access 70 | """Test _generate_upload_headers in case of a native crash""" 71 | crashdb = CrashDatabase(None, {"distro": "ubuntu"}) 72 | report = native_crash() 73 | headers = crashdb._generate_upload_headers(report) 74 | 75 | assert "i386" in headers["Tags"].split(" ") 76 | assert "need-i386-retrace" in headers["Tags"].split(" ") 77 | assert headers.get("Private") == "yes" 78 | 79 | 80 | def test_private_bug_headers() -> None: 81 | # pylint: disable=protected-access 82 | """Test _generate_upload_headers for a bug in a package for which 83 | the hook explicitly says it should be private""" 84 | crashdb = CrashDatabase(None, {"distro": "ubuntu"}) 85 | report = Report("Bug") 86 | report["Package"] = "apport" 87 | report["SourcePackage"] = "apport" 88 | report["PackageArchitecture"] = "all" 89 | report["Architecture"] = "amd64" 90 | report["DistroRelease"] = "Ubuntu 24.04" 91 | report["LaunchpadPrivate"] = "yes" 92 | headers = crashdb._generate_upload_headers(report) 93 | 94 | assert headers.get("Private") == "yes" 95 | assert not re.search(r"need-[a-z0-9]+-retrace", headers.get("Tags", "")) 96 | 97 | 98 | @unittest.mock.patch("urllib.request.build_opener") 99 | def test_upload_blob_conform_to_lp(builder_mock: unittest.mock.MagicMock) -> None: 100 | """Test that upload_blob does NOT encodes form data with CRLF as line separators, 101 | despite HTTP 1.1 spec""" 102 | builder_mock.return_value.open.return_value.info.return_value = { 103 | "X-Launchpad-Blob-Token": 42 104 | } 105 | 106 | data = io.BytesIO(b"foobarbaz") 107 | result = upload_blob(data, hostname="invalid.host") 108 | 109 | builder_mock.assert_called_once() 110 | builder_mock.return_value.open.assert_called_once() 111 | req = builder_mock.return_value.open.call_args[0][0] 112 | assert b"foobarbaz" in req.data 113 | # Check that we have LF-separated headers (because LP expects LF 114 | # line separators, see LP: #2097632) 115 | assert re.match(rb"^([-A-Za-z]+: [^\r\n]+\n)+\n", req.data) 116 | assert result == 42 117 | -------------------------------------------------------------------------------- /tests/unit/test_helper.py: -------------------------------------------------------------------------------- 1 | """Test test helper functions. Test inception for the win!""" 2 | 3 | # pylint: disable=missing-class-docstring,missing-function-docstring 4 | 5 | import unittest 6 | from unittest.mock import MagicMock, mock_open, patch 7 | 8 | import psutil 9 | 10 | from tests.helper import ( 11 | get_init_system, 12 | wait_for_process_to_appear, 13 | wait_for_sleeping_state, 14 | wrap_object, 15 | ) 16 | 17 | 18 | class Multiply: # pylint: disable=too-few-public-methods 19 | """Test class for wrap_object test cases.""" 20 | 21 | def __init__(self, multiplier: int) -> None: 22 | self.multiplier = multiplier 23 | 24 | def multiply(self, factor: int) -> int: 25 | return factor * self.multiplier 26 | 27 | 28 | class TestTestHelper(unittest.TestCase): 29 | def test_get_init_systemd(self) -> None: 30 | open_mock = mock_open(read_data="systemd\n") 31 | with patch("builtins.open", open_mock): 32 | self.assertEqual(get_init_system(), "systemd") 33 | open_mock.assert_called_once_with("/proc/1/comm", encoding="utf-8") 34 | 35 | def test_wrap_object_with_statement(self) -> None: 36 | with wrap_object(Multiply, "__init__") as mock: 37 | multiply = Multiply(7) 38 | self.assertEqual(multiply.multiply(6), 42) 39 | mock.assert_called_once_with(7) 40 | 41 | @patch("time.sleep") 42 | @patch("psutil.Process", spec=psutil.Process) 43 | def test_wait_for_sleeping_state( 44 | self, process_mock: MagicMock, sleep_mock: MagicMock 45 | ) -> None: 46 | """Test wait_for_sleeping_state() helper method.""" 47 | process_mock.return_value.status.side_effect = [ 48 | "not-sleeping", 49 | "also-not-sleeping", 50 | "sleeping", 51 | ] 52 | 53 | wait_for_sleeping_state(1234567890) 54 | 55 | sleep_mock.assert_called_with(0.1) 56 | self.assertEqual(sleep_mock.call_count, 2) 57 | self.assertEqual(process_mock.return_value.status.call_count, 3) 58 | 59 | @patch("time.sleep") 60 | @patch("psutil.Process", spec=psutil.Process) 61 | def test_wait_for_sleeping_state_timeout( 62 | self, process_mock: MagicMock, sleep_mock: MagicMock 63 | ) -> None: 64 | """Test wait_for_sleeping_state() helper method times out.""" 65 | process_mock.return_value.status.return_value = "never-sleeps" 66 | with self.assertRaises(TimeoutError): 67 | wait_for_sleeping_state(1234567890, timeout=10) 68 | 69 | sleep_mock.assert_called_with(0.1) 70 | self.assertEqual(sleep_mock.call_count, 101) 71 | 72 | @patch("time.sleep") 73 | @patch("tests.helper.subprocess.check_output") 74 | def test_wait_for_process_to_appear( 75 | self, check_output_mock: MagicMock, sleep_mock: MagicMock 76 | ) -> None: 77 | """Test wait_for_process_to_appear() helper method.""" 78 | check_output_mock.side_effect = [ 79 | b"100 101 102", # pre-existing processes 80 | b"100 101 102 103", # 103 is new 81 | ] 82 | pid: int = wait_for_process_to_appear("/bin/sleep", {100, 101, 102}) 83 | 84 | self.assertEqual(pid, 103) 85 | sleep_mock.assert_called_with(0.1) 86 | self.assertEqual(sleep_mock.call_count, 1) 87 | 88 | @patch("time.sleep") 89 | @patch("tests.helper.subprocess.check_output") 90 | def test_wait_for_process_to_appear_timeout( 91 | self, check_output_mock: MagicMock, sleep_mock: MagicMock 92 | ) -> None: 93 | """Test wait_for_process_to_appear() helper method times out.""" 94 | check_output_mock.return_value = b"" 95 | with self.assertRaises(TimeoutError): 96 | wait_for_process_to_appear("/bin/sleep", set(), timeout=10) 97 | 98 | sleep_mock.assert_called_with(0.1) 99 | self.assertEqual(sleep_mock.call_count, 101) 100 | 101 | @patch("time.sleep") 102 | @patch("tests.helper.subprocess.check_output") 103 | def test_wait_for_process_to_appear_multiple( 104 | self, check_output_mock: MagicMock, sleep_mock: MagicMock 105 | ) -> None: 106 | """Test wait_for_process_to_appear() helper method raises AssertionError. 107 | 108 | wait_for_process_to_appear expects to find only one PID. 109 | """ 110 | check_output_mock.side_effect = [ 111 | b"100 101 102", # pre-existing processes 112 | b"100 101 102 103 104", # 103 and 104 are new 113 | ] 114 | with self.assertRaises(AssertionError): 115 | wait_for_process_to_appear("/bin/sleep", {100, 101, 102}) 116 | 117 | sleep_mock.assert_called_with(0.1) 118 | self.assertEqual(sleep_mock.call_count, 1) 119 | -------------------------------------------------------------------------------- /tests/integration/test_recoverable_problem.py: -------------------------------------------------------------------------------- 1 | """Test recoverable_problem""" 2 | 3 | # Copyright (C) 2012 Canonical Ltd. 4 | # Author: Evan Dandrea 5 | # 6 | # This program is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the 8 | # Free Software Foundation; either version 2 of the License, or (at your 9 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 10 | # the full text of the license. 11 | 12 | import os 13 | import shutil 14 | import subprocess 15 | import tempfile 16 | import time 17 | import unittest 18 | import unittest.mock 19 | from unittest.mock import MagicMock 20 | 21 | import apport.report 22 | from tests.paths import get_data_directory, local_test_environment 23 | 24 | 25 | class TestRecoverableProblem(unittest.TestCase): 26 | """Test recoverable_problem""" 27 | 28 | def setUp(self) -> None: 29 | self.env = os.environ | local_test_environment() 30 | self.report_dir = tempfile.mkdtemp() 31 | self.addCleanup(shutil.rmtree, self.report_dir) 32 | self.env["APPORT_REPORT_DIR"] = self.report_dir 33 | self.datadir = get_data_directory() 34 | 35 | # False positive return statement for unittest.TestCase.fail 36 | # See https://github.com/pylint-dev/pylint/issues/4167 37 | # pylint: disable-next=inconsistent-return-statements 38 | def _wait_for_report(self) -> str: 39 | seconds = 0.0 40 | while seconds < 10: 41 | crashes = os.listdir(self.report_dir) 42 | if crashes: 43 | assert len(crashes) == 1 44 | return os.path.join(self.report_dir, crashes[0]) 45 | 46 | time.sleep(0.1) 47 | seconds += 0.1 48 | self.fail( 49 | f"timeout while waiting for .crash file to be created" 50 | f" in {self.report_dir}." 51 | ) 52 | 53 | @unittest.mock.patch("os.listdir") 54 | @unittest.mock.patch("time.sleep") 55 | def test_wait_for_report_timeout( 56 | self, sleep_mock: MagicMock, listdir_mock: MagicMock 57 | ) -> None: 58 | """Test wait_for_report() helper runs into timeout.""" 59 | listdir_mock.return_value = [] 60 | with unittest.mock.patch.object(self, "fail") as fail_mock: 61 | self._wait_for_report() 62 | fail_mock.assert_called_once() 63 | sleep_mock.assert_called_with(0.1) 64 | self.assertEqual(sleep_mock.call_count, 101) 65 | 66 | def _call_recoverable_problem(self, data: str) -> None: 67 | cmd = [self.datadir / "recoverable_problem"] 68 | proc = subprocess.run( 69 | cmd, 70 | check=False, 71 | env=self.env, 72 | input=data, 73 | stderr=subprocess.PIPE, 74 | text=True, 75 | ) 76 | if proc.returncode != 0: 77 | # we expect some error message 78 | self.assertNotEqual(proc.stderr, "") 79 | raise subprocess.CalledProcessError(proc.returncode, cmd[0]) 80 | self.assertEqual(proc.stderr, "") 81 | 82 | def test_recoverable_problem(self) -> None: 83 | """recoverable_problem with valid data""" 84 | self._call_recoverable_problem("hello\0there") 85 | path = self._wait_for_report() 86 | with open(path, "rb") as report_path: 87 | report = apport.report.Report() 88 | report.load(report_path) 89 | self.assertEqual(report["hello"], "there") 90 | self.assertIn(f"Pid:\t{os.getpid()}", report["ProcStatus"]) 91 | 92 | def test_recoverable_problem_dupe_sig(self) -> None: 93 | """recoverable_problem duplicate signature includes ExecutablePath""" 94 | self._call_recoverable_problem("Package\0test\0DuplicateSignature\0ds") 95 | path = self._wait_for_report() 96 | with open(path, "rb") as report_path: 97 | report = apport.report.Report() 98 | report.load(report_path) 99 | exec_path = report.get("ExecutablePath") 100 | self.assertEqual(report["DuplicateSignature"], f"{exec_path}:ds") 101 | self.assertIn(f"Pid:\t{os.getpid()}", report["ProcStatus"]) 102 | 103 | def test_invalid_data(self) -> None: 104 | """recoverable_problem with invalid data""" 105 | self.assertRaises( 106 | subprocess.CalledProcessError, self._call_recoverable_problem, "hello" 107 | ) 108 | 109 | self.assertRaises( 110 | subprocess.CalledProcessError, 111 | self._call_recoverable_problem, 112 | "hello\0there\0extraneous", 113 | ) 114 | 115 | self.assertRaises( 116 | subprocess.CalledProcessError, 117 | self._call_recoverable_problem, 118 | "hello\0\0there", 119 | ) 120 | -------------------------------------------------------------------------------- /apport/hook_ui.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2007 - 2011 Canonical Ltd. 2 | # Author: Martin Pitt 3 | # 4 | # This program is free software; you can redistribute it and/or modify it 5 | # under the terms of the GNU General Public License as published by the 6 | # Free Software Foundation; either version 2 of the License, or (at your 7 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 8 | # the full text of the license. 9 | 10 | """Interactive functions which can be used in package hooks""" 11 | 12 | import threading 13 | import time 14 | 15 | 16 | class HookUI: 17 | """Interactive functions which can be used in package hooks. 18 | 19 | This provides an interface for package hooks which need to ask interactive 20 | questions. Directly passing the UserInterface instance to the hooks needs 21 | to be avoided, since we need to call the UI methods in a different thread, 22 | and also don't want hooks to be able to poke in the UI. 23 | """ 24 | 25 | def __init__(self, ui): 26 | """Create a HookUI object. 27 | 28 | ui is the UserInterface instance to wrap. 29 | """ 30 | self.ui = ui 31 | 32 | # variables for communicating with the UI thread 33 | self._request_event = threading.Event() 34 | self._response_event = threading.Event() 35 | self._request_fn = None 36 | self._request_args = None 37 | self._response = None 38 | 39 | # 40 | # API for hooks 41 | # 42 | 43 | def information(self, text): 44 | """Show an information with OK/Cancel buttons. 45 | 46 | This can be used for asking the user to perform a particular action, 47 | such as plugging in a device which does not work. 48 | """ 49 | return self._trigger_ui_request("ui_info_message", "", text) 50 | 51 | def yesno(self, text): 52 | """Show a yes/no question. 53 | 54 | Return True if the user selected "Yes", False if selected "No" or 55 | "None" on cancel/dialog closing. 56 | """ 57 | return self._trigger_ui_request("ui_question_yesno", text) 58 | 59 | def choice(self, text, options, multiple=False): 60 | """Show an question with predefined choices. 61 | 62 | options is a list of strings to present. If multiple is True, they 63 | should be check boxes, if multiple is False they should be radio 64 | buttons. 65 | 66 | Return list of selected option indexes, or None if the user cancelled. 67 | If multiple == False, the list will always have one element. 68 | """ 69 | return self._trigger_ui_request("ui_question_choice", text, options, multiple) 70 | 71 | def file(self, text): 72 | """Show a file selector dialog. 73 | 74 | Return path if the user selected a file, or None if cancelled. 75 | """ 76 | return self._trigger_ui_request("ui_question_file", text) 77 | 78 | # 79 | # internal API for inter-thread communication 80 | # 81 | 82 | def _trigger_ui_request(self, fn, *args): 83 | """Called by HookUi functions in info collection thread.""" 84 | # only one at a time 85 | assert not self._request_event.is_set() 86 | assert not self._response_event.is_set() 87 | assert self._request_fn is None 88 | 89 | self._response = None 90 | self._request_fn = fn 91 | self._request_args = args 92 | self._request_event.set() 93 | self._response_event.wait() 94 | 95 | self._request_fn = None 96 | self._response_event.clear() 97 | 98 | return self._response 99 | 100 | def process_event(self): 101 | """Called by GUI thread to check and process hook UI requests.""" 102 | # sleep for 0.1 seconds to wait for events 103 | self._request_event.wait(0.1) 104 | if not self._request_event.is_set(): 105 | return 106 | 107 | assert not self._response_event.is_set() 108 | self._request_event.clear() 109 | self._response = getattr(self.ui, self._request_fn)(*self._request_args) 110 | self._response_event.set() 111 | 112 | 113 | class NoninteractiveHookUI(HookUI): 114 | """HookUI variant that does not ask the user any questions.""" 115 | 116 | def __init__(self): 117 | super().__init__(None) 118 | 119 | def __repr__(self) -> str: 120 | return f"{self.__class__.__name__}()" 121 | 122 | def information(self, text): 123 | return None 124 | 125 | def yesno(self, text): 126 | return None 127 | 128 | def choice(self, text, options, multiple=False): 129 | return None 130 | 131 | def file(self, text): 132 | return None 133 | 134 | def process_event(self): 135 | # Give other threads some chance to run 136 | time.sleep(0.1) 137 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [ 3 | {name = "Martin Pitt", email = "martin.pitt@ubuntu.com"}, 4 | ] 5 | description = "intercept, process, and report crashes and bug reports" 6 | dynamic = ["version"] 7 | license = {text = "GPLv2+"} 8 | name = "apport" 9 | requires-python = ">=3.11" 10 | 11 | [tool.black] 12 | line-length = 88 13 | 14 | [tool.codespell] 15 | skip = ".git,*.click,*.gif,*.po,*.png" 16 | ignore-words-list = "buildd" 17 | 18 | [tool.isort] 19 | line_length = 88 20 | profile = "black" 21 | 22 | [tool.mypy] 23 | disallow_incomplete_defs = true 24 | ignore_missing_imports = true 25 | 26 | [tool.pydocstyle] 27 | # TODO: Address the ignored codes (except D203,D213) 28 | ignore = "D1,D203,D205,D209,D213,D400,D401,D402,D415" 29 | match = ".*\\.py" 30 | 31 | [tool.pylint.main] 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code. 35 | extension-pkg-allow-list = ["apt_pkg"] 36 | 37 | # List of plugins (as comma separated values of python module names) to load, 38 | # usually to register additional checkers. Following plugins are not wanted: 39 | # * pylint.extensions.broad_try_clause 40 | # * pylint.extensions.consider_ternary_expression 41 | # * pylint.extensions.empty_comment 42 | # * pylint.extensions.for_any_all 43 | # * pylint.extensions.magic_value 44 | # * pylint.extensions.while_used 45 | load-plugins = [ 46 | "pylint.extensions.check_elif", 47 | "pylint.extensions.code_style", 48 | "pylint.extensions.comparison_placement", 49 | "pylint.extensions.consider_refactoring_into_while_condition", 50 | "pylint.extensions.dict_init_mutate", 51 | "pylint.extensions.docparams", 52 | "pylint.extensions.docstyle", 53 | "pylint.extensions.dunder", 54 | "pylint.extensions.eq_without_hash", 55 | "pylint.extensions.mccabe", 56 | "pylint.extensions.no_self_use", 57 | "pylint.extensions.overlapping_exceptions", 58 | "pylint.extensions.private_import", 59 | "pylint.extensions.redefined_variable_type", 60 | "pylint.extensions.set_membership", 61 | "pylint.extensions.typing", 62 | ] 63 | 64 | # Pickle collected data for later comparisons. 65 | persistent = false 66 | 67 | [tool.pylint.design] 68 | # Maximum number of arguments for function / method. 69 | max-args = 6 70 | 71 | # Maximum number of attributes for a class (see R0902). 72 | max-attributes = 9 73 | 74 | # McCabe complexity cyclomatic threshold 75 | max-complexity = 11 76 | 77 | # Maximum number of positional arguments for function / method. 78 | max-positional-arguments=6 79 | 80 | # Maximum number of public methods for a class (see R0904). 81 | max-public-methods = 25 82 | 83 | [tool.pylint.format] 84 | # Maximum number of characters on a single line. 85 | max-line-length = 88 86 | 87 | [tool.pylint."messages control"] 88 | # Disable the message, report, category or checker with the given id(s). You can 89 | # either give multiple identifiers separated by comma (,) or put this option 90 | # multiple times (only on the command line, not in the configuration file where 91 | # it should appear only once). You can also use "--disable=all" to disable 92 | # everything first and then re-enable specific checks. For example, if you want 93 | # to run only the similarities checker, you can use "--disable=all 94 | # --enable=similarities". If you want to run only the classes checker, but have 95 | # no Warning level messages displayed, use "--disable=all --enable=classes 96 | # --disable=W". 97 | disable = ["consider-using-assignment-expr", "duplicate-code", "fixme"] 98 | 99 | # Enable the message, report, category or checker with the given id(s). You can 100 | # either give multiple identifier separated by comma (,) or put this option 101 | # multiple time (only on the command line, not in the configuration file where it 102 | # should appear only once). See also the "--disable" option for examples. 103 | enable = ["useless-suppression"] 104 | 105 | [tool.pylint.reports] 106 | # Tells whether to display a full report or only the messages. 107 | reports = false 108 | 109 | # Activate the evaluation score. 110 | score = false 111 | 112 | [tool.pytest.ini_options] 113 | addopts = "--cov-branch" 114 | 115 | [tool.ruff] 116 | include = [ 117 | "pyproject.toml", 118 | "**/*.py", 119 | "bin/apport-cli", 120 | "bin/apport-retrace", 121 | "bin/apport-unpack", 122 | "bin/apport-valgrind", 123 | "bin/crash-digger", 124 | "bin/dupdb-admin", 125 | "data/apport", 126 | "data/apport-checkreports", 127 | "data/apportcheckresume", 128 | "data/gcc_ice_hook", 129 | "data/iwlwifi_error_dump", 130 | "data/java_uncaught_exception", 131 | "data/kernel_crashdump", 132 | "data/kernel_oops", 133 | "data/package_hook", 134 | "data/recoverable_problem", 135 | "data/unkillable_shutdown", 136 | "data/whoopsie-upload-all", 137 | "gtk/apport-gtk", 138 | "kde/apport-kde", 139 | ] 140 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """Installer script for Apport.""" 4 | 5 | import glob 6 | import logging 7 | import os.path 8 | import subprocess 9 | import sys 10 | 11 | from setuptools_apport.java import register_java_sub_commands 12 | 13 | try: 14 | import DistUtilsExtra.auto 15 | from DistUtilsExtra.command.build_extra import build_extra 16 | except ImportError: 17 | sys.stderr.write( 18 | "To build Apport you need https://launchpad.net/python-distutils-extra\n" 19 | ) 20 | sys.exit(1) 21 | 22 | BASH_COMPLETIONS = "share/bash-completion/completions/" 23 | 24 | 25 | # pylint: disable-next=invalid-name 26 | class clean_java_subdir(DistUtilsExtra.auto.clean_build_tree): 27 | """Java crash handler clean command.""" 28 | 29 | def run(self): 30 | DistUtilsExtra.auto.clean_build_tree.run(self) 31 | for root, _, files in os.walk("java"): 32 | for f in files: 33 | if f.endswith(".jar") or f.endswith(".class"): 34 | os.unlink(os.path.join(root, f)) 35 | 36 | 37 | # pylint: disable-next=invalid-name 38 | class install_fix_completion_symlinks(DistUtilsExtra.auto.install_auto): 39 | """Fix symlinks in the bash completion dir.""" 40 | 41 | def run(self): 42 | log = logging.getLogger(__name__) 43 | DistUtilsExtra.auto.install_auto.run(self) 44 | 45 | autoinstalled_completion_dir = os.path.join( 46 | self.install_data, "share", "apport", "bash-completion" 47 | ) 48 | for completion in glob.glob("data/bash-completion/*"): 49 | try: 50 | source = os.readlink(completion) 51 | except OSError: 52 | continue 53 | dest = os.path.join( 54 | self.install_data, BASH_COMPLETIONS, os.path.basename(completion) 55 | ) 56 | if not os.path.exists(dest): 57 | continue 58 | 59 | log.info("Convert %s into a symlink to %s...", dest, source) 60 | os.remove(dest) 61 | os.symlink(source, dest) 62 | 63 | autoinstalled = os.path.join( 64 | autoinstalled_completion_dir, os.path.basename(completion) 65 | ) 66 | os.remove(autoinstalled) 67 | 68 | # Clean-up left-over bash-completion from auto install 69 | if os.path.isdir(autoinstalled_completion_dir): 70 | os.rmdir(autoinstalled_completion_dir) 71 | 72 | 73 | # 74 | # main 75 | # 76 | 77 | from apport.ui import __version__ # noqa: E402, pylint: disable=C0413 78 | 79 | # determine systemd unit directory 80 | try: 81 | SYSTEMD_UNIT_DIR = subprocess.check_output( 82 | ["pkg-config", "--variable=systemdsystemunitdir", "systemd"], 83 | universal_newlines=True, 84 | ).strip() 85 | SYSTEMD_TMPFILES_DIR = subprocess.check_output( 86 | ["pkg-config", "--variable=tmpfilesdir", "systemd"], universal_newlines=True 87 | ).strip() 88 | except (FileNotFoundError, subprocess.CalledProcessError): 89 | # hardcoded fallback path 90 | SYSTEMD_UNIT_DIR = "/lib/systemd/system" 91 | SYSTEMD_TMPFILES_DIR = "/usr/lib/tmpfiles.d" 92 | 93 | try: 94 | UDEV_DIR = subprocess.check_output( 95 | ["pkg-config", "--variable=udevdir", "udev"], text=True 96 | ).strip() 97 | except (FileNotFoundError, subprocess.CalledProcessError): 98 | UDEV_DIR = "/lib/udev" 99 | 100 | cmdclass = register_java_sub_commands(build_extra, install_fix_completion_symlinks) 101 | DistUtilsExtra.auto.setup( 102 | name="apport", 103 | author="Martin Pitt", 104 | author_email="martin.pitt@ubuntu.com", 105 | url="https://launchpad.net/apport", 106 | license="gpl", 107 | description="intercept, process, and report crashes and bug reports", 108 | packages=[ 109 | "apport", 110 | "apport.crashdb_impl", 111 | "apport.packaging_impl", 112 | "problem_report", 113 | ], 114 | package_data={"apport": ["py.typed"], "problem_report": ["py.typed"]}, 115 | version=__version__, 116 | data_files=[ 117 | ("share/doc/apport/", glob.glob("doc/*.txt")), 118 | # these are not supposed to be called directly, use apport-bug instead 119 | ("share/apport", ["gtk/apport-gtk", "kde/apport-kde"]), 120 | (BASH_COMPLETIONS, glob.glob("data/bash-completion/*")), 121 | ("lib/pm-utils/sleep.d/", glob.glob("pm-utils/sleep.d/*")), 122 | (f"{UDEV_DIR}/rules.d", glob.glob("udev/*.rules")), 123 | ( 124 | SYSTEMD_UNIT_DIR, 125 | glob.glob("data/systemd/*.service") + glob.glob("data/systemd/*.socket"), 126 | ), 127 | ( 128 | f"{SYSTEMD_UNIT_DIR}/systemd-coredump@.service.d", 129 | ["data/systemd/systemd-coredump@.service.d/apport-coredump-hook.conf"], 130 | ), 131 | (SYSTEMD_TMPFILES_DIR, glob.glob("data/systemd/*.conf")), 132 | ], 133 | cmdclass={ 134 | "build": build_extra, 135 | "clean": clean_java_subdir, 136 | "install": install_fix_completion_symlinks, 137 | } 138 | | cmdclass, 139 | ) 140 | -------------------------------------------------------------------------------- /apport/crashdb_impl/debian.py: -------------------------------------------------------------------------------- 1 | """Debian crash database interface.""" 2 | 3 | # Debian adaptation Copyright (C) 2012 Ritesh Raj Sarraf 4 | # 5 | # This program is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation; either version 2 of the License, or (at your 8 | # option) any later version. See http://www.gnu.org/copyleft/gpl.html for 9 | # the full text of the license. 10 | 11 | 12 | import email.mime.text 13 | import smtplib 14 | import tempfile 15 | from typing import Any 16 | 17 | import apport.crashdb 18 | 19 | 20 | class CrashDatabase(apport.crashdb.CrashDatabase): 21 | """Debian crash database. 22 | 23 | This is a Apport CrashDB implementation for interacting with Debian BTS. 24 | """ 25 | 26 | # TODO: Implement several missing abstract methods from parent class 27 | # pylint: disable=abstract-method 28 | 29 | def __init__(self, auth_file: str | None, options: dict[str, Any]) -> None: 30 | """Initialize crash database connection. 31 | 32 | Debian implementation is pretty basic as most of its bug management 33 | processes revolve around the email interface 34 | """ 35 | apport.crashdb.CrashDatabase.__init__(self, auth_file, options) 36 | self.options = options 37 | 38 | if not self.options.get("smtphost"): 39 | self.options["smtphost"] = "reportbug.debian.org" 40 | 41 | if not self.options.get("recipient"): 42 | self.options["recipient"] = "submit@bugs.debian.org" 43 | 44 | def accepts(self, report): 45 | """Check if this report can be uploaded to this database. 46 | Checks for the proper settings of apport. 47 | """ 48 | if not self.options.get("sender") and "UnreportableReason" not in report: 49 | report["UnreportableReason"] = ( 50 | "Please configure sender settings in /etc/apport/crashdb.conf" 51 | ) 52 | 53 | # At this time, we are not ready to take CrashDumps 54 | if "Stacktrace" in report and not report.has_useful_stacktrace(): 55 | report["UnreportableReason"] = ( 56 | "Incomplete backtrace. Please install the debug symbol packages" 57 | ) 58 | 59 | return apport.crashdb.CrashDatabase.accepts(self, report) 60 | 61 | def upload(self, report, progress_callback=None, user_message_callback=None): 62 | """Upload given problem report return a handle for it. 63 | 64 | In Debian, we use BTS, which is heavily email oriented. This method 65 | crafts the bug into an email report understood by Debian BTS. 66 | """ 67 | # first and foremost, let's check if the apport bug filing 68 | # settings are set correct 69 | assert self.accepts(report) 70 | 71 | # Frame the report in the format the BTS understands 72 | try: 73 | (buggy_package, buggy_version) = report["Package"].split(" ") 74 | except (KeyError, ValueError): 75 | return False 76 | 77 | with tempfile.NamedTemporaryFile() as temp: 78 | temp.file.write(f"Package: {buggy_package}\n".encode("UTF-8")) 79 | temp.file.write(f"Version: {buggy_version}\n\n\n".encode("UTF-8")) 80 | temp.file.write(("=============================\n\n").encode("UTF-8")) 81 | 82 | # Let's remove the CoreDump first 83 | 84 | # Even if we have a valid backtrace, we already are reporting it 85 | # as text. We don't want to send very large emails to the BTS. 86 | # OTOH, if the backtrace is invalid, has_useful_backtrace() will 87 | # already deny reporting of the bug report. 88 | try: 89 | del report["CoreDump"] 90 | except KeyError: 91 | pass 92 | 93 | # Now write the apport bug report 94 | report.write(temp) 95 | 96 | temp.file.seek(0) 97 | 98 | msg = email.mime.text.MIMEText(temp.file.read().decode("UTF-8")) 99 | 100 | msg["Subject"] = report["Title"] 101 | msg["From"] = self.options["sender"] 102 | msg["To"] = self.options["recipient"] 103 | 104 | # Subscribe the submitted to the bug report 105 | msg.add_header("X-Debbugs-CC", self.options["sender"]) 106 | msg.add_header("Usertag", f"apport-{report['ProblemType'].lower()}") 107 | 108 | smtp = smtplib.SMTP(self.options["smtphost"]) 109 | smtp.sendmail( 110 | self.options["sender"], 111 | self.options["recipient"], 112 | msg.as_string().encode("UTF-8"), 113 | ) 114 | smtp.quit() 115 | return True 116 | 117 | def get_comment_url(self, report, handle): 118 | """Return an URL that should be opened after report has been uploaded 119 | and upload() returned handle. 120 | 121 | Should return None if no URL should be opened (anonymous filing without 122 | user comments); in that case this function should do whichever 123 | interactive steps it wants to perform. 124 | """ 125 | return None 126 | --------------------------------------------------------------------------------