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