amd-debug-tools 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of amd-debug-tools might be problematic. Click here for more details.

amd_debug/s2idle.py ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+ """s2idle analysis tool"""
4
+ import argparse
5
+ import sys
6
+ import os
7
+ import subprocess
8
+ import sqlite3
9
+
10
+ from datetime import date, timedelta, datetime
11
+ from amd_debug.common import is_root, relaunch_sudo, show_log_info, version, running_ssh
12
+
13
+ from amd_debug.validator import SleepValidator
14
+ from amd_debug.installer import Installer
15
+ from amd_debug.prerequisites import PrerequisiteValidator
16
+ from amd_debug.sleep_report import SleepReport
17
+
18
+
19
+ class Defaults:
20
+ """Default values for the script"""
21
+
22
+ duration = 10
23
+ wait = 4
24
+ count = 1
25
+ since = date.today() - timedelta(days=60)
26
+ until = date.today() + timedelta(days=1)
27
+ format_choices = ["txt", "md", "html", "stdout"]
28
+
29
+
30
+ class Headers:
31
+ """Headers for the script"""
32
+
33
+ DurationDescription = "How long should suspend cycles last (seconds)"
34
+ WaitDescription = "How long to wait in between suspend cycles (seconds)"
35
+ CountDescription = "How many suspend cycles to run"
36
+ SinceDescription = "What date to start report data"
37
+ UntilDescription = "What date to end report data"
38
+ LogDescription = "Location of log file"
39
+ ReportFileDescription = "Location of report file"
40
+ FormatDescription = "What format to output the report in"
41
+ MaxDurationDescription = "What is the maximum suspend cycle length (seconds)"
42
+ MaxWaitDescription = "What is the maximum time between suspend cycles (seconds)"
43
+
44
+
45
+ def display_report_file(fname, fmt) -> None:
46
+ """Display report file"""
47
+ if fmt != "html":
48
+ return
49
+ if not is_root():
50
+ subprocess.call(["xdg-open", fname])
51
+ return
52
+ user = os.environ.get("SUDO_USER")
53
+ if user:
54
+ # ensure that xdg tools will know how to display the file (user may need to call tool with sudo -E)
55
+ if os.environ.get("XDG_SESSION_TYPE"):
56
+ subprocess.call(["sudo", "-E", "-u", user, "xdg-open", fname])
57
+ else:
58
+ print(
59
+ f"To display report automatically in browser launch tool with '-E' argument (Example: sudo -E {sys.argv[0]})"
60
+ )
61
+
62
+
63
+ def get_report_file(report_file, extension) -> str:
64
+ """Prompt user for report file"""
65
+ if extension == "stdout":
66
+ return None
67
+ if not report_file:
68
+ return f"amd-s2idle-report-{date.today()}.{extension}"
69
+ return report_file
70
+
71
+
72
+ def get_report_format() -> str:
73
+ """Get report format"""
74
+ if running_ssh():
75
+ return "txt"
76
+ return "html"
77
+
78
+
79
+ def prompt_report_arguments(since, until, fname, fmt) -> str:
80
+ """Prompt user for report configuration"""
81
+ if not since:
82
+ default = Defaults.since
83
+ since = input(f"{Headers.SinceDescription} (default {default})? ")
84
+ if not since:
85
+ since = default.isoformat()
86
+ try:
87
+ since = datetime.fromisoformat(since)
88
+ except ValueError as e:
89
+ sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")
90
+ if not until:
91
+ default = Defaults.until
92
+ until = input(f"{Headers.SinceDescription} (default {default})? ")
93
+ if not until:
94
+ until = default.isoformat()
95
+ try:
96
+ until = datetime.fromisoformat(until)
97
+ except ValueError as e:
98
+ sys.exit(f"Invalid date, use YYYY-MM-DD: {e}")
99
+
100
+ if not fmt:
101
+ fmt = input(f"{Headers.FormatDescription} (default {get_report_format()})? ")
102
+ if not fmt:
103
+ fmt = get_report_format()
104
+ if fmt not in Defaults.format_choices:
105
+ sys.exit(f"Invalid format: {fmt}")
106
+ return [since, until, get_report_file(fname, fmt), fmt]
107
+
108
+
109
+ def prompt_test_arguments(duration, wait, count, rand) -> list:
110
+ """Prompt user for test configuration"""
111
+ if not duration:
112
+ if rand:
113
+ question = Headers.MaxDurationDescription
114
+ else:
115
+ question = Headers.DurationDescription
116
+ duration = input(f"{question} (default {Defaults.duration})? ")
117
+ if not duration:
118
+ duration = Defaults.duration
119
+ try:
120
+ duration = int(duration)
121
+ except ValueError as e:
122
+ sys.exit(f"Invalid duration: {e}")
123
+ if not wait:
124
+ if rand:
125
+ question = Headers.MaxWaitDescription
126
+ else:
127
+ question = Headers.WaitDescription
128
+ wait = input(f"{question} (default {Defaults.wait})? ")
129
+ if not wait:
130
+ wait = Defaults.wait
131
+ try:
132
+ wait = int(wait)
133
+ except ValueError as e:
134
+ sys.exit(f"Invalid wait: {e}")
135
+ if not count:
136
+ count = input(f"{Headers.CountDescription} (default {Defaults.count})? ")
137
+ if not count:
138
+ count = Defaults.count
139
+ try:
140
+ count = int(count)
141
+ except ValueError as e:
142
+ sys.exit(f"Invalid count: {e}")
143
+ return [duration, wait, count]
144
+
145
+
146
+ def report(since, until, fname, fmt, tool_debug, report_debug) -> bool:
147
+ """Generate a report from previous sleep cycles"""
148
+ try:
149
+ since, until, fname, fmt = prompt_report_arguments(since, until, fname, fmt)
150
+ except KeyboardInterrupt:
151
+ sys.exit("\nReport generation cancelled")
152
+ try:
153
+ app = SleepReport(
154
+ since=since,
155
+ until=until,
156
+ fname=fname,
157
+ fmt=fmt,
158
+ tool_debug=tool_debug,
159
+ report_debug=report_debug,
160
+ )
161
+ except sqlite3.OperationalError as e:
162
+ print(f"Failed to generate report: {e}")
163
+ return False
164
+ except PermissionError as e:
165
+ print(f"Failed to generate report: {e}")
166
+ return False
167
+ try:
168
+ app.run()
169
+ except PermissionError as e:
170
+ print(f"Failed to generate report: {e}")
171
+ return False
172
+ except ValueError as e:
173
+ print(f"Failed to generate report: {e}")
174
+ return False
175
+ display_report_file(fname, fmt)
176
+ return True
177
+
178
+
179
+ def test(
180
+ duration, wait, count, fmt, fname, force, debug, rand, logind, bios_debug
181
+ ) -> bool:
182
+ """Run a test"""
183
+ app = Installer(tool_debug=debug)
184
+ app.set_requirements("iasl", "ethtool")
185
+ if not app.install_dependencies():
186
+ print("Failed to install dependencies")
187
+ return False
188
+
189
+ try:
190
+ app = PrerequisiteValidator(debug)
191
+ run = app.run()
192
+ except PermissionError as e:
193
+ print(f"Failed to run prerequisite check: {e}")
194
+ return False
195
+ app.report()
196
+
197
+ if run or force:
198
+ app = SleepValidator(tool_debug=debug, bios_debug=bios_debug)
199
+ try:
200
+ duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
201
+ since, until, fname, fmt = prompt_report_arguments(
202
+ datetime.now().isoformat(), Defaults.until.isoformat(), fname, fmt
203
+ )
204
+ except KeyboardInterrupt:
205
+ sys.exit("\nTest cancelled")
206
+
207
+ app.run(
208
+ duration=duration,
209
+ wait=wait,
210
+ count=count,
211
+ rand=rand,
212
+ logind=logind,
213
+ )
214
+
215
+ app = SleepReport(
216
+ since=since,
217
+ until=until,
218
+ fname=fname,
219
+ fmt=fmt,
220
+ tool_debug=debug,
221
+ report_debug=True,
222
+ )
223
+ app.run()
224
+
225
+ # open report in browser if it's html
226
+ display_report_file(fname, fmt)
227
+
228
+ return True
229
+ return False
230
+
231
+
232
+ def install(debug) -> None:
233
+ """Install the tool"""
234
+ installer = Installer(tool_debug=debug)
235
+ installer.set_requirements("iasl", "ethtool")
236
+ if not installer.install_dependencies():
237
+ sys.exit("Failed to install dependencies")
238
+ try:
239
+ app = PrerequisiteValidator(debug)
240
+ run = app.run()
241
+ except PermissionError as e:
242
+ sys.exit(f"Failed to run prerequisite check: {e}")
243
+ if not run:
244
+ app.report()
245
+ sys.exit("Failed to meet prerequisites")
246
+
247
+ if not installer.install():
248
+ sys.exit("Failed to install")
249
+
250
+
251
+ def uninstall(debug) -> None:
252
+ """Uninstall the tool"""
253
+ app = Installer(tool_debug=debug)
254
+ if not app.remove():
255
+ sys.exit("Failed to remove")
256
+
257
+
258
+ def parse_args():
259
+ """Parse command line arguments"""
260
+ parser = argparse.ArgumentParser(
261
+ description="Swiss army knife for analyzing Linux s2idle problems",
262
+ 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"
263
+ "All optional arguments will be prompted if needed.\n"
264
+ "To use non-interactively, please populate all optional arguments.",
265
+ )
266
+ subparsers = parser.add_subparsers(help="Possible commands", dest="action")
267
+
268
+ # 'test' command
269
+ test_cmd = subparsers.add_parser("test", help="Run amd-s2idle test and report")
270
+ test_cmd.add_argument("--count", help=Headers.CountDescription)
271
+ test_cmd.add_argument(
272
+ "--duration",
273
+ help=Headers.DurationDescription,
274
+ )
275
+ test_cmd.add_argument(
276
+ "--wait",
277
+ help=Headers.WaitDescription,
278
+ )
279
+ test_cmd.add_argument(
280
+ "--logind", action="store_true", help="Use logind to suspend system"
281
+ )
282
+ test_cmd.add_argument(
283
+ "--random",
284
+ action="store_true",
285
+ help="Run sleep cycles for random durations and wait, using the --duration and --wait arguments as an upper bound",
286
+ )
287
+ test_cmd.add_argument(
288
+ "--force",
289
+ action="store_true",
290
+ help="Run suspend test even if prerequisites failed",
291
+ )
292
+ test_cmd.add_argument(
293
+ "--format",
294
+ choices=Defaults.format_choices,
295
+ help="Report format",
296
+ )
297
+ test_cmd.add_argument(
298
+ "--tool-debug",
299
+ action="store_true",
300
+ help="Enable tool debug logging",
301
+ )
302
+ test_cmd.add_argument(
303
+ "--bios-debug",
304
+ action="store_true",
305
+ help="Enable BIOS debug logging instead of notify logging",
306
+ )
307
+ test_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)
308
+
309
+ # 'report' command
310
+ report_cmd = subparsers.add_parser(
311
+ "report", help="Generate amd-s2idle report from previous runs"
312
+ )
313
+ report_cmd.add_argument(
314
+ "--since",
315
+ help=Headers.SinceDescription,
316
+ )
317
+ report_cmd.add_argument(
318
+ "--until",
319
+ default=Defaults.until.isoformat(),
320
+ help=Headers.UntilDescription,
321
+ )
322
+ report_cmd.add_argument("--report-file", help=Headers.ReportFileDescription)
323
+ report_cmd.add_argument(
324
+ "--format",
325
+ choices=Defaults.format_choices,
326
+ help="Report format",
327
+ )
328
+ report_cmd.add_argument(
329
+ "--tool-debug",
330
+ action="store_true",
331
+ help="Enable tool debug logging",
332
+ )
333
+ report_cmd.add_argument(
334
+ "--report-debug",
335
+ action="store_true",
336
+ help="Include debug messages in report (WARNING: can significantly increase report size)",
337
+ )
338
+
339
+ # if running in a venv, install/uninstall hook options
340
+ if sys.prefix != sys.base_prefix:
341
+ install_cmd = subparsers.add_parser(
342
+ "install", help="Install systemd s2idle hook"
343
+ )
344
+ uninstall_cmd = subparsers.add_parser(
345
+ "uninstall", help="Uninstall systemd s2idle hook"
346
+ )
347
+ install_cmd.add_argument(
348
+ "--tool-debug",
349
+ action="store_true",
350
+ help="Enable tool debug logging",
351
+ )
352
+ uninstall_cmd.add_argument(
353
+ "--tool-debug",
354
+ action="store_true",
355
+ help="Enable tool debug logging",
356
+ )
357
+
358
+ subparsers.add_parser("version", help="Show version information")
359
+
360
+ if len(sys.argv) == 1:
361
+ parser.print_help(sys.stderr)
362
+ sys.exit(1)
363
+
364
+ return parser.parse_args()
365
+
366
+
367
+ def main():
368
+ """Main function"""
369
+ args = parse_args()
370
+ ret = False
371
+ if args.action == "install":
372
+ relaunch_sudo()
373
+ install(args.tool_debug)
374
+ elif args.action == "uninstall":
375
+ relaunch_sudo()
376
+ uninstall(args.tool_debug)
377
+ elif args.action == "report":
378
+ ret = report(
379
+ args.since,
380
+ args.until,
381
+ args.report_file,
382
+ args.format,
383
+ args.tool_debug,
384
+ args.report_debug,
385
+ )
386
+ elif args.action == "test":
387
+ relaunch_sudo()
388
+ ret = test(
389
+ args.duration,
390
+ args.wait,
391
+ args.count,
392
+ args.format,
393
+ args.report_file,
394
+ args.force,
395
+ args.tool_debug,
396
+ args.random,
397
+ args.logind,
398
+ args.bios_debug,
399
+ )
400
+ elif args.action == "version":
401
+ print(version())
402
+ return True
403
+ else:
404
+ sys.exit("no action specified")
405
+ show_log_info()
406
+ return ret