1# Lint as: python2, python3
2# Copyright 2009 Google Inc. Released under the GPL v2
3
4"""
5This file contains the implementation of a host object for the local machine.
6"""
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
11import distutils.core
12import glob
13import os
14import platform
15import shutil
16import sys
17
18import common
19from autotest_lib.client.common_lib import hosts, error
20from autotest_lib.client.bin import utils
21import six
22
23
24class LocalHost(hosts.Host):
25    """This class represents a host running locally on the host."""
26
27
28    def _initialize(self, hostname=None, bootloader=None, *args, **dargs):
29        super(LocalHost, self)._initialize(*args, **dargs)
30
31        # hostname will be an actual hostname when this client was created
32        # by an autoserv process
33        if not hostname:
34            hostname = platform.node()
35        self.hostname = hostname
36        self.bootloader = bootloader
37        self.tmp_dirs = []
38
39
40    def close(self):
41        """Cleanup after we're done."""
42        for tmp_dir in self.tmp_dirs:
43            self.run('rm -rf "%s"' % (utils.sh_escape(tmp_dir)),
44                     ignore_status=True)
45
46
47    def wait_up(self, timeout=None):
48        # a local host is always up
49        return True
50
51
52    def run(self, command, timeout=3600, ignore_status=False,
53            ignore_timeout=False,
54            stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
55            stdin=None, args=(), **kwargs):
56        """
57        @see common_lib.hosts.Host.run()
58        """
59        try:
60            return utils.run(
61                command, timeout=timeout, ignore_status=ignore_status,
62                ignore_timeout=ignore_timeout, stdout_tee=stdout_tee,
63                stderr_tee=stderr_tee, stdin=stdin, args=args)
64        except error.CmdTimeoutError as e:
65            # CmdTimeoutError is a subclass of CmdError, so must be caught first
66            new_error = error.AutotestHostRunTimeoutError(
67                    e.command, e.result_obj, additional_text=e.additional_text)
68            six.reraise(error.AutotestHostRunTimeoutError, new_error, sys.exc_info()[2])
69        except error.CmdError as e:
70            new_error = error.AutotestHostRunCmdError(
71                    e.command, e.result_obj, additional_text=e.additional_text)
72            six.reraise(error.AutotestHostRunCmdError, new_error, sys.exc_info()[2])
73
74
75    def list_files_glob(self, path_glob):
76        """
77        Get a list of files on a remote host given a glob pattern path.
78        """
79        return glob.glob(path_glob)
80
81
82    def symlink_closure(self, paths):
83        """
84        Given a sequence of path strings, return the set of all paths that
85        can be reached from the initial set by following symlinks.
86
87        @param paths: sequence of path strings.
88        @return: a sequence of path strings that are all the unique paths that
89                can be reached from the given ones after following symlinks.
90        """
91        paths = set(paths)
92        closure = set()
93
94        while paths:
95            path = paths.pop()
96            if not os.path.exists(path):
97                continue
98            closure.add(path)
99            if os.path.islink(path):
100                link_to = os.path.join(os.path.dirname(path),
101                                       os.readlink(path))
102                if link_to not in closure:
103                    paths.add(link_to)
104
105        return closure
106
107
108    def _copy_file(self, source, dest, delete_dest=False, preserve_perm=False,
109                   preserve_symlinks=False):
110        """Copy files from source to dest, will be the base for {get,send}_file.
111
112        If source is a directory and ends with a trailing slash, only the
113        contents of the source directory will be copied to dest, otherwise
114        source itself will be copied under dest.
115
116        @param source: The file/directory on localhost to copy.
117        @param dest: The destination path on localhost to copy to.
118        @param delete_dest: A flag set to choose whether or not to delete
119                            dest if it exists.
120        @param preserve_perm: Tells get_file() to try to preserve the sources
121                              permissions on files and dirs.
122        @param preserve_symlinks: Try to preserve symlinks instead of
123                                  transforming them into files/dirs on copy.
124        """
125        # We copy dest under source if either:
126        #  1. Source is a directory and doesn't end with /.
127        #  2. Source is a file and dest is a directory.
128        source_is_dir = os.path.isdir(source)
129        if ((source_is_dir and not source.endswith(os.sep)) or
130            (not source_is_dir and os.path.isdir(dest))):
131            dest = os.path.join(dest, os.path.basename(source))
132
133        if delete_dest and os.path.exists(dest):
134            # Check if it's a file or a dir and use proper remove method.
135            if os.path.isdir(dest):
136                shutil.rmtree(dest)
137                os.mkdir(dest)
138            else:
139                os.remove(dest)
140
141        if preserve_symlinks and os.path.islink(source):
142            os.symlink(os.readlink(source), dest)
143        # If source is a dir, use distutils.dir_util.copytree since
144        # shutil.copy_tree has weird limitations.
145        elif os.path.isdir(source):
146            distutils.dir_util.copy_tree(source, dest,
147                    preserve_symlinks=preserve_symlinks,
148                    preserve_mode=preserve_perm,
149                    update=1)
150        else:
151            shutil.copyfile(source, dest)
152
153        if preserve_perm:
154            shutil.copymode(source, dest)
155
156
157    def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
158                 preserve_symlinks=False):
159        """Copy files from source to dest.
160
161        If source is a directory and ends with a trailing slash, only the
162        contents of the source directory will be copied to dest, otherwise
163        source itself will be copied under dest. This is to match the
164        behavior of AbstractSSHHost.get_file().
165
166        @param source: The file/directory on localhost to copy.
167        @param dest: The destination path on localhost to copy to.
168        @param delete_dest: A flag set to choose whether or not to delete
169                            dest if it exists.
170        @param preserve_perm: Tells get_file() to try to preserve the sources
171                              permissions on files and dirs.
172        @param preserve_symlinks: Try to preserve symlinks instead of
173                                  transforming them into files/dirs on copy.
174        """
175        self._copy_file(source, dest, delete_dest=delete_dest,
176                        preserve_perm=preserve_perm,
177                        preserve_symlinks=preserve_symlinks)
178
179
180    def send_file(self, source, dest, delete_dest=False,
181                  preserve_symlinks=False, excludes=None):
182        """Copy files from source to dest.
183
184        If source is a directory and ends with a trailing slash, only the
185        contents of the source directory will be copied to dest, otherwise
186        source itself will be copied under dest. This is to match the
187        behavior of AbstractSSHHost.send_file().
188
189        @param source: The file/directory on the drone to send to the device.
190        @param dest: The destination path on the device to copy to.
191        @param delete_dest: A flag set to choose whether or not to delete
192                            dest on the device if it exists.
193        @param preserve_symlinks: Controls if symlinks on the source will be
194                                  copied as such on the destination or
195                                  transformed into the referenced
196                                  file/directory.
197        @param excludes: A list of file pattern that matches files not to be
198                         sent. `send_file` will fail if exclude is set, since
199                         local copy does not support --exclude.
200        """
201        if excludes:
202            raise error.AutotestHostRunError(
203                    '--exclude is not supported in LocalHost.send_file method. '
204                    'excludes: %s' % ','.join(excludes), None)
205        self._copy_file(source, dest, delete_dest=delete_dest,
206                        preserve_symlinks=preserve_symlinks)
207
208
209    def get_tmp_dir(self, parent='/tmp'):
210        """
211        Return the pathname of a directory on the host suitable
212        for temporary file storage.
213
214        The directory and its content will be deleted automatically
215        on the destruction of the Host object that was used to obtain
216        it.
217
218        @param parent: The leading path to make the tmp dir.
219        """
220        tmp_dir = self.run(
221            'mkdir -p "%s" && mktemp -d -p "%s"' % (parent, parent)
222        ).stdout.rstrip()
223        self.tmp_dirs.append(tmp_dir)
224        return tmp_dir
225