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/common.py ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ This module contains common utility functions and classes for various amd-debug-tools.
6
+ """
7
+
8
+ import importlib.metadata
9
+ import logging
10
+ import os
11
+ import platform
12
+ import time
13
+ import struct
14
+ import subprocess
15
+ import re
16
+ import sys
17
+ from datetime import date, timedelta
18
+
19
+
20
+ class Colors:
21
+ """Colors for the terminal"""
22
+
23
+ DEBUG = "\033[90m"
24
+ HEADER = "\033[95m"
25
+ OK = "\033[94m"
26
+ WARNING = "\033[32m"
27
+ FAIL = "\033[91m"
28
+ ENDC = "\033[0m"
29
+ UNDERLINE = "\033[4m"
30
+
31
+
32
+ def read_file(fn) -> str:
33
+ """Read a file and return the contents"""
34
+ with open(fn, "r", encoding="utf-8") as r:
35
+ return r.read().strip()
36
+
37
+
38
+ def compare_file(fn, expect) -> bool:
39
+ """Compare a file to an expected string"""
40
+ return read_file(fn) == expect
41
+
42
+
43
+ def get_group_color(group) -> str:
44
+ """Get the color for a group"""
45
+ if group == "🚦":
46
+ color = Colors.WARNING
47
+ elif group == "🗣️":
48
+ color = Colors.HEADER
49
+ elif group == "💯":
50
+ color = Colors.UNDERLINE
51
+ elif any(mk in group for mk in ["🦟", "🖴"]):
52
+ color = Colors.DEBUG
53
+ elif any(mk in group for mk in ["❌", "👀"]):
54
+ color = Colors.FAIL
55
+ elif any(mk in group for mk in ["✅", "🔋", "🐧", "💻", "○", "💤", "🥱"]):
56
+ color = Colors.OK
57
+ else:
58
+ color = group
59
+ return color
60
+
61
+
62
+ def print_color(message, group) -> None:
63
+ """Print a message with a color"""
64
+ prefix = f"{group} "
65
+ suffix = Colors.ENDC
66
+ color = get_group_color(group)
67
+ if color == group:
68
+ prefix = ""
69
+ log_txt = f"{prefix}{message}".strip()
70
+ if any(c in color for c in [Colors.OK, Colors.HEADER, Colors.UNDERLINE]):
71
+ logging.info(log_txt)
72
+ elif color == Colors.WARNING:
73
+ logging.warning(log_txt)
74
+ elif color == Colors.FAIL:
75
+ logging.error(log_txt)
76
+ else:
77
+ logging.debug(log_txt)
78
+ if "TERM" in os.environ and os.environ["TERM"] == "dumb":
79
+ suffix = ""
80
+ color = ""
81
+ print(f"{prefix}{color}{message}{suffix}")
82
+
83
+
84
+ def fatal_error(message):
85
+ """Prints a fatal error message and exits"""
86
+ _configure_log(None)
87
+ print_color(message, "👀")
88
+ sys.exit(1)
89
+
90
+
91
+ def show_log_info():
92
+ """Show log information"""
93
+ logger = logging.getLogger()
94
+ for handler in logger.handlers:
95
+ if isinstance(handler, logging.FileHandler):
96
+ filename = handler.baseFilename
97
+ if filename != "/dev/null":
98
+ print(f"Debug logs are saved to: {filename}")
99
+
100
+
101
+ def _configure_log(prefix) -> str:
102
+ """Configure logging for the tool"""
103
+ if len(logging.root.handlers) > 0:
104
+ return
105
+
106
+ if prefix:
107
+ user = os.environ.get("SUDO_USER")
108
+ home = os.path.expanduser(f"~{user if user else ''}")
109
+ path = os.environ.get("XDG_DATA_HOME") or os.path.join(
110
+ home, ".local", "share", "amd-debug-tools"
111
+ )
112
+ os.makedirs(path, exist_ok=True)
113
+ log = os.path.join(
114
+ path,
115
+ f"{prefix}-{date.today()}.txt",
116
+ )
117
+ if not os.path.exists(log):
118
+ with open(log, "w", encoding="utf-8") as f:
119
+ f.write("")
120
+ if "SUDO_UID" in os.environ:
121
+ os.chown(path, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]))
122
+ os.chown(log, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]))
123
+ level = logging.DEBUG
124
+ else:
125
+ log = "/dev/null"
126
+ level = logging.WARNING
127
+ # for saving a log file for analysis
128
+ logging.basicConfig(
129
+ format="%(asctime)s %(levelname)s:\t%(message)s",
130
+ filename=log,
131
+ level=level,
132
+ )
133
+ return log
134
+
135
+
136
+ def check_lockdown():
137
+ """Check if the system is in lockdown"""
138
+ fn = os.path.join("/", "sys", "kernel", "security", "lockdown")
139
+ if not os.path.exists(fn):
140
+ return False
141
+ lockdown = read_file(fn)
142
+ if lockdown.split()[0] != "[none]":
143
+ return lockdown
144
+ return False
145
+
146
+
147
+ def print_temporary_message(msg) -> int:
148
+ """Print a temporary message to the console"""
149
+ print(msg, end="\r", flush=True)
150
+ return len(msg)
151
+
152
+
153
+ def clear_temporary_message(msg_len) -> None:
154
+ """Clear a temporary message from the console"""
155
+ print(" " * msg_len, end="\r")
156
+
157
+
158
+ def run_countdown(action, t) -> bool:
159
+ """Run a countdown timer"""
160
+ msg = ""
161
+ if t < 0:
162
+ return False
163
+ if t == 0:
164
+ return True
165
+ while t > 0:
166
+ msg = f"{action} in {timedelta(seconds=t)}"
167
+ print_temporary_message(msg)
168
+ time.sleep(1)
169
+ t -= 1
170
+ clear_temporary_message(len(msg))
171
+ return True
172
+
173
+
174
+ def get_distro() -> str:
175
+ """Get the distribution name"""
176
+ distro = "unknown"
177
+ if os.path.exists("/etc/os-release"):
178
+ with open("/etc/os-release", "r", encoding="utf-8") as f:
179
+ for line in f:
180
+ if line.startswith("ID="):
181
+ return line.split("=")[1].strip().strip('"')
182
+ if os.path.exists("/etc/arch-release"):
183
+ return "arch"
184
+ elif os.path.exists("/etc/fedora-release"):
185
+ return "fedora"
186
+ elif os.path.exists("/etc/debian_version"):
187
+ return "debian"
188
+
189
+ return distro
190
+
191
+
192
+ def get_pretty_distro() -> str:
193
+ """Get the pretty distribution name"""
194
+ distro = "Unknown"
195
+ if os.path.exists("/etc/os-release"):
196
+ with open("/etc/os-release", "r", encoding="utf-8") as f:
197
+ for line in f:
198
+ if line.startswith("PRETTY_NAME="):
199
+ distro = line.split("=")[1].strip().strip('"')
200
+ break
201
+ return distro
202
+
203
+
204
+ def is_root() -> bool:
205
+ """Check if the user is root"""
206
+ return os.geteuid() == 0
207
+
208
+
209
+ def BIT(num): # pylint: disable=invalid-name
210
+ """Return a bit shifted value"""
211
+ return 1 << num
212
+
213
+
214
+ def get_log_priority(num):
215
+ """Maps an integer debug level to a priority type"""
216
+ if num:
217
+ try:
218
+ num = int(num)
219
+ except ValueError:
220
+ return num
221
+ if num == 7:
222
+ return "🦟"
223
+ elif num == 4:
224
+ return "🚦"
225
+ elif num <= 3:
226
+ return "❌"
227
+ return "○"
228
+
229
+
230
+ def minimum_kernel(major, minor) -> bool:
231
+ """Checks if the kernel version is at least major.minor"""
232
+ ver = platform.uname().release.split(".")
233
+ kmajor = int(ver[0])
234
+ kminor = int(ver[1])
235
+ if kmajor > int(major):
236
+ return True
237
+ if kmajor < int(major):
238
+ return False
239
+ return kminor >= int(minor)
240
+
241
+
242
+ def systemd_in_use() -> bool:
243
+ """Check if systemd is in use"""
244
+ # Check if /proc/1/comm exists and read its contents
245
+ init_daemon = read_file("/proc/1/comm")
246
+ return init_daemon == "systemd"
247
+
248
+
249
+ def get_property_pyudev(properties, key, fallback=""):
250
+ """Get a property from a udev device"""
251
+ try:
252
+ return properties.get(key, fallback)
253
+ except UnicodeDecodeError:
254
+ return ""
255
+
256
+
257
+ def read_msr(msr, cpu):
258
+ """Read a Model-Specific Register (MSR) value from the CPU."""
259
+ p = f"/dev/cpu/{cpu}/msr"
260
+ if not os.path.exists(p) and is_root():
261
+ os.system("modprobe msr")
262
+ try:
263
+ f = os.open(p, os.O_RDONLY)
264
+ except OSError as exc:
265
+ raise PermissionError from exc
266
+ try:
267
+ os.lseek(f, msr, os.SEEK_SET)
268
+ val = struct.unpack("Q", os.read(f, 8))[0]
269
+ except OSError as exc:
270
+ raise PermissionError from exc
271
+ finally:
272
+ os.close(f)
273
+ return val
274
+
275
+
276
+ def relaunch_sudo() -> None:
277
+ """Relaunch the script with sudo if not already running as root"""
278
+ if not is_root():
279
+ logging.debug("Relaunching with sudo")
280
+ os.execvp("sudo", ["sudo", "-E"] + sys.argv)
281
+
282
+
283
+ def running_ssh():
284
+ return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ
285
+
286
+
287
+ def _git_describe() -> str:
288
+ """Get the git description of the current commit"""
289
+ try:
290
+ result = subprocess.check_output(
291
+ ["git", "log", "-1", '--format=commit %h ("%s")'],
292
+ cwd=os.path.dirname(__file__),
293
+ text=True,
294
+ stderr=subprocess.DEVNULL,
295
+ )
296
+ return result.strip()
297
+ except subprocess.CalledProcessError:
298
+ return None
299
+ except FileNotFoundError:
300
+ return None
301
+
302
+
303
+ def version() -> str:
304
+ """Get version of the tool"""
305
+ ver = "unknown"
306
+ try:
307
+ ver = importlib.metadata.version("amd-debug-tools")
308
+ except importlib.metadata.PackageNotFoundError:
309
+ pass
310
+ describe = _git_describe()
311
+ if describe:
312
+ ver = f"{ver} [{describe}]"
313
+ return ver
314
+
315
+
316
+ class AmdTool:
317
+ """Base class for AMD tools"""
318
+
319
+ def __init__(self, prefix):
320
+ self.log = _configure_log(prefix)
321
+ logging.debug("command: %s (module: %s)", sys.argv, type(self).__name__)
322
+ logging.debug("Version: %s", version())
323
+ if os.uname().sysname != "Linux":
324
+ raise RuntimeError("This tool only runs on Linux")
amd_debug/database.py ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from datetime import datetime
5
+ import sqlite3
6
+ import os
7
+
8
+ from amd_debug.common import read_file
9
+
10
+ SCHEMA_VERSION = 1
11
+
12
+
13
+ def migrate(cur, user_version) -> None:
14
+ """Migrate sqlite database schema"""
15
+ cur.execute("PRAGMA user_version")
16
+ val = cur.fetchone()[0]
17
+ # Schema 1
18
+ # - add priority column
19
+ if val == 0:
20
+ cur.execute("ALTER TABLE debug ADD COLUMN priority INTEGER")
21
+ # Update schema if necessary
22
+ if val != user_version:
23
+ cur.execute(f"PRAGMA user_version = {user_version}")
24
+
25
+
26
+ class SleepDatabase:
27
+ """Database class to store sleep cycle data"""
28
+
29
+ def __init__(self, dbf=None) -> None:
30
+ self.db = None
31
+ self.last_suspend = None
32
+ self.cycle_data_cnt = 0
33
+ self.debug_cnt = 0
34
+
35
+ if not dbf:
36
+ # if we were packaged we would have a directory in /var/lib
37
+ path = os.path.join("/", "var", "lib", "amd-s2idle")
38
+ if not os.path.exists(path):
39
+ path = os.path.join("/", "var", "local", "lib", "amd-s2idle")
40
+ os.makedirs(path, exist_ok=True)
41
+
42
+ dbf = os.path.join(path, "data.db")
43
+ new = not os.path.exists(dbf)
44
+ self.db = sqlite3.connect(dbf)
45
+ cur = self.db.cursor()
46
+ cur.execute(
47
+ "CREATE TABLE IF NOT EXISTS prereq_data ("
48
+ "t0 INTEGER,"
49
+ "id INTEGER,"
50
+ "message TEXT,"
51
+ "symbol TEXT,"
52
+ "PRIMARY KEY(t0, id))"
53
+ )
54
+ cur.execute(
55
+ "CREATE TABLE IF NOT EXISTS debug ("
56
+ "t0 INTEGER,"
57
+ "id INTEGER,"
58
+ "message TEXT,"
59
+ "priority INTEGER,"
60
+ "PRIMARY KEY(t0, id))"
61
+ )
62
+ cur.execute(
63
+ "CREATE TABLE IF NOT EXISTS cycle ("
64
+ "t0 INTEGER PRIMARY KEY,"
65
+ "t1 INTEGER,"
66
+ "requested INTEGER,"
67
+ "gpio TEXT,"
68
+ "wake_irq TEXT,"
69
+ "kernel REAL,"
70
+ "hw REAL)"
71
+ )
72
+ cur.execute(
73
+ "CREATE TABLE IF NOT EXISTS cycle_data ("
74
+ "t0 INTEGER,"
75
+ "id INTEGER,"
76
+ "message TEXT,"
77
+ "symbol TEXT,"
78
+ "PRIMARY KEY(t0, id))"
79
+ )
80
+ cur.execute(
81
+ "CREATE TABLE IF NOT EXISTS battery ("
82
+ "t0 INTEGER PRIMARY KEY,"
83
+ "name TEXT,"
84
+ "b0 INTEGER,"
85
+ "b1 INTEGER,"
86
+ "full INTEGER,"
87
+ "unit TEXT)"
88
+ )
89
+ self.prereq_data_cnt = 0
90
+
91
+ if new:
92
+ cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
93
+ else:
94
+ migrate(cur, SCHEMA_VERSION)
95
+
96
+ def __del__(self) -> None:
97
+ if self.db:
98
+ self.db.close()
99
+
100
+ def start_cycle(self, timestamp):
101
+ """Start a new sleep cycle"""
102
+ self.last_suspend = timestamp
103
+
104
+ # increment the counters so that systemd hooks work
105
+ cur = self.db.cursor()
106
+ cur.execute(
107
+ "SELECT MAX(id) FROM cycle_data WHERE t0=?",
108
+ (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
109
+ )
110
+ val = cur.fetchone()[0]
111
+ if val is not None:
112
+ self.cycle_data_cnt = val + 1
113
+ else:
114
+ self.cycle_data_cnt = 0
115
+ cur.execute(
116
+ "SELECT MAX(id) FROM debug WHERE t0=?",
117
+ (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
118
+ )
119
+ val = cur.fetchone()[0]
120
+ if val is not None:
121
+ self.debug_cnt = val + 1
122
+ else:
123
+ self.debug_cnt = 0
124
+
125
+ def sync(self) -> None:
126
+ """Sync the database to disk"""
127
+ self.db.commit()
128
+
129
+ def record_debug(self, message, level=6) -> None:
130
+ """Helper function to record a message to debug database"""
131
+ assert self.last_suspend
132
+ cur = self.db.cursor()
133
+ cur.execute(
134
+ "INSERT into debug (t0, id, message, priority) VALUES (?, ?, ?, ?)",
135
+ (
136
+ int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
137
+ self.debug_cnt,
138
+ message,
139
+ level,
140
+ ),
141
+ )
142
+ self.debug_cnt += 1
143
+
144
+ def record_debug_file(self, fn):
145
+ """Helper function to record the entire contents of a file to debug database"""
146
+ try:
147
+ contents = read_file(fn)
148
+ self.record_debug(contents)
149
+ except PermissionError:
150
+ self.record_debug(f"Unable to capture {fn}")
151
+
152
+ def record_battery_energy(self, name, energy, full, unit):
153
+ """Helper function to record battery energy"""
154
+ cur = self.db.cursor()
155
+ cur.execute(
156
+ "SELECT * FROM battery WHERE t0=?",
157
+ (int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
158
+ )
159
+ if cur.fetchone():
160
+ cur.execute(
161
+ "UPDATE battery SET b1=? WHERE t0=?",
162
+ (energy, int(self.last_suspend.strftime("%Y%m%d%H%M%S"))),
163
+ )
164
+ else:
165
+ cur.execute(
166
+ """
167
+ INSERT into battery (t0, name, b0, b1, full, unit)
168
+ VALUES (?, ?, ?, ?, ?, ?)
169
+ """,
170
+ (
171
+ int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
172
+ name,
173
+ energy,
174
+ None,
175
+ full,
176
+ unit,
177
+ ),
178
+ )
179
+
180
+ def record_cycle_data(self, message, symbol) -> None:
181
+ """Helper function to record a message to cycle_data database"""
182
+ assert self.last_suspend
183
+ cur = self.db.cursor()
184
+ cur.execute(
185
+ """
186
+ INSERT into cycle_data (t0, id, message, symbol)
187
+ VALUES (?, ?, ?, ?)
188
+ """,
189
+ (
190
+ (
191
+ int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
192
+ self.cycle_data_cnt,
193
+ message,
194
+ symbol,
195
+ )
196
+ ),
197
+ )
198
+ self.cycle_data_cnt += 1
199
+
200
+ def record_cycle(
201
+ self,
202
+ requested_duration=0,
203
+ active_gpios="",
204
+ wakeup_irqs="",
205
+ kernel_duration=0,
206
+ hw_sleep_duration=0,
207
+ ) -> None:
208
+ """Helper function to record a sleep cycle into the cycle database"""
209
+ assert self.last_suspend
210
+ cur = self.db.cursor()
211
+ cur.execute(
212
+ """
213
+ REPLACE INTO cycle (t0, t1, requested, gpio, wake_irq, kernel, hw)
214
+ VALUES (?, ?, ?, ?, ?, ?, ?)
215
+ """,
216
+ (
217
+ int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
218
+ int(datetime.now().strftime("%Y%m%d%H%M%S")),
219
+ requested_duration,
220
+ str(active_gpios) if active_gpios else "",
221
+ str(wakeup_irqs),
222
+ kernel_duration,
223
+ hw_sleep_duration,
224
+ ),
225
+ )
226
+
227
+ def record_prereq(self, message, symbol) -> None:
228
+ """Helper function to record a message to prereq_data database"""
229
+ assert self.last_suspend
230
+ cur = self.db.cursor()
231
+ cur.execute(
232
+ """
233
+ INSERT into prereq_data (t0, id, message, symbol)
234
+ VALUES (?, ?, ?, ?)
235
+ """,
236
+ (
237
+ (
238
+ int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
239
+ self.prereq_data_cnt,
240
+ message,
241
+ symbol,
242
+ )
243
+ ),
244
+ )
245
+ self.prereq_data_cnt += 1
246
+
247
+ def report_prereq(self, t0) -> list:
248
+ """Helper function to report the prereq_data database"""
249
+ if t0 is None:
250
+ return []
251
+ cur = self.db.cursor()
252
+ cur.execute(
253
+ "SELECT * FROM prereq_data WHERE t0=?",
254
+ (int(t0.strftime("%Y%m%d%H%M%S")),),
255
+ )
256
+ return cur.fetchall()
257
+
258
+ def report_debug(self, t0) -> str:
259
+ """Helper function to report the debug database"""
260
+ if t0 is None:
261
+ return ""
262
+ cur = self.db.cursor()
263
+ cur.execute(
264
+ "SELECT message, priority FROM debug WHERE t0=?",
265
+ (int(t0.strftime("%Y%m%d%H%M%S")),),
266
+ )
267
+ return cur.fetchall()
268
+
269
+ def report_cycle(self, t0=None) -> list:
270
+ """Helper function to report a cycle from database"""
271
+ if t0 is None:
272
+ assert self.last_suspend
273
+ t0 = self.last_suspend
274
+ cur = self.db.cursor()
275
+ cur.execute(
276
+ "SELECT * FROM cycle WHERE t0=?",
277
+ (int(t0.strftime("%Y%m%d%H%M%S")),),
278
+ )
279
+ return cur.fetchall()
280
+
281
+ def report_cycle_data(self, t0=None) -> str:
282
+ """Helper function to report a table matching a timestamp from cycle_data database"""
283
+ if t0 is None:
284
+ t0 = self.last_suspend
285
+ cur = self.db.cursor()
286
+ cur.execute(
287
+ "SELECT message, symbol FROM cycle_data WHERE t0=? ORDER BY symbol",
288
+ (int(t0.strftime("%Y%m%d%H%M%S")),),
289
+ )
290
+ data = ""
291
+ for row in cur.fetchall():
292
+ data += f"{row[1]} {row[0]}\n"
293
+ return data
294
+
295
+ def report_battery(self, t0=None) -> list:
296
+ """Helper function to report a line from battery database"""
297
+ if t0 is None:
298
+ t0 = self.last_suspend
299
+ cur = self.db.cursor()
300
+ cur.execute(
301
+ "SELECT * FROM battery WHERE t0=?",
302
+ (int(t0.strftime("%Y%m%d%H%M%S")),),
303
+ )
304
+ return cur.fetchall()
305
+
306
+ def get_last_prereq_ts(self) -> int:
307
+ """Helper function to report the last line from prereq database"""
308
+ cur = self.db.cursor()
309
+ cur.execute("SELECT * FROM prereq_data ORDER BY t0 DESC LIMIT 1")
310
+ result = cur.fetchone()
311
+ return result[0] if result else None
312
+
313
+ def get_last_cycle(self) -> list:
314
+ """Helper function to report the last line from battery database"""
315
+ cur = self.db.cursor()
316
+ cur.execute("SELECT t0 FROM cycle ORDER BY t0 DESC LIMIT 1")
317
+ return cur.fetchone()
318
+
319
+ def report_summary_dataframe(self, since, until) -> object:
320
+ """Helper function to report a dataframe from the database"""
321
+ import pandas as pd # pylint: disable=import-outside-toplevel
322
+
323
+ pd.set_option("display.precision", 2)
324
+ return pd.read_sql_query(
325
+ sql="SELECT cycle.t0, cycle.t1, hw, requested, gpio, wake_irq, b0, b1, full FROM cycle LEFT JOIN battery ON cycle.t0 = battery.t0 WHERE cycle.t0 >= ? and cycle.t0 <= ?",
326
+ con=self.db,
327
+ params=(
328
+ int(since.strftime("%Y%m%d%H%M%S")),
329
+ int(until.strftime("%Y%m%d%H%M%S")),
330
+ ),
331
+ )