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