blob: 162efbbdfbf47877f60e76ccd7465166098d9986 [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""An implementation of the RemoteexecConfig proto interface."""
import datetime
import getpass
import json
import logging
import os
from pathlib import Path
import shlex
from typing import List
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.utils import hostname_util
# Synced with "log_dir" and "proxy_log_dir" in ./sdk/reclient_cfgs/reproxy.cfg
_REMOTEEXEC_LOG_BASE_DIR = Path("/tmp")
_REMOTEEXEC_LOG_DIRNAME_PATTERN = "reclient-*"
_REMOTEEXEC_LOG_FILE_PATTERN = (
"*.INFO.*",
"reproxy_*.INFO",
"*.rrpl",
)
class LogsArchiver:
"""Manages archiving remoteexec log files."""
def __init__(self, build_log_dir: Path, dest_dir: Path) -> None:
"""Initializes the archiver.
Args:
build_log_dir: path to the build directory.
dest_dir: path to the target directory to which logs are written.
"""
self.remoteexec_log_dir_for_testing = None
self._dest_base_dir = dest_dir
self._build_log_dir = build_log_dir
osutils.SafeMakedirs(self._dest_base_dir)
def archive(self) -> List[str]:
"""Archives all log files.
Returns:
A list of compressed log file paths.
"""
return self.archive_remoteexec_logs() + self.archive_ninja_log()
def archive_remoteexec_logs(self) -> List[str]:
"""Archives remoteexec log files.
Returns:
A list of compressed log file paths.
"""
log_dir_pattern = (
self.remoteexec_log_dir_for_testing or _REMOTEEXEC_LOG_BASE_DIR
)
log_dirs = sorted(log_dir_pattern.glob(_REMOTEEXEC_LOG_DIRNAME_PATTERN))
archived_log_files = []
for log_dir in log_dirs:
logging.info("Processing log dir: %s", log_dir)
for pattern in _REMOTEEXEC_LOG_FILE_PATTERN:
archived_log_files += self._archive_files(log_dir, pattern)
return archived_log_files
def _archive_files(self, directory: Path, pattern: str) -> List[Path]:
"""Archives files matched with pattern, with gzip'ing.
Args:
directory: directory of the files.
pattern: matching path pattern (eg. "*.INFO").
Returns:
A list of compressed log file paths.
"""
# Find files matched with the |pattern| in |directory|. Sort for
# stabilization.
paths = sorted(directory.glob(pattern))
if not paths:
logging.warning("No glob files matched with %s", pattern)
result = []
for path in paths:
archived_filename = f"{path.name}.gz"
log_label = f"{directory.name}/{archived_filename}"
logging.info("Compressing %s", log_label)
dest_filepath = (
self._dest_base_dir / directory.name / archived_filename
)
osutils.SafeMakedirs(dest_filepath.parent)
cros_build_lib.CompressFile(path, dest_filepath)
osutils.SafeUnlink(path)
result.append(log_label)
return result
def archive_ninja_log(self) -> List[str]:
"""Archives ninja_log file and its related metadata.
This archives the ninja_log file generated by ninja to build Chrome.
Also, it appends some related metadata at the end of the file following
'# end of ninja log' marker.
Returns:
The list of the archived files.
"""
ninja_log_path = os.path.join(self._build_log_dir, "ninja_log")
if not os.path.exists(ninja_log_path):
logging.warning("ninja_log is not found: %s", ninja_log_path)
return []
ninja_log_content = osutils.ReadFile(ninja_log_path)
try:
st = os.stat(ninja_log_path)
ninja_log_mtime = datetime.datetime.fromtimestamp(st.st_mtime)
except OSError:
logging.exception("Failed to get timestamp: %s", ninja_log_path)
return []
ninja_log_info = self._build_ninja_info()
# Append metadata at the end of the log content.
ninja_log_content += "# end of ninja log\n" + json.dumps(ninja_log_info)
# Aligned with goma_utils in chromium bot.
pid = os.getpid()
ninja_log_path = self._build_log_dir / (
"ninja_log.%s.%s.%s.%d"
% (
getpass.getuser(),
hostname_util.get_host_name(),
ninja_log_mtime.strftime("%Y%m%d-%H%M%S"),
pid,
)
)
osutils.WriteFile(ninja_log_path, ninja_log_content)
archived_filename = os.path.basename(ninja_log_path) + ".gz"
archived_path = self._dest_base_dir / archived_filename
cros_build_lib.CompressFile(ninja_log_path, archived_path)
return [archived_filename]
def _build_ninja_info(self):
"""Reads metadata for the ninja run.
Each metadata should be written into a dedicated file in the log
directory. Read the info, and build the dict containing metadata.
Returns:
A dict of the metadata.
"""
info = {"platform": "chromeos"}
command_path = self._build_log_dir / "ninja_command"
if os.path.exists(command_path):
info["cmdline"] = shlex.split(
osutils.ReadFile(command_path).strip()
)
cwd_path = self._build_log_dir / "ninja_cwd"
if os.path.exists(cwd_path):
info["cwd"] = osutils.ReadFile(cwd_path).strip()
exit_path = self._build_log_dir / "ninja_exit"
if os.path.exists(exit_path):
info["exit"] = int(osutils.ReadFile(exit_path).strip())
env_path = self._build_log_dir / "ninja_env"
if os.path.exists(env_path):
# env is null-byte separated, and has a trailing null byte.
content = osutils.ReadFile(env_path).rstrip("\0")
info["env"] = dict(
line.split("=", 1) for line in content.split("\0")
)
return info