Use different exit codes for critical errors vs configuration errors (#5674)

Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com>
This commit is contained in:
Jakub Kuczys 2022-12-25 22:27:07 +01:00 committed by GitHub
parent 0e58897bfc
commit e8c044a9bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 60 additions and 44 deletions

View File

@ -52,9 +52,10 @@ Paste the following in the file, and replace all instances of :code:`username` w
User=username User=username
Group=username Group=username
Type=idle Type=idle
Restart=always Restart=on-abnormal
RestartSec=15 RestartSec=15
RestartPreventExitStatus=0 RestartForceExitStatus=1
RestartForceExitStatus=26
TimeoutStopSec=10 TimeoutStopSec=10
[Install] [Install]

View File

@ -27,7 +27,7 @@ if _sys.version_info < MIN_PYTHON_VERSION:
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have " f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
f"{_sys.version}! Please update Python." f"{_sys.version}! Please update Python."
) )
_sys.exit(1) _sys.exit(78)
class VersionInfo: class VersionInfo:

View File

@ -53,13 +53,13 @@ def list_instances():
"No instances have been configured! Configure one " "No instances have been configured! Configure one "
"using `redbot-setup` before trying to run the bot!" "using `redbot-setup` before trying to run the bot!"
) )
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
else: else:
text = "Configured Instances:\n\n" text = "Configured Instances:\n\n"
for instance_name in _get_instance_names(): for instance_name in _get_instance_names():
text += "{}\n".format(instance_name) text += "{}\n".format(instance_name)
print(text) print(text)
sys.exit(0) sys.exit(ExitCodes.SHUTDOWN)
async def debug_info(*args: Any) -> None: async def debug_info(*args: Any) -> None:
@ -80,10 +80,10 @@ async def edit_instance(red, cli_flags):
if data_path is None and copy_data: if data_path is None and copy_data:
print("--copy-data can't be used without --edit-data-path argument") print("--copy-data can't be used without --edit-data-path argument")
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
if new_name is None and confirm_overwrite: if new_name is None and confirm_overwrite:
print("--overwrite-existing-instance can't be used without --edit-instance-name argument") print("--overwrite-existing-instance can't be used without --edit-instance-name argument")
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
if ( if (
no_prompt no_prompt
and all(to_change is None for to_change in (token, owner, new_name, data_path)) and all(to_change is None for to_change in (token, owner, new_name, data_path))
@ -94,7 +94,7 @@ async def edit_instance(red, cli_flags):
" Available arguments (check help for more information):" " Available arguments (check help for more information):"
" --edit-instance-name, --edit-data-path, --copy-data, --owner, --token, --prefix" " --edit-instance-name, --edit-data-path, --copy-data, --owner, --token, --prefix"
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
await _edit_token(red, token, no_prompt) await _edit_token(red, token, no_prompt)
await _edit_prefix(red, prefix, no_prompt) await _edit_prefix(red, prefix, no_prompt)
@ -357,10 +357,10 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
token = new_token token = new_token
else: else:
log.critical("Token and prefix must be set in order to login.") log.critical("Token and prefix must be set in order to login.")
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
if cli_flags.dry_run: if cli_flags.dry_run:
sys.exit(0) sys.exit(ExitCodes.SHUTDOWN)
try: try:
# `async with red:` is unnecessary here because we call red.close() in shutdown handler # `async with red:` is unnecessary here because we call red.close() in shutdown handler
await red.start(token) await red.start(token)
@ -371,8 +371,8 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
if confirm("\nDo you want to reset the token?"): if confirm("\nDo you want to reset the token?"):
await red._config.token.set("") await red._config.token.set("")
print("Token has been reset.") print("Token has been reset.")
sys.exit(0) sys.exit(ExitCodes.SHUTDOWN)
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
except discord.PrivilegedIntentsRequired: except discord.PrivilegedIntentsRequired:
console = rich.get_console() console = rich.get_console()
console.print( console.print(
@ -381,7 +381,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
"https://docs.discord.red/en/stable/bot_application_guide.html#enabling-privileged-intents", "https://docs.discord.red/en/stable/bot_application_guide.html#enabling-privileged-intents",
style="red", style="red",
) )
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
except _NoOwnerSet: except _NoOwnerSet:
print( print(
"Bot doesn't have any owner set!\n" "Bot doesn't have any owner set!\n"
@ -399,7 +399,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
"c) pass owner ID(s) when launching Red with --owner" "c) pass owner ID(s) when launching Red with --owner"
" (and --co-owner if you need more than one) flag\n" " (and --co-owner if you need more than one) flag\n"
) )
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
return None return None
@ -410,12 +410,12 @@ def handle_early_exit_flags(cli_flags: Namespace):
elif cli_flags.version: elif cli_flags.version:
print("Red V3") print("Red V3")
print("Current Version: {}".format(__version__)) print("Current Version: {}".format(__version__))
sys.exit(0) sys.exit(ExitCodes.SHUTDOWN)
elif cli_flags.debuginfo: elif cli_flags.debuginfo:
early_exit_runner(cli_flags, debug_info) early_exit_runner(cli_flags, debug_info)
elif not cli_flags.instance_name and (not cli_flags.no_instance or cli_flags.edit): elif not cli_flags.instance_name and (not cli_flags.no_instance or cli_flags.edit):
print("Error: No instance name was provided!") print("Error: No instance name was provided!")
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
async def shutdown_handler(red, signal_type=None, exit_code=None): async def shutdown_handler(red, signal_type=None, exit_code=None):
@ -553,7 +553,7 @@ def main():
asyncio.set_event_loop(None) asyncio.set_event_loop(None)
loop.stop() loop.stop()
loop.close() loop.close()
exit_code = red._shutdown_mode if red is not None else 1 exit_code = red._shutdown_mode if red is not None else ExitCodes.CRITICAL
sys.exit(exit_code) sys.exit(exit_code)

View File

@ -11,7 +11,6 @@ import weakref
import functools import functools
from collections import namedtuple, OrderedDict from collections import namedtuple, OrderedDict
from datetime import datetime from datetime import datetime
from enum import IntEnum
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
@ -39,6 +38,7 @@ from discord.ext import commands as dpy_commands
from discord.ext.commands import when_mentioned_or from discord.ext.commands import when_mentioned_or
from . import Config, i18n, commands, errors, drivers, modlog, bank from . import Config, i18n, commands, errors, drivers, modlog, bank
from .cli import ExitCodes
from .cog_manager import CogManager, CogManagerUI from .cog_manager import CogManager, CogManagerUI
from .core_commands import Core from .core_commands import Core
from .data_manager import cog_data_path from .data_manager import cog_data_path
@ -69,7 +69,7 @@ SHARED_API_TOKENS = "SHARED_API_TOKENS"
log = logging.getLogger("red") log = logging.getLogger("red")
__all__ = ("Red", "ExitCodes") __all__ = ("Red",)
NotMessage = namedtuple("NotMessage", "guild") NotMessage = namedtuple("NotMessage", "guild")
@ -2190,11 +2190,3 @@ class Red(
failed_cogs=failures["cog"], failed_cogs=failures["cog"],
unhandled=failures["unhandled"], unhandled=failures["unhandled"],
) )
class ExitCodes(IntEnum):
# This needs to be an int enum to be used
# with sys.exit
CRITICAL = 1
SHUTDOWN = 0
RESTART = 26

View File

@ -2,6 +2,7 @@ import argparse
import asyncio import asyncio
import logging import logging
import sys import sys
from enum import IntEnum
from typing import Optional from typing import Optional
import discord import discord
@ -10,6 +11,21 @@ from discord import __version__ as discord_version
from redbot.core.utils._internal_utils import cli_level_to_log_level from redbot.core.utils._internal_utils import cli_level_to_log_level
# This needs to be an int enum to be used
# with sys.exit
class ExitCodes(IntEnum):
#: Clean shutdown (through signals, keyboard interrupt, [p]shutdown, etc.).
SHUTDOWN = 0
#: An unrecoverable error occurred during application's runtime.
CRITICAL = 1
#: The CLI command was used incorrectly, such as when the wrong number of arguments are given.
INVALID_CLI_USAGE = 2
#: Restart was requested by the bot owner (probably through [p]restart command).
RESTART = 26
#: Some kind of configuration error occurred.
CONFIGURATION_ERROR = 78 # Exit code borrowed from os.EX_CONFIG.
def confirm(text: str, default: Optional[bool] = None) -> bool: def confirm(text: str, default: Optional[bool] = None) -> bool:
if default is None: if default is None:
options = "y/n" options = "y/n"
@ -23,9 +39,12 @@ def confirm(text: str, default: Optional[bool] = None) -> bool:
while True: while True:
try: try:
value = input(f"{text}: [{options}] ").lower().strip() value = input(f"{text}: [{options}] ").lower().strip()
except (KeyboardInterrupt, EOFError): except KeyboardInterrupt:
print("\nAborted!") print("\nAborted!")
sys.exit(1) sys.exit(ExitCodes.SHUTDOWN)
except EOFError:
print("\nAborted!")
sys.exit(ExitCodes.INVALID_CLI_USAGE)
if value in ("y", "yes"): if value in ("y", "yes"):
return True return True
if value in ("n", "no"): if value in ("n", "no"):

View File

@ -12,6 +12,7 @@ import appdirs
from discord.utils import deprecated from discord.utils import deprecated
from . import commands from . import commands
from .cli import ExitCodes
__all__ = [ __all__ = [
"create_temp_config", "create_temp_config",
@ -118,7 +119,7 @@ def load_basic_configuration(instance_name_: str):
"You need to configure the bot instance using `redbot-setup`" "You need to configure the bot instance using `redbot-setup`"
" prior to running the bot." " prior to running the bot."
) )
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
try: try:
basic_config = config[instance_name] basic_config = config[instance_name]
except KeyError: except KeyError:
@ -126,7 +127,7 @@ def load_basic_configuration(instance_name_: str):
"Instance with this name doesn't exist." "Instance with this name doesn't exist."
" You can create new instance using `redbot-setup` prior to running the bot." " You can create new instance using `redbot-setup` prior to running the bot."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
def _base_data_path() -> Path: def _base_data_path() -> Path:

View File

@ -8,6 +8,8 @@ from aiohttp_json_rpc.rpc import JsonRpcMethod
import logging import logging
from redbot.core.cli import ExitCodes
log = logging.getLogger("red.rpc") log = logging.getLogger("red.rpc")
__all__ = ["RPC", "RPCMixin", "get_name"] __all__ = ["RPC", "RPCMixin", "get_name"]
@ -89,7 +91,7 @@ class RPC:
) )
except Exception as exc: except Exception as exc:
log.exception("RPC setup failure", exc_info=exc) log.exception("RPC setup failure", exc_info=exc)
sys.exit(1) sys.exit(ExitCodes.CRITICAL)
else: else:
await self._site.start() await self._site.start()
log.debug("Created RPC server listener on port %s", port) log.debug("Created RPC server listener on port %s", port)

View File

@ -20,7 +20,7 @@ from redbot.setup import (
create_backup, create_backup,
) )
from redbot.core import __version__, version_info as red_version_info, VersionInfo from redbot.core import __version__, version_info as red_version_info, VersionInfo
from redbot.core.cli import confirm from redbot.core.cli import ExitCodes, confirm
from redbot.core.data_manager import load_existing_config from redbot.core.data_manager import load_existing_config
if sys.platform == "linux": if sys.platform == "linux":
@ -155,7 +155,7 @@ def main():
req_ver=".".join(map(str, MIN_PYTHON_VERSION)), sys_ver=sys.version req_ver=".".join(map(str, MIN_PYTHON_VERSION)), sys_ver=sys.version
) )
) # Don't make an f-string, these may not exist on the python version being rejected! ) # Don't make an f-string, these may not exist on the python version being rejected!
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
if INTERACTIVE_MODE: if INTERACTIVE_MODE:
main_menu(flags_to_pass) main_menu(flags_to_pass)

View File

@ -21,6 +21,7 @@ from redbot.core.utils._internal_utils import (
cli_level_to_log_level, cli_level_to_log_level,
) )
from redbot.core import config, data_manager, drivers from redbot.core import config, data_manager, drivers
from redbot.core.cli import ExitCodes
from redbot.core.data_manager import appdir, config_dir, config_file from redbot.core.data_manager import appdir, config_dir, config_file
from redbot.core.drivers import BackendType, IdentifierData from redbot.core.drivers import BackendType, IdentifierData
@ -30,7 +31,7 @@ try:
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
except PermissionError: except PermissionError:
print("You don't have permission to write to '{}'\nExiting...".format(config_dir)) print("You don't have permission to write to '{}'\nExiting...".format(config_dir))
sys.exit(1) sys.exit(ExitCodes.CONFIGURATION_ERROR)
instance_data = data_manager.load_existing_config() instance_data = data_manager.load_existing_config()
if instance_data is None: if instance_data is None:
@ -77,7 +78,7 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
"We were unable to check your chosen directory." "We were unable to check your chosen directory."
" Provided path may contain an invalid character." " Provided path may contain an invalid character."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
if not exists: if not exists:
try: try:
@ -85,15 +86,15 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
except OSError: except OSError:
print( print(
"We were unable to create your chosen directory." "We were unable to create your chosen directory."
" You may need to restart this process with admin" " You may need to create the directory and set proper permissions"
" privileges." " for it manually before it can be used as the data directory."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
print("You have chosen {} to be your data directory.".format(data_path)) print("You have chosen {} to be your data directory.".format(data_path))
if not click.confirm("Please confirm", default=True): if not click.confirm("Please confirm", default=True):
print("Please start the process over.") print("Please start the process over.")
sys.exit(0) sys.exit(ExitCodes.CRITICAL)
return str(data_path.resolve()) return str(data_path.resolve())
@ -143,7 +144,7 @@ def get_name(name: str) -> str:
" and can only include characters A-z, numbers," " and can only include characters A-z, numbers,"
" and non-consecutive underscores (_) and periods (.)." " and non-consecutive underscores (_) and periods (.)."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
return name return name
while len(name) == 0: while len(name) == 0:
@ -191,7 +192,7 @@ def basic_setup(
"Providing instance name through --instance-name is required" "Providing instance name through --instance-name is required"
" when using non-interactive mode." " when using non-interactive mode."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
if interactive: if interactive:
print( print(
@ -225,14 +226,14 @@ def basic_setup(
"Are you absolutely certain you want to continue?", default=False "Are you absolutely certain you want to continue?", default=False
): ):
print("Not continuing") print("Not continuing")
sys.exit(0) sys.exit(ExitCodes.SHUTDOWN)
else: else:
print( print(
"An instance with this name already exists.\n" "An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one," "If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag." " run this command with --overwrite-existing-instance flag."
) )
sys.exit(1) sys.exit(ExitCodes.INVALID_CLI_USAGE)
save_config(name, default_dirs) save_config(name, default_dirs)
if interactive: if interactive: