Yolofactorize install/upgrade/restore error handling into a smart 'hook_exec_with_script_debug_if_failure'

This commit is contained in:
Alexandre Aubin 2021-09-05 20:24:36 +02:00
parent 56f525cf80
commit 3936589d3b
4 changed files with 101 additions and 130 deletions

View file

@ -511,7 +511,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
"""
from packaging import version
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.hook import hook_add, hook_remove, hook_callback, hook_exec_with_script_debug_if_failure
from yunohost.permission import permission_sync_to_user
from yunohost.regenconf import manually_modified_files
@ -633,36 +633,13 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
# Execute the app upgrade script
upgrade_failed = True
try:
upgrade_retcode = hook_exec(
extracted_app_folder + "/scripts/upgrade", env=env_dict
)[0]
upgrade_failed = True if upgrade_retcode != 0 else False
if upgrade_failed:
error = m18n.n("app_upgrade_script_failed")
logger.error(
m18n.n("app_upgrade_failed", app=app_instance_name, error=error)
)
failure_message_with_debug_instructions = operation_logger.error(error)
if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
upgrade_retcode = -1
error = m18n.n("operation_interrupted")
logger.error(
m18n.n("app_upgrade_failed", app=app_instance_name, error=error)
upgrade_failed, failure_message_with_debug_instructions = hook_exec_with_script_debug_if_failure(
extracted_app_folder + "/scripts/upgrade",
env=env_dict,
operation_logger=operation_logger,
error_message_if_script_failed=m18n.n("app_upgrade_script_failed"),
error_message_if_failed=lambda e: m18n.n("app_upgrade_failed", app=app_instance_name, error=e)
)
failure_message_with_debug_instructions = operation_logger.error(error)
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(
m18n.n("app_install_failed", app=app_instance_name, error=error)
)
failure_message_with_debug_instructions = operation_logger.error(error)
finally:
# Whatever happened (install success or failure) we check if it broke the system
# and warn the user about it
@ -692,7 +669,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
if upgrade_failed or broke_the_system:
# display this if there are remaining apps
if apps[number + 1 :]:
if apps[number + 1:]:
not_upgraded_apps = apps[number:]
logger.error(
m18n.n(
@ -808,7 +785,7 @@ def app_install(
force -- Do not ask for confirmation when installing experimental / low-quality apps
"""
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.hook import hook_add, hook_remove, hook_callback, hook_exec, hook_exec_with_script_debug_if_failure
from yunohost.log import OperationLogger
from yunohost.permission import (
user_permission_list,
@ -999,29 +976,13 @@ def app_install(
# Execute the app install script
install_failed = True
try:
install_retcode = hook_exec(
os.path.join(extracted_app_folder, "scripts/install"), env=env_dict
)[0]
# "Common" app install failure : the script failed and returned exit code != 0
install_failed = True if install_retcode != 0 else False
if install_failed:
error = m18n.n("app_install_script_failed")
logger.error(m18n.n("app_install_failed", app=app_id, error=error))
failure_message_with_debug_instructions = operation_logger.error(error)
if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(m18n.n("app_install_failed", app=app_id, error=error))
failure_message_with_debug_instructions = operation_logger.error(error)
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(m18n.n("app_install_failed", app=app_id, error=error))
failure_message_with_debug_instructions = operation_logger.error(error)
install_failed, failure_message_with_debug_instructions = hook_exec_with_script_debug_if_failure(
os.path.join(extracted_app_folder, "scripts/install"),
env=env_dict,
operation_logger=operation_logger,
error_message_if_script_failed=m18n.n("app_install_script_failed"),
error_message_if_failed=lambda e: m18n.n("app_install_failed", app=app_id, error=e)
)
finally:
# If success so far, validate that app didn't break important stuff
if not install_failed:
@ -1134,53 +1095,6 @@ def app_install(
hook_callback("post_app_install", env=env_dict)
def dump_app_log_extract_for_debugging(operation_logger):
with open(operation_logger.log_path, "r") as f:
lines = f.readlines()
filters = [
r"set [+-]x$",
r"set [+-]o xtrace$",
r"local \w+$",
r"local legacy_args=.*$",
r".*Helper used in legacy mode.*",
r"args_array=.*$",
r"local -A args_array$",
r"ynh_handle_getopts_args",
r"ynh_script_progression",
]
filters = [re.compile(f_) for f_ in filters]
lines_to_display = []
for line in lines:
if ": " not in line.strip():
continue
# A line typically looks like
# 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
# And we just want the part starting by "DEBUG - "
line = line.strip().split(": ", 1)[1]
if any(filter_.search(line) for filter_ in filters):
continue
lines_to_display.append(line)
if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line:
break
elif len(lines_to_display) > 20:
lines_to_display.pop(0)
logger.warning(
"Here's an extract of the logs before the crash. It might help debugging the error:"
)
for line in lines_to_display:
logger.info(line)
@is_unit_operation()
def app_remove(operation_logger, app, purge=False):
"""

View file

@ -48,7 +48,6 @@ from yunohost.app import (
app_info,
_is_installed,
_make_environment_for_app_script,
dump_app_log_extract_for_debugging,
_patch_legacy_helpers,
_patch_legacy_php_versions,
_patch_legacy_php_versions_in_settings,
@ -60,6 +59,7 @@ from yunohost.hook import (
hook_info,
hook_callback,
hook_exec,
hook_exec_with_script_debug_if_failure,
CUSTOM_HOOK_FOLDER,
)
from yunohost.tools import (
@ -1496,37 +1496,14 @@ class RestoreManager:
# Execute the app install script
restore_failed = True
try:
restore_retcode = hook_exec(
restore_failed, failure_message_with_debug_instructions = hook_exec_with_script_debug_if_failure(
restore_script,
chdir=app_backup_in_archive,
env=env_dict,
)[0]
# "Common" app restore failure : the script failed and returned exit code != 0
restore_failed = True if restore_retcode != 0 else False
if restore_failed:
error = m18n.n("app_restore_script_failed")
logger.error(
m18n.n("app_restore_failed", app=app_instance_name, error=error)
)
failure_message_with_debug_instructions = operation_logger.error(error)
if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(
m18n.n("app_restore_failed", app=app_instance_name, error=error)
operation_logger=operation_logger,
error_message_if_script_failed=m18n.n("app_restore_script_failed"),
error_message_if_failed=lambda e: m18n.n("app_restore_failed", app=app_instance_name, error=e)
)
failure_message_with_debug_instructions = operation_logger.error(error)
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(
m18n.n("app_restore_failed", app=app_instance_name, error=error)
)
failure_message_with_debug_instructions = operation_logger.error(error)
finally:
# Cleaning temporary scripts directory
shutil.rmtree(tmp_workdir_for_app, ignore_errors=True)

View file

@ -498,6 +498,40 @@ def _hook_exec_python(path, args, env, loggers):
return ret
def hook_exec_with_script_debug_if_failure(*args, **kwargs):
operation_logger = kwargs.pop("operation_logger")
error_message_if_failed = kwargs.pop("error_message_if_failed")
error_message_if_script_failed = kwargs.pop("error_message_if_script_failed")
failed = True
failure_message_with_debug_instructions = None
try:
retcode, retpayload = hook_exec(*args, **kwargs)
failed = True if retcode != 0 else False
if failed:
error = error_message_if_script_failed
logger.error(error_message_if_failed(error))
failure_message_with_debug_instructions = operation_logger.error(error)
if Moulinette.interface.type != "api":
operation_logger.dump_script_log_extract_for_debugging()
# Script got manually interrupted ...
# N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(error_message_if_failed(error))
failure_message_with_debug_instructions = operation_logger.error(error)
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(error_message_if_failed(error))
failure_message_with_debug_instructions = operation_logger.error(error)
return failed, failure_message_with_debug_instructions
def _extract_filename_parts(filename):
"""Extract hook parts from filename"""
if "-" in filename:

View file

@ -707,6 +707,52 @@ class OperationLogger(object):
else:
self.error(m18n.n("log_operation_unit_unclosed_properly"))
def dump_script_log_extract_for_debugging(self):
with open(self.log_path, "r") as f:
lines = f.readlines()
filters = [
r"set [+-]x$",
r"set [+-]o xtrace$",
r"local \w+$",
r"local legacy_args=.*$",
r".*Helper used in legacy mode.*",
r"args_array=.*$",
r"local -A args_array$",
r"ynh_handle_getopts_args",
r"ynh_script_progression",
]
filters = [re.compile(f_) for f_ in filters]
lines_to_display = []
for line in lines:
if ": " not in line.strip():
continue
# A line typically looks like
# 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo
# And we just want the part starting by "DEBUG - "
line = line.strip().split(": ", 1)[1]
if any(filter_.search(line) for filter_ in filters):
continue
lines_to_display.append(line)
if line.endswith("+ ynh_exit_properly") or " + ynh_die " in line:
break
elif len(lines_to_display) > 20:
lines_to_display.pop(0)
logger.warning(
"Here's an extract of the logs before the crash. It might help debugging the error:"
)
for line in lines_to_display:
logger.info(line)
def _get_datetime_from_name(name):