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