"""Common file and os related utilities, including tempdir manipulation."""
+from __future__ import print_function
+
import collections
+import contextlib
import cStringIO
import ctypes
import ctypes.util
+import datetime
import errno
import logging
+import operator
import os
import pwd
import re
import shutil
-import signal
import tempfile
from chromite.lib import cros_build_lib
return None
-def FindDepotTools():
- """Returns the location of depot_tools if it is in $PATH."""
- gclient_dir = os.path.dirname(Which('gclient.py', mode=os.F_OK) or '')
- gitcl_dir = os.path.dirname(Which('git_cl.py', mode=os.F_OK) or '')
- if gclient_dir and gclient_dir == gitcl_dir:
- return gclient_dir
-
-
def FindMissingBinaries(needed_tools):
"""Verifies that the required tools are present on the system.
# errno is bubbled up to us and we can detect it specifically without
# potentially ignoring all other possible failures.
return e.errno == errno.EBUSY
- retry_util.GenericRetry(_retry, 5, RmDir, path, sudo=sudo, sleep=1)
+ retry_util.GenericRetry(_retry, 30, RmDir, path, sudo=sudo, sleep=60)
def SetEnvironment(env):
multiline=multiline)
-def StrSignal(sig_num):
- """Convert a signal number to the symbolic name
-
- Note: Some signal number have multiple names, so you might get
- back a confusing result like "SIGIOT|SIGABRT". Since they have
- the same signal number, it's impossible to say which one is right.
-
- Args:
- sig_num: The numeric signal you wish to convert
-
- Returns:
- A string of the signal name(s)
- """
- sig_names = []
- for name, num in signal.__dict__.iteritems():
- if name.startswith('SIG') and num == sig_num:
- sig_names.append(name)
- if sig_names:
- return '|'.join(sig_names)
- else:
- return 'SIG_%i' % sig_num
-
-
def ListBlockDevices(device_path=None, in_bytes=False):
"""Lists all block devices.
cmd.append(device_path)
cmd += ['--output', ','.join(keys)]
- output = cros_build_lib.RunCommand(cmd, capture_output=True).output.strip()
+ output = cros_build_lib.RunCommand(
+ cmd, debug_level=logging.DEBUG, capture_output=True).output.strip()
devices = []
for line in output.splitlines():
d = {}
return int(d.SIZE) if in_bytes else d.SIZE
raise ValueError('No size info of %s is found.' % device_path)
+
+
+def GetExitStatus(status):
+ """Get the exit status of a child from an os.waitpid call.
+
+ Args:
+ status: The return value of os.waitpid(pid, 0)[1]
+
+ Returns:
+ The exit status of the process. If the process exited with a signal,
+ the return value will be 128 plus the signal number.
+ """
+ if os.WIFSIGNALED(status):
+ return 128 + os.WTERMSIG(status)
+ else:
+ assert os.WIFEXITED(status), 'Unexpected exit status %r' % status
+ return os.WEXITSTATUS(status)
+
+
+FileInfo = collections.namedtuple(
+ 'FileInfo', ['path', 'owner', 'size', 'atime', 'mtime'])
+
+
+def StatFilesInDirectory(path, recursive=False, to_string=False):
+ """Stat files in the directory |path|.
+
+ Args:
+ path: Path to the target directory.
+ recursive: Whether to recurisvely list all files in |path|.
+ to_string: Whether to return a string containing the metadata of the
+ files.
+
+ Returns:
+ If |to_string| is False, returns a list of FileInfo objects. Otherwise,
+ returns a string of metadata of the files.
+ """
+ path = ExpandPath(path)
+ def ToFileInfo(path, stat):
+ return FileInfo(path,
+ pwd.getpwuid(stat.st_uid)[0],
+ stat.st_size,
+ datetime.datetime.fromtimestamp(stat.st_atime),
+ datetime.datetime.fromtimestamp(stat.st_mtime))
+
+ file_infos = []
+ for root, dirs, files in os.walk(path, topdown=True):
+ for filename in dirs + files:
+ filepath = os.path.join(root, filename)
+ file_infos.append(ToFileInfo(filepath, os.lstat(filepath)))
+
+ if not recursive:
+ # Process only the top-most directory.
+ break
+
+ if not to_string:
+ return file_infos
+
+ msg = 'Listing the content of %s' % path
+ msg_format = ('Path: {x.path}, Owner: {x.owner}, Size: {x.size} bytes, '
+ 'Accessed: {x.atime}, Modified: {x.mtime}')
+ msg = '%s\n%s' % (msg,
+ '\n'.join([msg_format.format(x=x) for x in file_infos]))
+ return msg
+
+
+def MountImagePartition(image_file, part_number, destination, gpt_table=None,
+ sudo=True, makedirs=True, mount_opts=('ro', ),
+ skip_mtab=False):
+ """Mount a |partition| from |image_file| to |destination|.
+
+ If there is a GPT table (GetImageDiskPartitionInfo), it will be used for
+ start offset and size of the selected partition. Otherwise, the GPT will
+ be read again from |image_file|. The GPT table MUST have unit of "B".
+
+ The mount option will be:
+
+ -o offset=XXX,sizelimit=YYY,(*mount_opts)
+
+ Args:
+ image_file: A path to the image file (chromiumos_base_image.bin).
+ part_number: A partition number.
+ destination: A path to the mount point.
+ gpt_table: A dictionary of PartitionInfo objects. See
+ cros_build_lib.GetImageDiskPartitionInfo.
+ sudo: Same as MountDir.
+ makedirs: Same as MountDir.
+ mount_opts: Same as MountDir.
+ skip_mtab: Same as MountDir.
+ """
+
+ if gpt_table is None:
+ gpt_table = cros_build_lib.GetImageDiskPartitionInfo(image_file, 'B',
+ key_selector='number')
+
+ for _, part in gpt_table.items():
+ if part.number == part_number:
+ break
+ else:
+ part = None
+ raise ValueError('Partition number %d not found in the GPT %r.' %
+ (part_number, gpt_table))
+
+ opts = ['loop', 'offset=%d' % part.start, 'sizelimit=%d' % part.size]
+ opts += mount_opts
+ MountDir(image_file, destination, sudo=sudo, makedirs=makedirs,
+ mount_opts=opts, skip_mtab=skip_mtab)
+
+
+@contextlib.contextmanager
+def ChdirContext(target_dir):
+ """A context manager to chdir() into |target_dir| and back out on exit.
+
+ Args:
+ target_dir: A target directory to chdir into.
+ """
+
+ cwd = os.getcwd()
+ os.chdir(target_dir)
+ try:
+ yield
+ finally:
+ os.chdir(cwd)
+
+
+class MountImageContext(object):
+ """A context manager to mount an image."""
+
+ def __init__(self, image_file, destination, part_selects=(1, 3)):
+ """Construct a context manager object to actually do the job.
+
+ Specified partitions will be mounted under |destination| according to the
+ pattern:
+
+ partition ---mount--> dir-<partition number>
+
+ Symlinks with labels "dir-<label>" will also be created in |destination| to
+ point to the mounted partitions. If there is a conflict in symlinks, the
+ first one wins.
+
+ The image is unmounted when this context manager exits.
+
+ with MountImageContext('build/images/wolf/latest', 'root_mount_point'):
+ # "dir-1", and "dir-3" will be mounted in root_mount_point
+ ...
+
+ Args:
+ image_file: A path to the image file.
+ destination: A directory in which all mount points and symlinks will be
+ created. This parameter is relative to the CWD at the time __init__ is
+ called.
+ part_selects: A list of partition numbers or labels to be mounted. If an
+ element is an integer, it is matched as partition number, otherwise
+ a partition label.
+ """
+ self._image_file = image_file
+ self._gpt_table = cros_build_lib.GetImageDiskPartitionInfo(
+ self._image_file, 'B', key_selector='number'
+ )
+ # Target dir is absolute path so that we do not have to worry about
+ # CWD being changed later.
+ self._target_dir = ExpandPath(destination)
+ self._part_selects = part_selects
+ self._mounted = set()
+ self._linked_labels = set()
+
+ def _GetMountPointAndSymlink(self, part):
+ """Given a PartitionInfo, return a tuple of mount point and symlink.
+
+ Args:
+ part: A PartitionInfo object.
+
+ Returns:
+ A tuple (mount_point, symlink).
+ """
+ dest_number = os.path.join(self._target_dir, 'dir-%d' % part.number)
+ dest_label = os.path.join(self._target_dir, 'dir-%s' % part.name)
+ return dest_number, dest_label
+
+ def _Mount(self, part):
+ """Mount the partition and create a symlink to the mount point.
+
+ The partition is mounted as "dir-partNumber", and the symlink "dir-label".
+ If "dir-label" already exists, no symlink is created.
+
+ Args:
+ part: A PartitionInfo object.
+
+ Raises:
+ ValueError if mount point already exists.
+ """
+ if part in self._mounted:
+ return
+
+ dest_number, dest_label = self._GetMountPointAndSymlink(part)
+ if os.path.exists(dest_number):
+ raise ValueError('Mount point %s already exists.' % dest_number)
+
+ MountImagePartition(self._image_file, part.number,
+ dest_number, self._gpt_table)
+ self._mounted.add(part)
+
+ if not os.path.exists(dest_label):
+ os.symlink(os.path.basename(dest_number), dest_label)
+ self._linked_labels.add(dest_label)
+
+ def _Unmount(self, part):
+ """Unmount a partition that was mounted by _Mount."""
+ dest_number, dest_label = self._GetMountPointAndSymlink(part)
+ # Due to crosbug/358933, the RmDir call might fail. So we skip the cleanup.
+ UmountDir(dest_number, cleanup=False)
+ self._mounted.remove(part)
+
+ if dest_label in self._linked_labels:
+ SafeUnlink(dest_label)
+ self._linked_labels.remove(dest_label)
+
+ def _CleanUp(self):
+ """Unmount all mounted partitions."""
+ to_be_rmdir = []
+ for part in list(self._mounted):
+ self._Unmount(part)
+ dest_number, _ = self._GetMountPointAndSymlink(part)
+ to_be_rmdir.append(dest_number)
+ # Because _Unmount did not RmDir the mount points, we do that here.
+ for path in to_be_rmdir:
+ retry_util.RetryException(cros_build_lib.RunCommandError, 30,
+ RmDir, path, sudo=True, sleep=60)
+
+ def __enter__(self):
+ for selector in self._part_selects:
+ matcher = operator.attrgetter('number')
+ if not isinstance(selector, int):
+ matcher = operator.attrgetter('name')
+ for _, part in self._gpt_table.items():
+ if matcher(part) == selector:
+ try:
+ self._Mount(part)
+ except:
+ self._CleanUp()
+ raise
+ break
+ else:
+ self._CleanUp()
+ raise ValueError('Partition %r not found in the GPT %r.' %
+ (selector, self._gpt_table))
+
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self._CleanUp()
+
+
+MountInfo = collections.namedtuple('MountInfo',
+ 'source destination filesystem options')
+
+
+def IterateMountPoints(proc_file='/proc/mounts'):
+ """Iterate over all mounts as reported by "/proc/mounts".
+
+ Args:
+ proc_file: A path to a file whose content is similar to /proc/mounts.
+ Default to "/proc/mounts" itself.
+
+ Returns:
+ A generator that yields MountInfo objects.
+ """
+ with open(proc_file, 'rt') as f:
+ for line in f:
+ # Escape any \xxx to a char.
+ source, destination, filesystem, options, _, _ = [
+ re.sub(r'\\([0-7]{3})', lambda m: chr(int(m.group(1), 8)), x)
+ for x in line.split()
+ ]
+ mtab = MountInfo(source, destination, filesystem, options)
+ yield mtab
+
+
+def ResolveSymlink(file_name, root='/'):
+ """Resolve a symlink |file_name| relative to |root|.
+
+ For example:
+
+ ROOT-A/absolute_symlink --> /an/abs/path
+ ROOT-A/relative_symlink --> a/relative/path
+
+ absolute_symlink will be resolved to ROOT-A/an/abs/path
+ relative_symlink will be resolved to ROOT-A/a/relative/path
+
+ Args:
+ file_name: A path to the file.
+ root: A path to the root directory.
+
+ Returns:
+ |file_name| if |file_name| is not a symlink. Otherwise, the ultimate path
+ that |file_name| points to, with links resolved relative to |root|.
+ """
+ count = 0
+ while os.path.islink(file_name):
+ count += 1
+ if count > 128:
+ raise ValueError('Too many link levels for %s.' % file_name)
+ link = os.readlink(file_name)
+ if link.startswith('/'):
+ file_name = os.path.join(root, link[1:])
+ else:
+ file_name = os.path.join(os.path.dirname(file_name), link)
+ return file_name