mirror of https://gitee.com/openkylin/apport.git
420 lines
13 KiB
Python
Executable File
420 lines
13 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
"""Command line Apport user interface."""
|
|
|
|
# Copyright (C) 2007 - 2009 Canonical Ltd.
|
|
# Author: Michael Hofmann <mh21@piware.de>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation; either version 2 of the License, or (at your
|
|
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
|
|
# the full text of the license.
|
|
|
|
# Web browser support:
|
|
# w3m, lynx: do not work
|
|
# elinks: works
|
|
|
|
# TODO: Address following pylint complaints
|
|
# pylint: disable=invalid-name,missing-function-docstring
|
|
|
|
import errno
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import termios
|
|
from gettext import gettext as _
|
|
|
|
import apport.ui
|
|
|
|
|
|
class CLIDialog:
|
|
"""Command line dialog wrapper."""
|
|
|
|
def __init__(self, heading, text):
|
|
self.heading = f"\n*** {heading}\n"
|
|
self.text = text
|
|
self.keys = []
|
|
self.buttons = []
|
|
self.visible = False
|
|
|
|
@staticmethod
|
|
def raw_input_char(prompt, multi_char=False):
|
|
"""raw_input, but read a single character unless multi_char is True.
|
|
|
|
@param: prompt: the text presented to the user to solict a response.
|
|
@param: multi_char: Boolean True if we need to read until <enter>.
|
|
"""
|
|
|
|
sys.stdout.write(prompt)
|
|
sys.stdout.write(" ")
|
|
sys.stdout.flush()
|
|
|
|
file = sys.stdin.fileno()
|
|
saved_attributes = termios.tcgetattr(file)
|
|
attributes = termios.tcgetattr(file)
|
|
attributes[3] = attributes[3] & ~(termios.ICANON)
|
|
attributes[6][termios.VMIN] = 1
|
|
attributes[6][termios.VTIME] = 0
|
|
termios.tcsetattr(file, termios.TCSANOW, attributes)
|
|
try:
|
|
if multi_char:
|
|
response = str(sys.stdin.readline()).strip()
|
|
else:
|
|
response = str(sys.stdin.read(1))
|
|
finally:
|
|
termios.tcsetattr(file, termios.TCSANOW, saved_attributes)
|
|
|
|
sys.stdout.write("\n")
|
|
return response
|
|
|
|
def show(self):
|
|
self.visible = True
|
|
print(self.heading)
|
|
if self.text:
|
|
print(self.text)
|
|
|
|
def run(self, prompt=None):
|
|
if not self.visible:
|
|
self.show()
|
|
|
|
sys.stdout.write("\n")
|
|
try:
|
|
# Only one button
|
|
if len(self.keys) <= 1:
|
|
self.raw_input_char(_("Press any key to continue..."))
|
|
return 0
|
|
# Multiple choices
|
|
while True:
|
|
if prompt is not None:
|
|
print(prompt)
|
|
else:
|
|
print(_("What would you like to do? Your options are:"))
|
|
for index, button in enumerate(self.buttons):
|
|
print(f" {self.keys[index]}: {button}")
|
|
|
|
if len(self.keys) <= 10:
|
|
# A 10 option prompt would can still be a single character
|
|
# response because the 10 options listed will be 1-9 and C.
|
|
# Therefore there are 10 unique responses which can be
|
|
# given.
|
|
multi_char = False
|
|
else:
|
|
multi_char = True
|
|
response = self.raw_input_char(
|
|
_("Please choose (%s):") % ("/".join(self.keys)), multi_char
|
|
)
|
|
try:
|
|
return self.keys.index(response.upper()) + 1
|
|
except ValueError:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
sys.stdout.write("\n")
|
|
sys.exit(1)
|
|
|
|
def addbutton(self, button, hotkey=None):
|
|
if hotkey:
|
|
self.keys.append(hotkey)
|
|
self.buttons.append(button)
|
|
else:
|
|
self.keys.append(re.search("&(.)", button).group(1).upper())
|
|
self.buttons.append(re.sub("&", "", button))
|
|
return len(self.keys)
|
|
|
|
|
|
class CLIProgressDialog(CLIDialog):
|
|
"""Command line progress dialog wrapper."""
|
|
|
|
def __init__(self, heading, text):
|
|
CLIDialog.__init__(self, heading, text)
|
|
self.progresscount = 0
|
|
|
|
def set(self, progress=None):
|
|
self.progresscount = (self.progresscount + 1) % 5
|
|
if self.progresscount:
|
|
return
|
|
|
|
if progress is not None:
|
|
sys.stdout.write(f"\r{progress * 100}%")
|
|
else:
|
|
sys.stdout.write(".")
|
|
sys.stdout.flush()
|
|
|
|
|
|
class CLIUserInterface(apport.ui.UserInterface):
|
|
"""Command line Apport user interface"""
|
|
|
|
def __init__(self, argv: list[str]):
|
|
apport.ui.UserInterface.__init__(self, argv)
|
|
self.in_update_view = False
|
|
self.progress = None
|
|
|
|
def _get_details(self) -> str:
|
|
"""Build report string for display."""
|
|
assert self.report
|
|
|
|
details = ""
|
|
max_show = 1000000
|
|
for key, value in self.report.sorted_items():
|
|
details += f"== {key} =================================\n"
|
|
# string value
|
|
keylen = len(value)
|
|
if isinstance(value, str) and keylen < max_show:
|
|
s = value
|
|
elif keylen >= max_show:
|
|
s = _("(%i bytes)") % keylen
|
|
else:
|
|
s = _("(binary data)")
|
|
|
|
details += s
|
|
details += "\n\n"
|
|
|
|
return details
|
|
|
|
def ui_update_view(self, stdout=None):
|
|
self.in_update_view = True
|
|
report = self._get_details()
|
|
try:
|
|
apport.ui.run_as_real_user(
|
|
["/usr/bin/sensible-pager"], input=report.encode("UTF-8"), stdout=stdout
|
|
)
|
|
except OSError as error:
|
|
# ignore broken pipe (premature quit)
|
|
if error.errno == errno.EPIPE:
|
|
pass
|
|
else:
|
|
raise
|
|
self.in_update_view = False
|
|
|
|
#
|
|
# ui_* implementation of abstract UserInterface classes
|
|
#
|
|
|
|
def _save_report_in_temp_directory(self) -> str:
|
|
assert self.report
|
|
prefix = "apport."
|
|
if "Package" in self.report:
|
|
prefix += self.report["Package"].split()[0] + "."
|
|
(fd, report_file) = tempfile.mkstemp(prefix=prefix, suffix=".apport")
|
|
with os.fdopen(fd, "wb") as f:
|
|
self.report.write(f)
|
|
return report_file
|
|
|
|
def ui_present_report_details(
|
|
self, allowed_to_report: bool = True, modal_for: int | None = None
|
|
) -> apport.ui.Action:
|
|
dialog = CLIDialog(
|
|
_("Send problem report to the developers?"),
|
|
_(
|
|
"After the problem report has been sent,"
|
|
" please fill out the form in the\n"
|
|
"automatically opened web browser."
|
|
),
|
|
)
|
|
|
|
complete = dialog.addbutton(
|
|
_("&Send report (%s)") % self.format_filesize(self.get_complete_size())
|
|
)
|
|
|
|
if self.can_examine_locally():
|
|
examine = dialog.addbutton(_("&Examine locally"))
|
|
else:
|
|
examine = None
|
|
|
|
view = dialog.addbutton(_("&View report"))
|
|
save = dialog.addbutton(
|
|
_("&Keep report file for sending later or copying to somewhere else")
|
|
)
|
|
ignore = dialog.addbutton(
|
|
_("Cancel and &ignore future crashes of this program version")
|
|
)
|
|
|
|
dialog.addbutton(_("&Cancel"))
|
|
|
|
while True:
|
|
response = dialog.run()
|
|
|
|
return_value = apport.ui.Action()
|
|
if response == examine:
|
|
return_value.examine = True
|
|
return return_value
|
|
if response == complete:
|
|
return_value.report = True
|
|
if response == ignore:
|
|
return_value.ignore = True
|
|
if response == view:
|
|
self.collect_info()
|
|
self.ui_update_view()
|
|
continue
|
|
if response == save:
|
|
# we do not already have a report file if we report a bug
|
|
if not self.report_file:
|
|
self.report_file = self._save_report_in_temp_directory()
|
|
print(_("Problem report file:") + f" {self.report_file}")
|
|
|
|
return return_value
|
|
|
|
def ui_info_message(self, title, text):
|
|
dialog = CLIDialog(title, text)
|
|
dialog.addbutton(_("&Confirm"))
|
|
dialog.run()
|
|
|
|
def ui_error_message(self, title, text):
|
|
dialog = CLIDialog(_("Error: %s") % title, text)
|
|
dialog.addbutton(_("&Confirm"))
|
|
dialog.run()
|
|
|
|
def ui_start_info_collection_progress(self):
|
|
self.progress = CLIProgressDialog(
|
|
_("Collecting problem information"),
|
|
_(
|
|
"The collected information can be sent"
|
|
" to the developers to improve the\n"
|
|
"application. This might take a few minutes."
|
|
),
|
|
)
|
|
self.progress.show()
|
|
|
|
def ui_pulse_info_collection_progress(self):
|
|
assert self.progress is not None
|
|
self.progress.set()
|
|
|
|
def ui_stop_info_collection_progress(self):
|
|
sys.stdout.write("\n")
|
|
|
|
def ui_start_upload_progress(self):
|
|
self.progress = CLIProgressDialog(
|
|
_("Uploading problem information"),
|
|
_(
|
|
"The collected information is being sent"
|
|
" to the bug tracking system.\n"
|
|
"This might take a few minutes."
|
|
),
|
|
)
|
|
self.progress.show()
|
|
|
|
def ui_set_upload_progress(self, progress: float | None) -> None:
|
|
assert self.progress is not None
|
|
self.progress.set(progress)
|
|
|
|
def ui_stop_upload_progress(self):
|
|
sys.stdout.write("\n")
|
|
|
|
def ui_question_yesno(self, text):
|
|
"""Show a yes/no question.
|
|
|
|
Return True if the user selected "Yes", False if selected "No" or
|
|
"None" on cancel/dialog closing.
|
|
"""
|
|
dialog = CLIDialog(text, None)
|
|
r_yes = dialog.addbutton("&Yes")
|
|
r_no = dialog.addbutton("&No")
|
|
r_cancel = dialog.addbutton(_("&Cancel"))
|
|
result = dialog.run()
|
|
if result == r_yes:
|
|
return True
|
|
if result == r_no:
|
|
return False
|
|
assert result == r_cancel
|
|
return None
|
|
|
|
def ui_question_choice(self, text, options, multiple):
|
|
"""Show an question with predefined choices.
|
|
|
|
options is a list of strings to present. If multiple is True, they
|
|
should be check boxes, if multiple is False they should be radio
|
|
buttons.
|
|
|
|
Return list of selected option indexes, or None if the user cancelled.
|
|
If multiple == False, the list will always have one element.
|
|
"""
|
|
result = []
|
|
dialog = CLIDialog(text, None)
|
|
|
|
if multiple:
|
|
while True:
|
|
dialog = CLIDialog(text, None)
|
|
index = 0
|
|
choice_index_map = {}
|
|
for option in options:
|
|
if index not in result:
|
|
choice_index_map[dialog.addbutton(option, str(index + 1))] = (
|
|
index
|
|
)
|
|
index += 1
|
|
done = dialog.addbutton(_("&Done"))
|
|
cancel = dialog.addbutton(_("&Cancel"))
|
|
|
|
if result:
|
|
cur = ", ".join([str(r + 1) for r in result])
|
|
else:
|
|
cur = _("none")
|
|
response = dialog.run(_("Selected: %s. Multiple choices:") % cur)
|
|
if response == cancel:
|
|
return None
|
|
if response == done:
|
|
break
|
|
result.append(choice_index_map[response])
|
|
|
|
else:
|
|
# single choice (radio button)
|
|
dialog = CLIDialog(text, None)
|
|
index = 1
|
|
for option in options:
|
|
dialog.addbutton(option, str(index))
|
|
index += 1
|
|
|
|
cancel = dialog.addbutton(_("&Cancel"))
|
|
response = dialog.run(_("Choices:"))
|
|
if response == cancel:
|
|
return None
|
|
result.append(response - 1)
|
|
|
|
return result
|
|
|
|
def ui_question_file(self, text):
|
|
"""Show a file selector dialog.
|
|
|
|
Return path if the user selected a file, or None if cancelled.
|
|
"""
|
|
print(f"\n*** {text}")
|
|
while True:
|
|
sys.stdout.write(_("Path to file (Enter to cancel):"))
|
|
sys.stdout.write(" ")
|
|
f = sys.stdin.readline().strip()
|
|
if not f:
|
|
return None
|
|
if not os.path.exists(f):
|
|
print(_("File does not exist."))
|
|
elif os.path.isdir(f):
|
|
print(_("This is a directory."))
|
|
else:
|
|
return f
|
|
|
|
def open_url(self, url):
|
|
header = _("To continue, you must visit the following URL:")
|
|
footer = _(
|
|
"You can launch a browser now,"
|
|
" or copy this URL into a browser on another computer."
|
|
)
|
|
text = f"{header}\n\n {url}\n\n{footer}"
|
|
|
|
answer = self.ui_question_choice(text, [_("Launch a browser now")], False)
|
|
if answer == [0]:
|
|
apport.ui.UserInterface.open_url(self, url)
|
|
|
|
def ui_has_terminal(self):
|
|
# we are already running in a terminal, so this works by definition
|
|
return True
|
|
|
|
def ui_run_terminal(self, command):
|
|
subprocess.call(command, shell=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = CLIUserInterface(sys.argv)
|
|
if not app.run_argv():
|
|
print(_("No pending crash reports. Try --help for more information."))
|