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            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        return executeCommand(env_cmd + cmd, cwd=work_dir)
38
39
40class PrefixExecutor(Executor):
41    """Prefix an executor with some other command wrapper.
42
43    Most useful for setting ulimits on commands, or running an emulator like
44    qemu and valgrind.
45    """
46    def __init__(self, commandPrefix, chain):
47        super(PrefixExecutor, self).__init__()
48
49        self.commandPrefix = commandPrefix
50        self.chain = chain
51
52    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
53        cmd = cmd or [exe_path]
54        return self.chain.run(exe_path, self.commandPrefix + cmd, work_dir,
55                              file_deps, env=env)
56
57
58class PostfixExecutor(Executor):
59    """Postfix an executor with some args."""
60    def __init__(self, commandPostfix, chain):
61        super(PostfixExecutor, self).__init__()
62
63        self.commandPostfix = commandPostfix
64        self.chain = chain
65
66    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
67        cmd = cmd or [exe_path]
68        return self.chain.run(cmd + self.commandPostfix, work_dir, file_deps,
69                              env=env)
70
71
72
73class TimeoutExecutor(PrefixExecutor):
74    """Execute another action under a timeout.
75
76    Deprecated. http://reviews.llvm.org/D6584 adds timeouts to LIT.
77    """
78    def __init__(self, duration, chain):
79        super(TimeoutExecutor, self).__init__(
80            ['timeout', duration], chain)
81
82
83class RemoteExecutor(Executor):
84    def __init__(self):
85        self.local_run = executeCommand
86
87    def remote_temp_dir(self):
88        return self._remote_temp(True)
89
90    def remote_temp_file(self):
91        return self._remote_temp(False)
92
93    def _remote_temp(self, is_dir):
94        raise NotImplementedError()
95
96    def copy_in(self, local_srcs, remote_dsts):
97        # This could be wrapped up in a tar->scp->untar for performance
98        # if there are lots of files to be copied/moved
99        for src, dst in zip(local_srcs, remote_dsts):
100            self._copy_in_file(src, dst)
101
102    def _copy_in_file(self, src, dst):
103        raise NotImplementedError()
104
105    def delete_remote(self, remote):
106        try:
107            self._execute_command_remote(['rm', '-rf', remote])
108        except OSError:
109            # TODO: Log failure to delete?
110            pass
111
112    def run(self, exe_path, cmd=None, work_dir='.', file_deps=None, env=None):
113        target_exe_path = None
114        target_cwd = None
115        try:
116            target_cwd = self.remote_temp_dir()
117            target_exe_path = os.path.join(target_cwd, 'libcxx_test.exe')
118            if cmd:
119                # Replace exe_path with target_exe_path.
120                cmd = [c if c != exe_path else target_exe_path for c in cmd]
121            else:
122                cmd = [target_exe_path]
123
124            srcs = [exe_path]
125            dsts = [target_exe_path]
126            if file_deps is not None:
127                dev_paths = [os.path.join(target_cwd, os.path.basename(f))
128                             for f in file_deps]
129                srcs.extend(file_deps)
130                dsts.extend(dev_paths)
131            self.copy_in(srcs, dsts)
132            return self._execute_command_remote(cmd, target_cwd, env)
133        finally:
134            if target_cwd:
135                self.delete_remote(target_cwd)
136
137    def _execute_command_remote(self, cmd, remote_work_dir='.', env=None):
138        raise NotImplementedError()
139
140
141class SSHExecutor(RemoteExecutor):
142    def __init__(self, host, username=None):
143        super(SSHExecutor, self).__init__()
144
145        self.user_prefix = username + '@' if username else ''
146        self.host = host
147        self.scp_command = 'scp'
148        self.ssh_command = 'ssh'
149
150        # TODO(jroelofs): switch this on some -super-verbose-debug config flag
151        if False:
152            self.local_run = tracing.trace_function(
153                self.local_run, log_calls=True, log_results=True,
154                label='ssh_local')
155
156    def _remote_temp(self, is_dir):
157        # TODO: detect what the target system is, and use the correct
158        # mktemp command for it. (linux and darwin differ here, and I'm
159        # sure windows has another way to do it)
160
161        # Not sure how to do suffix on osx yet
162        dir_arg = '-d' if is_dir else ''
163        cmd = 'mktemp -q {} /tmp/libcxx.XXXXXXXXXX'.format(dir_arg)
164        temp_path, err, exitCode = self._execute_command_remote([cmd])
165        temp_path = temp_path.strip()
166        if exitCode != 0:
167            raise RuntimeError(err)
168        return temp_path
169
170    def _copy_in_file(self, src, dst):
171        scp = self.scp_command
172        remote = self.host
173        remote = self.user_prefix + remote
174        cmd = [scp, '-p', src, remote + ':' + dst]
175        self.local_run(cmd)
176
177    def _execute_command_remote(self, cmd, remote_work_dir='.', env=None):
178        remote = self.user_prefix + self.host
179        ssh_cmd = [self.ssh_command, '-oBatchMode=yes', remote]
180        if env:
181            env_cmd = ['env'] + ['%s=%s' % (k, v) for k, v in env.items()]
182        else:
183            env_cmd = []
184        remote_cmd = ' '.join(env_cmd + cmd)
185        if remote_work_dir != '.':
186            remote_cmd = 'cd ' + remote_work_dir + ' && ' + remote_cmd
187        return self.local_run(ssh_cmd + [remote_cmd])
188