blob: 36d6759d177b81953a0ed23643dce5a5aa9f2d98 [file] [log] [blame]
# SPDX-License-Identifier: MIT
"""s2idle analysis tool"""
import argparse
import sys
import os
import subprocess
import sqlite3
from datetime import date, timedelta, datetime
from amd_debug.common import (
convert_string_to_bool,
colorize_choices,
is_root,
relaunch_sudo,
show_log_info,
version,
running_ssh,
)
from amd_debug.validator import SleepValidator
from amd_debug.installer import Installer
from amd_debug.prerequisites import PrerequisiteValidator
from amd_debug.sleep_report import SleepReport
class Defaults:
"""Default values for the script"""
duration = 10
wait = 4
count = 1
since = date.today() - timedelta(days=60)
until = date.today() + timedelta(days=1)
format_choices = ["txt", "md", "html", "stdout"]
boolean_choices = ["true", "false"]
class Headers:
"""Headers for the script"""
DurationDescription = "How long should suspend cycles last (seconds)"
WaitDescription = "How long to wait in between suspend cycles (seconds)"
CountDescription = "How many suspend cycles to run"
SinceDescription = "What date to start report data"
UntilDescription = "What date to end report data"
LogDescription = "Location of log file"
ReportFileDescription = "Location of report file"
FormatDescription = "What format to output the report in"
MaxDurationDescription = "What is the maximum suspend cycle length (seconds)"
MaxWaitDescription = "What is the maximum time between suspend cycles (seconds)"
ReportDebugDescription = "Enable debug output in report (increased size)"
def display_report_file(fname, fmt) -> None:
"""Display report file"""
if fmt != "html":
return
if not is_root():
subprocess.call(["xdg-open", fname])
return
user = os.environ.get("SUDO_USER")
if user:
# ensure that xdg tools will know how to display the file
# (user may need to call tool with sudo -E)
if os.environ.get("XDG_SESSION_TYPE"):
subprocess.call(["sudo", "-E", "-u", user, "xdg-open", fname])
else:
print(
"To display report automatically in browser launch tool "
f"with '-E' argument (Example: sudo -E {sys.argv[0]})"
)
def get_report_file(report_file, extension) -> str:
"""Prompt user for report file"""
if extension == "stdout":
return ""
if not report_file:
return f"amd-s2idle-report-{date.today()}.{extension}"
return report_file
def get_report_format() -> str:
"""Get report format"""
if running_ssh():
return "txt"
return "html"
def prompt_report_arguments(since, until, fname, fmt, report_debug) -> list:
"""Prompt user for report configuration"""
if not since:
default = Defaults.since
since = input(f"{Headers.SinceDescription} (default {default})? ")
if not since:
since = default.isoformat()
try:
since = datetime.fromisoformat(since)
except ValueError as e:
sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")
if not until:
default = Defaults.until
until = input(f"{Headers.SinceDescription} (default {default})? ")
if not until:
until = default.isoformat()
try:
until = datetime.fromisoformat(until)
except ValueError as e:
sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")
if not fmt:
fmt = input(
f"{Headers.FormatDescription} ({colorize_choices(Defaults.format_choices, get_report_format())})? "
)
if not fmt:
fmt = get_report_format()
if fmt not in Defaults.format_choices:
sys.exit(f"Invalid format: {fmt}")
if report_debug is None:
inp = (
input(
f"{Headers.ReportDebugDescription} ({colorize_choices(Defaults.boolean_choices, 'true')})? "
)
.lower()
.capitalize()
)
report_debug = True if not inp else convert_string_to_bool(inp)
return [since, until, get_report_file(fname, fmt), fmt, report_debug]
def prompt_test_arguments(duration, wait, count, rand) -> list:
"""Prompt user for test configuration"""
if not duration:
if rand:
question = Headers.MaxDurationDescription
else:
question = Headers.DurationDescription
duration = input(f"{question} (default {Defaults.duration})? ")
if not duration:
duration = Defaults.duration
try:
duration = int(duration)
except ValueError as e:
sys.exit(f"Invalid duration: {e}")
if not wait:
if rand:
question = Headers.MaxWaitDescription
else:
question = Headers.WaitDescription
wait = input(f"{question} (default {Defaults.wait})? ")
if not wait:
wait = Defaults.wait
try:
wait = int(wait)
except ValueError as e:
sys.exit(f"Invalid wait: {e}")
if not count:
count = input(f"{Headers.CountDescription} (default {Defaults.count})? ")
if not count:
count = Defaults.count
try:
count = int(count)
except ValueError as e:
sys.exit(f"Invalid count: {e}")
return [duration, wait, count]
def report(since, until, fname, fmt, tool_debug, report_debug) -> bool:
"""Generate a report from previous sleep cycles"""
try:
since, until, fname, fmt, report_debug = prompt_report_arguments(
since, until, fname, fmt, report_debug
)
except KeyboardInterrupt:
sys.exit("\nReport generation cancelled")
try:
app = SleepReport(
since=since,
until=until,
fname=fname,
fmt=fmt,
tool_debug=tool_debug,
report_debug=report_debug,
)
except sqlite3.OperationalError as e:
print(f"Failed to generate report: {e}")
return False
except PermissionError as e:
print(f"Failed to generate report: {e}")
return False
try:
app.run()
except PermissionError as e:
print(f"Failed to generate report: {e}")
return False
except ValueError as e:
print(f"Failed to generate report: {e}")
return False
display_report_file(fname, fmt)
return True
def run_test_cycle(
duration, wait, count, fmt, fname, force, debug, rand, logind, bios_debug
) -> bool:
"""Run a test"""
app = Installer(tool_debug=debug)
app.set_requirements("iasl", "ethtool", "edid-decode")
if not app.install_dependencies():
print("Failed to install dependencies")
return False
try:
duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
total_seconds = (duration + wait) * count
until_time = datetime.now() + timedelta(seconds=total_seconds)
since, until, fname, fmt, report_debug = prompt_report_arguments(
datetime.now().isoformat(), until_time.isoformat(), fname, fmt, True
)
except KeyboardInterrupt:
sys.exit("\nTest cancelled")
try:
app = PrerequisiteValidator(debug)
run = app.run()
except PermissionError as e:
print(f"Failed to run prerequisite check: {e}")
return False
app.report()
if run or force:
app = SleepValidator(tool_debug=debug, bios_debug=bios_debug)
run = app.run(
duration=duration,
wait=wait,
count=count,
rand=rand,
logind=logind,
)
until = datetime.now()
else:
since = None
until = None
app = SleepReport(
since=since,
until=until,
fname=fname,
fmt=fmt,
tool_debug=debug,
report_debug=report_debug,
)
app.run()
# open report in browser if it's html
display_report_file(fname, fmt)
return True
def install(debug) -> None:
"""Install the tool"""
installer = Installer(tool_debug=debug)
installer.set_requirements("iasl", "ethtool", "edid-decode")
if not installer.install_dependencies():
sys.exit("Failed to install dependencies")
try:
app = PrerequisiteValidator(debug)
run = app.run()
except PermissionError as e:
sys.exit(f"Failed to run prerequisite check: {e}")
if not run:
app.report()
sys.exit("Failed to meet prerequisites")
if not installer.install():
sys.exit("Failed to install")
def uninstall(debug) -> None:
"""Uninstall the tool"""
app = Installer(tool_debug=debug)
if not app.remove():
sys.exit("Failed to remove")
def parse_args():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="Swiss army knife for analyzing Linux s2idle problems",
epilog="The tool can run an immediate test with the 'test' command "
"or can be used to hook into systemd for building reports later.\n"
"All optional arguments will be prompted if needed.\n"
"To use non-interactively, please populate all optional arguments.",
)
subparsers = parser.add_subparsers(help="Possible commands", dest="action")
# 'test' command
test_cmd = subparsers.add_parser("test", help="Run amd-s2idle test and report")
test_cmd.add_argument("--count", help=Headers.CountDescription)
test_cmd.add_argument(
"--duration",
help=Headers.DurationDescription,
)
test_cmd.add_argument(
"--wait",
help=Headers.WaitDescription,
)
test_cmd.add_argument(
"--logind", action="store_true", help="Use logind to suspend system"
)
test_cmd.add_argument(
"--random",
action="store_true",
help="Run sleep cycles for random durations and wait, using the "
"--duration and --wait arguments as an upper bound",
)
test_cmd.add_argument(
"--force",
action="store_true",
help="Run suspend test even if prerequisites failed",
)
test_cmd.add_argument(
"--format",
choices=Defaults.format_choices,
help="Report format",
)
test_cmd.add_argument(
"--tool-debug",
action="store_true",
help="Enable tool debug logging",
)
test_cmd.add_argument(
"--bios-debug",
action="store_true",
help="Enable BIOS debug logging instead of notify logging",
)
test_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)
# 'report' command
report_cmd = subparsers.add_parser(
"report", help="Generate amd-s2idle report from previous runs"
)
report_cmd.add_argument(
"--since",
help=Headers.SinceDescription,
)
report_cmd.add_argument(
"--until",
default=Defaults.until.isoformat(),
help=Headers.UntilDescription,
)
report_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)
report_cmd.add_argument(
"--format",
choices=Defaults.format_choices,
help="Report format",
)
report_cmd.add_argument(
"--tool-debug",
action="store_true",
help="Enable tool debug logging",
)
report_cmd.add_argument(
"--report-debug",
action=argparse.BooleanOptionalAction,
help="Include debug messages in report (WARNING: can significantly increase report size)",
)
# if running in a venv, install/uninstall hook options
if sys.prefix != sys.base_prefix:
install_cmd = subparsers.add_parser(
"install", help="Install systemd s2idle hook"
)
uninstall_cmd = subparsers.add_parser(
"uninstall", help="Uninstall systemd s2idle hook"
)
install_cmd.add_argument(
"--tool-debug",
action="store_true",
help="Enable tool debug logging",
)
uninstall_cmd.add_argument(
"--tool-debug",
action="store_true",
help="Enable tool debug logging",
)
parser.add_argument(
"--version", action="store_true", help="Show version information"
)
if len(sys.argv) == 1:
parser.print_help(sys.stderr)
sys.exit(1)
return parser.parse_args()
def main() -> None | int:
"""Main function"""
args = parse_args()
ret = False
if args.action == "install":
relaunch_sudo()
install(args.tool_debug)
elif args.action == "uninstall":
relaunch_sudo()
uninstall(args.tool_debug)
elif args.action == "report":
ret = report(
args.since,
args.until,
args.report_file,
args.format,
args.tool_debug,
args.report_debug,
)
elif args.action == "test":
relaunch_sudo()
ret = run_test_cycle(
args.duration,
args.wait,
args.count,
args.format,
args.report_file,
args.force,
args.tool_debug,
args.random,
args.logind,
args.bios_debug,
)
elif args.version:
print(version())
return
else:
sys.exit("no action specified")
show_log_info()
if ret is False:
return 1
return