redbot/core/_debuginfo.py
from __future__ import annotations
import getpass
import os
import platform
import sys
from typing import Optional
import discord
import pip
import psutil
from redbot import __version__
from redbot.core import data_manager
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box
def noop_box(text: str, **kwargs) -> str:
return text
def _datasize(num: int):
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
if abs(num) < 1024.0:
return "{0:.1f}{1}".format(num, unit)
num /= 1024.0
return "{0:.1f}{1}".format(num, "YB")
class DebugInfoSection:
def __init__(self, section_name: str, *section_parts: str) -> None:
self.section_name = section_name
self.section_parts = section_parts
def get_command_text(self) -> str:
parts = [box(f"## {self.section_name}:", lang="md")]
for part in self.section_parts:
parts.append(box(part))
return "".join(parts)
def get_cli_text(self) -> str:
parts = [f"\x1b[32m## {self.section_name}:\x1b[0m"]
for part in self.section_parts:
parts.append(part)
return "\n".join(parts)
class DebugInfo:
def __init__(self, bot: Optional[Red] = None) -> None:
self.bot = bot
@property
def is_logged_in(self) -> bool:
return self.bot is not None and self.bot.application_id is not None
@property
def is_connected(self) -> bool:
return self.bot is not None and self.bot.is_ready()
async def get_cli_text(self) -> str:
parts = ["\x1b[31m# Debug Info for Red:\x1b[0m"]
for section in (
self._get_system_metadata_section(),
self._get_os_variables_section(),
await self._get_red_vars_section(),
):
parts.append("")
parts.append(section.get_cli_text())
return "\n".join(parts)
async def get_command_text(self) -> str:
parts = [box("# Debug Info for Red:", lang="md")]
for section in (
self._get_system_metadata_section(),
self._get_os_variables_section(),
await self._get_red_vars_section(),
):
parts.append("\n")
parts.append(section.get_command_text())
return "".join(parts)
def _get_system_metadata_section(self) -> DebugInfoSection:
memory_ram = psutil.virtual_memory()
ram_string = "{used}/{total} ({percent}%)".format(
used=_datasize(memory_ram.used),
total=_datasize(memory_ram.total),
percent=memory_ram.percent,
)
return DebugInfoSection(
"System Metadata",
f"CPU Cores: {psutil.cpu_count()} ({platform.machine()})\nRAM: {ram_string}",
)
def _get_os_variables_section(self) -> DebugInfoSection:
IS_WINDOWS = os.name == "nt"
IS_MAC = sys.platform == "darwin"
IS_LINUX = sys.platform == "linux"
python_version = ".".join(map(str, sys.version_info[:3]))
pyver = f"{python_version} ({platform.architecture()[0]})"
pipver = pip.__version__
redver = __version__
dpy_version = discord.__version__
if IS_WINDOWS:
os_info = platform.uname()
osver = f"{os_info.system} {os_info.release} (version {os_info.version})"
elif IS_MAC:
os_info = platform.mac_ver()
osver = f"Mac OSX {os_info[0]} {os_info[2]}"
elif IS_LINUX:
import distro
osver = f"{distro.name()} {distro.version()}".strip()
else:
osver = "Could not parse OS, report this on Github."
user_who_ran = getpass.getuser()
resp_os = f"OS version: {osver}\nUser: {user_who_ran}\n" # Ran where off to?!
resp_py_metadata = (
f"Python executable: {sys.executable}\n"
f"Python version: {pyver}\n"
f"Pip version: {pipver}\n"
)
resp_red_metadata = f"Red version: {redver}\nDiscord.py version: {dpy_version}"
return DebugInfoSection(
"OS variables",
resp_os,
resp_py_metadata,
resp_red_metadata,
)
async def _get_red_vars_section(self) -> DebugInfoSection:
instance_name = data_manager.instance_name()
if instance_name is None:
return DebugInfoSection(
"Red variables",
f"Metadata file: {data_manager.config_file}",
)
parts = [f"Instance name: {instance_name}"]
if self.bot is not None:
# sys.original_argv is available since 3.10 and shows the actual command line arguments
# rather than a Python-transformed version (i.e. with '-c' or path to `__main__.py`
# as first element). We could just not show the first argument for consistency
# but it can be useful.
cli_args = getattr(sys, "orig_argv", sys.argv).copy()
# best effort attempt to expunge a token argument
for idx, arg in enumerate(cli_args):
if not arg.startswith("--to"):
continue
arg_name, sep, arg_value = arg.partition("=")
if arg_name not in ("--to", "--tok", "--toke", "--token"):
continue
if sep:
cli_args[idx] = f"{arg_name}{sep}[EXPUNGED]"
elif len(cli_args) > idx + 1:
cli_args[idx + 1] = f"[EXPUNGED]"
parts.append(f"Command line arguments: {cli_args!r}")
# This formatting is a bit ugly but this is a debug information command
# and calling repr() on prefix strings ensures that the list isn't ambiguous.
prefixes = ", ".join(map(repr, await self.bot._config.prefix()))
parts.append(f"Global prefix(es): {prefixes}")
if self.is_logged_in:
owners = []
for uid in self.bot.owner_ids:
try:
u = await self.bot.get_or_fetch_user(uid)
owners.append(f"{u.id} ({u})")
except discord.HTTPException:
owners.append(f"{uid} (Unresolvable)")
owners_string = ", ".join(owners) or "None"
parts.append(f"Owner(s): {', '.join(owners) or 'None'}")
if self.is_connected:
disabled_intents = (
", ".join(
intent_name.replace("_", " ").title()
for intent_name, enabled in self.bot.intents
if not enabled
)
or "None"
)
parts.append(f"Disabled intents: {disabled_intents}")
parts.append(f"Storage type: {data_manager.storage_type()}")
parts.append(f"Data path: {data_manager.basic_config['DATA_PATH']}")
parts.append(f"Metadata file: {data_manager.config_file}")
return DebugInfoSection(
"Red variables",
"\n".join(parts),
)