1#===----------------------------------------------------------------------===##
2#
3#                     The LLVM Compiler Infrastructure
4#
5# This file is dual licensed under the MIT and the University of Illinois Open
6# Source Licenses. See LICENSE.TXT for details.
7#
8#===----------------------------------------------------------------------===##
9
10import platform
11import os
12
13from libcxx.test import tracing
14from libcxx.util import executeCommand
15
16
17class Executor(object):
18    def run(self, exe_path, cmd, local_cwd, file_deps=None, env=None):
19        """Execute a command.
20            Be very careful not to change shared state in this function.
21            Executor objects are shared between python processes in `lit -jN`.
22        Args:
23            exe_path: str:    Local path to the executable to be run
24            cmd: [str]:       subprocess.call style command
25            local_cwd: str:   Local path to the working directory
26            file_deps: [str]: Files required by the test
27            env: {str: str}:  Environment variables to execute under
28        Returns:
29            cmd, out, err, exitCode
30        """
31        raise NotImplementedError
32
33
34class LocalExecutor(Executor):
35    def __init__(self):
36        super(LocalExecutor, self).__init__()
37        self.is_windows = platform.system() == 'Windows'
38
39    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
40        cmd = cmd or [exe_path]
41        if work_dir == '.':
42            work_dir = os.getcwd()
43        out, err, rc = executeCommand(cmd, cwd=work_dir, env=env)
44        return (cmd, out, err, rc)
45
46
47class PrefixExecutor(Executor):
48    """Prefix an executor with some other command wrapper.
49
50    Most useful for setting ulimits on commands, or running an emulator like
51    qemu and valgrind.
52    """
53    def __init__(self, commandPrefix, chain):
54        super(PrefixExecutor, self).__init__()
55
56        self.commandPrefix = commandPrefix
57        self.chain = chain
58
59    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
60        cmd = cmd or [exe_path]
61        return self.chain.run(exe_path, self.commandPrefix + cmd, work_dir,
62                              file_deps, env=env)
63
64
65class PostfixExecutor(Executor):
66    """Postfix an executor with some args."""
67    def __init__(self, commandPostfix, chain):
68        super(PostfixExecutor, self).__init__()
69
70        self.commandPostfix = commandPostfix
71        self.chain = chain
72
73    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
74        cmd = cmd or [exe_path]
75        return self.chain.run(cmd + self.commandPostfix, work_dir, file_deps,
76                              env=env)
77
78
79
80class TimeoutExecutor(PrefixExecutor):
81    """Execute another action under a timeout.
82
83    Deprecated. http://reviews.llvm.org/D6584 adds timeouts to LIT.
84    """
85    def __init__(self, duration, chain):
86        super(TimeoutExecutor, self).__init__(
87            ['timeout', duration], chain)
88
89
90class RemoteExecutor(Executor):
91    def __init__(self):
92        self.local_run = executeCommand
93
94    def remote_temp_dir(self):
95        return self._remote_temp(True)
96
97    def remote_temp_file(self):
98        return self._remote_temp(False)
99
100    def _remote_temp(self, is_dir):
101        raise NotImplementedError()
102
103    def copy_in(self, local_srcs, remote_dsts):
104        # This could be wrapped up in a tar->scp->untar for performance
105        # if there are lots of files to be copied/moved
106        for src, dst in zip(local_srcs, remote_dsts):
107            self._copy_in_file(src, dst)
108
109    def _copy_in_file(self, src, dst):
110        raise NotImplementedError()
111
112    def delete_remote(self, remote):
113        try:
114            self._execute_command_remote(['rm', '-rf', remote])
115        except OSError:
116            # TODO: Log failure to delete?
117            pass
118
119    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
120        target_exe_path = None
121        target_cwd = None
122        try:
123            target_cwd = self.remote_temp_dir()
124            target_exe_path = os.path.join(target_cwd, 'libcxx_test.exe')
125            if cmd:
126                # Replace exe_path with target_exe_path.
127                cmd = [c if c != exe_path else target_exe_path for c in cmd]
128            else:
129                cmd = [target_exe_path]
130
131            srcs = [exe_path]
132            dsts = [target_exe_path]
133            if file_deps is not None:
134                dev_paths = [os.path.join(target_cwd, os.path.basename(f))
135                             for f in file_deps]
136                srcs.extend(file_deps)
137                dsts.extend(dev_paths)
138            self.copy_in(srcs, dsts)
139            # TODO(jroelofs): capture the copy_in and delete_remote commands,
140            # and conjugate them with '&&'s around the first tuple element
141            # returned here:
142            return self._execute_command_remote(cmd, target_cwd, env)
143        finally:
144            if target_cwd:
145                self.delete_remote(target_cwd)
146
147    def _execute_command_remote(self, cmd, remote_work_dir='.', env=None):
148        raise NotImplementedError()
149
150
151class SSHExecutor(RemoteExecutor):
152    def __init__(self, host, username=None):
153        super(SSHExecutor, self).__init__()
154
155        self.user_prefix = username + '@' if username else ''
156        self.host = host
157        self.scp_command = 'scp'
158        self.ssh_command = 'ssh'
159
160        # TODO(jroelofs): switch this on some -super-verbose-debug config flag
161        if False:
162            self.local_run = tracing.trace_function(
163                self.local_run, log_calls=True, log_results=True,
164                label='ssh_local')
165
166    def _remote_temp(self, is_dir):
167        # TODO: detect what the target system is, and use the correct
168        # mktemp command for it. (linux and darwin differ here, and I'm
169        # sure windows has another way to do it)
170
171        # Not sure how to do suffix on osx yet
172        dir_arg = '-d' if is_dir else ''
173        cmd = 'mktemp -q {} /tmp/libcxx.XXXXXXXXXX'.format(dir_arg)
174        _, temp_path, err, exitCode = self._execute_command_remote([cmd])
175        temp_path = temp_path.strip()
176        if exitCode != 0:
177            raise RuntimeError(err)
178        return temp_path
179
180    def _copy_in_file(self, src, dst):
181        scp = self.scp_command
182        remote = self.host
183        remote = self.user_prefix + remote
184        cmd = [scp, '-p', src, remote + ':' + dst]
185        self.local_run(cmd)
186
187    def _execute_command_remote(self, cmd, remote_work_dir='.', env=None):
188        remote = self.user_prefix + self.host
189        ssh_cmd = [self.ssh_command, '-oBatchMode=yes', remote]
190        if env:
191            env_cmd = ['env'] + ['%s=%s' % (k, v) for k, v in env.items()]
192        else:
193            env_cmd = []
194        remote_cmd = ' '.join(env_cmd + cmd)
195        if remote_work_dir != '.':
196            remote_cmd = 'cd ' + remote_work_dir + ' && ' + remote_cmd
197        out, err, rc = self.local_run(ssh_cmd + [remote_cmd])
198        return (remote_cmd, out, err, rc)
199