blob: aaab2dde3bba4d15878dcc42cf6d360257e2472b [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001# Copyright 2014-2015 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15
16
17import os
18import stat
19import logging
20import subprocess
21import re
22import threading
23import tempfile
24import shutil
Anouk Van Laer29a79402017-01-31 12:48:58 +000025import socket
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +000026import time
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010027
Patrick Bellasic1d7cfa2015-10-12 15:25:12 +010028import pexpect
29from distutils.version import StrictVersion as V
30if V(pexpect.__version__) < V('4.0.0'):
31 import pxssh
32else:
33 from pexpect import pxssh
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010034from pexpect import EOF, TIMEOUT, spawn
35
36from devlib.exception import HostError, TargetError, TimeoutError
37from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output
Anouk Van Laer29a79402017-01-31 12:48:58 +000038from devlib.utils.types import boolean
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010039
40
41ssh = None
42scp = None
43sshpass = None
44
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010045
Anouk Van Laer29a79402017-01-31 12:48:58 +000046logger = logging.getLogger('ssh')
47gem5_logger = logging.getLogger('gem5-connection')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010048
Gareth Stockwell76b059c2016-07-29 15:47:50 +010049def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010050 _check_env()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +000051 start_time = time.time()
52 while True:
53 if telnet:
54 if keyfile:
55 raise ValueError('keyfile may not be used with a telnet connection.')
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +000056 conn = TelnetPxssh(original_prompt=original_prompt)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +000057 else: # ssh
58 conn = pxssh.pxssh()
59
60 try:
61 if keyfile:
62 conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout)
63 else:
64 conn.login(host, username, password, port=port, login_timeout=timeout)
65 break
66 except EOF:
67 timeout -= time.time() - start_time
68 if timeout <= 0:
69 message = 'Could not connect to {}; is the host name correct?'
70 raise TargetError(message.format(host))
71 time.sleep(5)
72
Patrick Bellasi5d288ef2015-11-10 11:26:31 +000073 conn.setwinsize(500,200)
74 conn.sendline('')
75 conn.prompt()
Patrick Bellasi10c80a92015-11-10 11:27:25 +000076 conn.setecho(False)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010077 return conn
78
79
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +000080class TelnetPxssh(pxssh.pxssh):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010081 # pylint: disable=arguments-differ
82
Gareth Stockwell76b059c2016-07-29 15:47:50 +010083 def __init__(self, original_prompt):
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +000084 super(TelnetPxssh, self).__init__()
Gareth Stockwell76b059c2016-07-29 15:47:50 +010085 self.original_prompt = original_prompt or r'[#$]'
86
87 def login(self, server, username, password='', login_timeout=10,
Sebastian Goscik73f2e282016-07-29 15:14:41 +010088 auto_prompt_reset=True, sync_multiplier=1, port=23):
Gareth Stockwellf2449362016-07-29 15:49:41 +010089 args = ['telnet']
90 if username is not None:
91 args += ['-l', username]
92 args += [server, str(port)]
93 cmd = ' '.join(args)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010094
95 spawn._spawn(self, cmd) # pylint: disable=protected-access
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010096
Anouk Van Laer29a79402017-01-31 12:48:58 +000097 try:
Gareth Stockwellf2449362016-07-29 15:49:41 +010098 i = self.expect('(?i)(?:password)', timeout=login_timeout)
99 if i == 0:
100 self.sendline(password)
101 i = self.expect([self.original_prompt, 'Login incorrect'], timeout=login_timeout)
Gareth Stockwellf2449362016-07-29 15:49:41 +0100102 if i:
103 raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
Anouk Van Laer29a79402017-01-31 12:48:58 +0000104 except TIMEOUT:
105 if not password:
106 # No password promt before TIMEOUT & no password provided
107 # so assume everything is okay
108 pass
109 else:
110 raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100111
112 if not self.sync_original_prompt(sync_multiplier):
113 self.close()
114 raise pxssh.ExceptionPxssh('could not synchronize with original prompt')
115
116 if auto_prompt_reset:
117 if not self.set_unique_prompt():
118 self.close()
119 message = 'could not set shell prompt (recieved: {}, expected: {}).'
120 raise pxssh.ExceptionPxssh(message.format(self.before, self.PROMPT))
121 return True
122
123
124def check_keyfile(keyfile):
125 """
126 keyfile must have the right access premissions in order to be useable. If the specified
127 file doesn't, create a temporary copy and set the right permissions for that.
128
129 Returns either the ``keyfile`` (if the permissions on it are correct) or the path to a
130 temporary copy with the right permissions.
131 """
132 desired_mask = stat.S_IWUSR | stat.S_IRUSR
133 actual_mask = os.stat(keyfile).st_mode & 0xFF
134 if actual_mask != desired_mask:
135 tmp_file = os.path.join(tempfile.gettempdir(), os.path.basename(keyfile))
136 shutil.copy(keyfile, tmp_file)
137 os.chmod(tmp_file, desired_mask)
138 return tmp_file
139 else: # permissions on keyfile are OK
140 return keyfile
141
142
143class SshConnection(object):
144
145 default_password_prompt = '[sudo] password'
146 max_cancel_attempts = 5
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100147 default_timeout=10
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100148
149 @property
150 def name(self):
151 return self.host
152
153 def __init__(self,
154 host,
155 username,
156 password=None,
157 keyfile=None,
158 port=None,
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100159 timeout=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100160 telnet=False,
161 password_prompt=None,
Gareth Stockwell76b059c2016-07-29 15:47:50 +0100162 original_prompt=None,
Anthony Barbierc837a292017-06-28 15:08:35 +0100163 platform=None,
164 sudo_cmd="sudo -- sh -c '{}'"
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100165 ):
166 self.host = host
167 self.username = username
168 self.password = password
169 self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
170 self.port = port
171 self.lock = threading.Lock()
172 self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
Anthony Barbierc837a292017-06-28 15:08:35 +0100173 self.sudo_cmd = sudo_cmd
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100174 logger.debug('Logging in {}@{}'.format(username, host))
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100175 timeout = timeout if timeout is not None else self.default_timeout
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000176 self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100177
178 def push(self, source, dest, timeout=30):
179 dest = '{}@{}:{}'.format(self.username, self.host, dest)
180 return self._scp(source, dest, timeout)
181
182 def pull(self, source, dest, timeout=30):
183 source = '{}@{}:{}'.format(self.username, self.host, source)
184 return self._scp(source, dest, timeout)
185
Anouk Van Laer29a79402017-01-31 12:48:58 +0000186 def execute(self, command, timeout=None, check_exit_code=True,
187 as_root=False, strip_colors=True): #pylint: disable=unused-argument
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100188 if command == '':
189 # Empty command is valid but the __devlib_ec stuff below will
190 # produce a syntax error with bash. Treat as a special case.
191 return ''
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000192 try:
193 with self.lock:
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100194 _command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command)
195 raw_output = self._execute_and_wait_for_prompt(
196 _command, timeout, as_root, strip_colors)
197 output, exit_code_text, _ = raw_output.rsplit('\r\n', 2)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000198 if check_exit_code:
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000199 try:
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100200 exit_code = int(exit_code_text)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000201 if exit_code:
202 message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
203 raise TargetError(message.format(exit_code, command, output))
204 except (ValueError, IndexError):
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100205 logger.warning(
206 'Could not get exit code for "{}",\ngot: "{}"'\
207 .format(command, exit_code_text))
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000208 return output
209 except EOF:
210 raise TargetError('Connection lost.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100211
Michele Di Giorgio3bf30172016-03-01 11:12:10 +0000212 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000213 try:
214 port_string = '-p {}'.format(self.port) if self.port else ''
215 keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
Michele Di Giorgio3bf30172016-03-01 11:12:10 +0000216 if as_root:
Anthony Barbierc837a292017-06-28 15:08:35 +0100217 command = self.sudo_cmd.format(command)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000218 command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
219 logger.debug(command)
220 if self.password:
221 command = _give_password(self.password, command)
222 return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
223 except EOF:
224 raise TargetError('Connection lost.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100225
226 def close(self):
227 logger.debug('Logging out {}@{}'.format(self.username, self.host))
228 self.conn.logout()
229
230 def cancel_running_command(self):
231 # simulate impatiently hitting ^C until command prompt appears
232 logger.debug('Sending ^C')
233 for _ in xrange(self.max_cancel_attempts):
234 self.conn.sendline(chr(3))
235 if self.conn.prompt(0.1):
236 return True
237 return False
238
239 def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
240 self.conn.prompt(0.1) # clear an existing prompt if there is one.
Sergei Trofimovbd27de12017-04-20 17:00:17 +0100241 if self.username == 'root':
242 # As we're already root, there is no need to use sudo.
243 as_root = False
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100244 if as_root:
Anthony Barbierc837a292017-06-28 15:08:35 +0100245 command = self.sudo_cmd.format(escape_single_quotes(command))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100246 if log:
247 logger.debug(command)
248 self.conn.sendline(command)
249 if self.password:
250 index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5)
251 if index == 0:
252 self.conn.sendline(self.password)
253 else: # not as_root
254 if log:
255 logger.debug(command)
256 self.conn.sendline(command)
257 timed_out = self._wait_for_prompt(timeout)
258 # the regex removes line breaks potential introduced when writing
259 # command to shell.
260 output = process_backspaces(self.conn.before)
261 output = re.sub(r'\r([^\n])', r'\1', output)
262 if '\r\n' in output: # strip the echoed command
263 output = output.split('\r\n', 1)[1]
264 if timed_out:
265 self.cancel_running_command()
266 raise TimeoutError(command, output)
267 if strip_colors:
268 output = strip_bash_colors(output)
269 return output
270
271 def _wait_for_prompt(self, timeout=None):
272 if timeout:
273 return not self.conn.prompt(timeout)
274 else: # cannot timeout; wait forever
275 while not self.conn.prompt(1):
276 pass
277 return False
278
279 def _scp(self, source, dest, timeout=30):
280 # NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely)
281 # fails to connect to a device if port is explicitly specified using -P
282 # option, even if it is the default port, 22. To minimize this problem,
283 # only specify -P for scp if the port is *not* the default.
284 port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else ''
285 keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
286 command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest)
Brendan Jackman93b39a72017-08-09 15:59:24 +0100287 command_redacted = command
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100288 logger.debug(command)
289 if self.password:
290 command = _give_password(self.password, command)
Brendan Jackman93b39a72017-08-09 15:59:24 +0100291 command_redacted = command.replace(self.password, '<redacted>')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100292 try:
293 check_output(command, timeout=timeout, shell=True)
294 except subprocess.CalledProcessError as e:
Brendan Jackman380ad052017-08-09 16:00:46 +0100295 raise HostError("Failed to copy file with '{}'. Output:\n{}".format(
296 command_redacted, e.output))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100297 except TimeoutError as e:
Brendan Jackman93b39a72017-08-09 15:59:24 +0100298 raise TimeoutError(command_redacted, e.output)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100299
300
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000301class TelnetConnection(SshConnection):
302
303 def __init__(self,
304 host,
305 username,
306 password=None,
307 port=None,
308 timeout=None,
309 password_prompt=None,
310 original_prompt=None,
Anouk Van Laer29a79402017-01-31 12:48:58 +0000311 platform=None):
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000312 self.host = host
313 self.username = username
314 self.password = password
315 self.port = port
316 self.keyfile = None
317 self.lock = threading.Lock()
318 self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
319 logger.debug('Logging in {}@{}'.format(username, host))
320 timeout = timeout if timeout is not None else self.default_timeout
321 self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt)
322
323
Anouk Van Laer29a79402017-01-31 12:48:58 +0000324class Gem5Connection(TelnetConnection):
325
326 def __init__(self,
327 platform,
328 host=None,
329 username=None,
330 password=None,
331 port=None,
332 timeout=None,
333 password_prompt=None,
334 original_prompt=None,
335 ):
336 if host is not None:
337 host_system = socket.gethostname()
338 if host_system != host:
339 raise TargetError("Gem5Connection can only connect to gem5 "
340 "simulations on your current host, which "
341 "differs from the one given {}!"
342 .format(host_system, host))
343 if username is not None and username != 'root':
344 raise ValueError('User should be root in gem5!')
345 if password is not None and password != '':
346 raise ValueError('No password needed in gem5!')
347 self.username = 'root'
348 self.is_rooted = True
349 self.password = None
350 self.port = None
351 # Long timeouts to account for gem5 being slow
352 # Can be overriden if the given timeout is longer
353 self.default_timeout = 3600
354 if timeout is not None:
355 if timeout > self.default_timeout:
356 logger.info('Overwriting the default timeout of gem5 ({})'
357 ' to {}'.format(self.default_timeout, timeout))
358 self.default_timeout = timeout
359 else:
360 logger.info('Ignoring the given timeout --> gem5 needs longer timeouts')
361 self.ready_timeout = self.default_timeout * 3
362 # Counterpart in gem5_interact_dir
363 self.gem5_input_dir = '/mnt/host/'
364 # Location of m5 binary in the gem5 simulated system
365 self.m5_path = None
366 # Actual telnet connection to gem5 simulation
367 self.conn = None
368 # Flag to indicate the gem5 device is ready to interact with the
369 # outer world
370 self.ready = False
371 # Lock file to prevent multiple connections to same gem5 simulation
372 # (gem5 does not allow this)
373 self.lock_directory = '/tmp/'
374 self.lock_file_name = None # Will be set once connected to gem5
375
376 # These parameters will be set by either the method to connect to the
377 # gem5 platform or directly to the gem5 simulation
378 # Intermediate directory to push things to gem5 using VirtIO
379 self.gem5_interact_dir = None
380 # Directory to store output from gem5 on the host
381 self.gem5_out_dir = None
382 # Actual gem5 simulation
383 self.gem5simulation = None
384
385 # Connect to gem5
386 if platform:
387 self._connect_gem5_platform(platform)
388
389 # Wait for boot
390 self._wait_for_boot()
391
392 # Mount the virtIO to transfer files in/out gem5 system
393 self._mount_virtio()
394
395 def set_hostinteractdir(self, indir):
396 logger.info('Setting hostinteractdir from {} to {}'
397 .format(self.gem5_input_dir, indir))
398 self.gem5_input_dir = indir
399
400 def push(self, source, dest, timeout=None):
401 """
402 Push a file to the gem5 device using VirtIO
403
404 The file to push to the device is copied to the temporary directory on
405 the host, before being copied within the simulation to the destination.
406 Checks, in the form of 'ls' with error code checking, are performed to
407 ensure that the file is copied to the destination.
408 """
409 # First check if the connection is set up to interact with gem5
410 self._check_ready()
411
412 filename = os.path.basename(source)
413 logger.debug("Pushing {} to device.".format(source))
414 logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir))
415 logger.debug("dest: {}".format(dest))
416 logger.debug("filename: {}".format(filename))
417
418 # We need to copy the file to copy to the temporary directory
419 self._move_to_temp_dir(source)
420
421 # Dest in gem5 world is a file rather than directory
422 if os.path.basename(dest) != filename:
423 dest = os.path.join(dest, filename)
424 # Back to the gem5 world
425 self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename))
426 self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir,
427 filename,
428 dest))
429 self._gem5_shell("sync")
430 self._gem5_shell("ls -al {}".format(dest))
431 self._gem5_shell("ls -al {}".format(self.gem5_input_dir))
432 logger.debug("Push complete.")
433
434 def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument
435 """
436 Pull a file from the gem5 device using m5 writefile
437
438 The file is copied to the local directory within the guest as the m5
439 writefile command assumes that the file is local. The file is then
440 written out to the host system using writefile, prior to being moved to
441 the destination on the host.
442 """
443 # First check if the connection is set up to interact with gem5
444 self._check_ready()
445
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100446 result = self._gem5_shell("ls {}".format(source))
447 files = result.split()
Anouk Van Laer29a79402017-01-31 12:48:58 +0000448
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100449 for filename in files:
450 dest_file = os.path.basename(filename)
451 logger.debug("pull_file {} {}".format(filename, dest_file))
452 # writefile needs the file to be copied to be in the current
453 # working directory so if needed, copy to the working directory
454 # We don't check the exit code here because it is non-zero if the
455 # source and destination are the same. The ls below will cause an
456 # error if the file was not where we expected it to be.
457 if os.path.isabs(source):
458 if os.path.dirname(source) != self.execute('pwd',
459 check_exit_code=False):
460 self._gem5_shell("cat '{}' > '{}'".format(filename,
461 dest_file))
462 self._gem5_shell("sync")
463 self._gem5_shell("ls -la {}".format(dest_file))
464 logger.debug('Finished the copy in the simulator')
465 self._gem5_util("writefile {}".format(dest_file))
Anouk Van Laer29a79402017-01-31 12:48:58 +0000466
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100467 if 'cpu' not in filename:
468 while not os.path.exists(os.path.join(self.gem5_out_dir,
469 dest_file)):
470 time.sleep(1)
Anouk Van Laer29a79402017-01-31 12:48:58 +0000471
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100472 # Perform the local move
473 if os.path.exists(os.path.join(dest, dest_file)):
474 logger.warning(
475 'Destination file {} already exists!'\
476 .format(dest_file))
477 else:
478 shutil.move(os.path.join(self.gem5_out_dir, dest_file), dest)
479 logger.debug("Pull complete.")
Anouk Van Laer29a79402017-01-31 12:48:58 +0000480
481 def execute(self, command, timeout=1000, check_exit_code=True,
482 as_root=False, strip_colors=True):
483 """
484 Execute a command on the gem5 platform
485 """
486 # First check if the connection is set up to interact with gem5
487 self._check_ready()
488
Sascha Bischofff5906cb2017-05-11 10:28:05 +0100489 output = self._gem5_shell(command,
490 check_exit_code=check_exit_code,
491 as_root=as_root)
Anouk Van Laer29a79402017-01-31 12:48:58 +0000492 if strip_colors:
493 output = strip_bash_colors(output)
494 return output
495
496 def background(self, command, stdout=subprocess.PIPE,
497 stderr=subprocess.PIPE, as_root=False):
498 # First check if the connection is set up to interact with gem5
499 self._check_ready()
500
501 # Create the logfile for stderr/stdout redirection
502 command_name = command.split(' ')[0].split('/')[-1]
503 redirection_file = 'BACKGROUND_{}.log'.format(command_name)
504 trial = 0
505 while os.path.isfile(redirection_file):
506 # Log file already exists so add to name
507 redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial)
508 trial += 1
509
510 # Create the command to pass on to gem5 shell
511 complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file)
512 output = self._gem5_shell(complete_command, as_root=as_root)
513 output = strip_bash_colors(output)
514 gem5_logger.info('STDERR/STDOUT of background command will be '
515 'redirected to {}. Use target.pull() to '
516 'get this file'.format(redirection_file))
517 return output
518
519 def close(self):
520 """
521 Close and disconnect from the gem5 simulation. Additionally, we remove
522 the temporary directory used to pass files into the simulation.
523 """
524 gem5_logger.info("Gracefully terminating the gem5 simulation.")
525 try:
526 self._gem5_util("exit")
527 self.gem5simulation.wait()
528 except EOF:
529 pass
530 gem5_logger.info("Removing the temporary directory")
531 try:
532 shutil.rmtree(self.gem5_interact_dir)
533 except OSError:
534 gem5_logger.warn("Failed to remove the temporary directory!")
535
536 # Delete the lock file
537 os.remove(self.lock_file_name)
538
539 # Functions only to be called by the Gem5 connection itself
540 def _connect_gem5_platform(self, platform):
541 port = platform.gem5_port
542 gem5_simulation = platform.gem5
543 gem5_interact_dir = platform.gem5_interact_dir
544 gem5_out_dir = platform.gem5_out_dir
545
546 self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir)
547
548 # This function connects to the gem5 simulation
549 def connect_gem5(self, port, gem5_simulation, gem5_interact_dir,
550 gem5_out_dir):
551 """
552 Connect to the telnet port of the gem5 simulation.
553
554 We connect, and wait for the prompt to be found. We do not use a timeout
555 for this, and wait for the prompt in a while loop as the gem5 simulation
556 can take many hours to reach a prompt when booting the system. We also
557 inject some newlines periodically to try and force gem5 to show a
558 prompt. Once the prompt has been found, we replace it with a unique
559 prompt to ensure that we are able to match it properly. We also disable
560 the echo as this simplifies parsing the output when executing commands
561 on the device.
562 """
563 host = socket.gethostname()
564 gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port))
565
566 # Check if there is no on-going connection yet
567 lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port)
568 if os.path.isfile(lock_file_name):
569 # There is already a connection to this gem5 simulation
570 raise TargetError('There is already a connection to the gem5 '
571 'simulation using port {} on {}!'
572 .format(port, host))
573
574 # Connect to the gem5 telnet port. Use a short timeout here.
575 attempts = 0
576 while attempts < 10:
577 attempts += 1
578 try:
579 self.conn = TelnetPxssh(original_prompt=None)
580 self.conn.login(host, self.username, port=port,
581 login_timeout=10, auto_prompt_reset=False)
582 break
583 except pxssh.ExceptionPxssh:
584 pass
585 else:
586 gem5_simulation.kill()
587 raise TargetError("Failed to connect to the gem5 telnet session.")
588
589 gem5_logger.info("Connected! Waiting for prompt...")
590
591 # Create the lock file
592 self.lock_file_name = lock_file_name
593 open(self.lock_file_name, 'w').close() # Similar to touch
594 gem5_logger.info("Created lock file {} to prevent reconnecting to "
595 "same simulation".format(self.lock_file_name))
596
597 # We need to find the prompt. It might be different if we are resuming
598 # from a checkpoint. Therefore, we test multiple options here.
599 prompt_found = False
600 while not prompt_found:
601 try:
602 self._login_to_device()
603 except TIMEOUT:
604 pass
605 try:
606 # Try and force a prompt to be shown
607 self.conn.send('\n')
608 self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60)
609 prompt_found = True
610 except TIMEOUT:
611 pass
612
613 gem5_logger.info("Successfully logged in")
614 gem5_logger.info("Setting unique prompt...")
615
616 self.conn.set_unique_prompt()
617 self.conn.prompt()
618 gem5_logger.info("Prompt found and replaced with a unique string")
619
620 # We check that the prompt is what we think it should be. If not, we
621 # need to update the regex we use to match.
622 self._find_prompt()
623
624 self.conn.setecho(False)
625 self._sync_gem5_shell()
626
627 # Fully connected to gem5 simulation
628 self.gem5_interact_dir = gem5_interact_dir
629 self.gem5_out_dir = gem5_out_dir
630 self.gem5simulation = gem5_simulation
631
632 # Ready for interaction now
633 self.ready = True
634
635 def _login_to_device(self):
636 """
637 Login to device, will be overwritten if there is an actual login
638 """
639 pass
640
641 def _find_prompt(self):
642 prompt = r'\[PEXPECT\][\\\$\#]+ '
643 synced = False
644 while not synced:
645 self.conn.send('\n')
646 i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout)
647 if i == 0:
648 synced = True
649 elif i == 1:
650 prompt = self.conn.UNIQUE_PROMPT
651 synced = True
652 else:
653 prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip())
654 prompt = re.sub(r'\#', r'\\\#', prompt)
655 prompt = re.sub(r'\[', r'\[', prompt)
656 prompt = re.sub(r'\]', r'\]', prompt)
657
658 self.conn.PROMPT = prompt
659
660 def _sync_gem5_shell(self):
661 """
662 Synchronise with the gem5 shell.
663
664 Write some unique text to the gem5 device to allow us to synchronise
665 with the shell output. We actually get two prompts so we need to match
666 both of these.
667 """
668 gem5_logger.debug("Sending Sync")
669 self.conn.send("echo \*\*sync\*\*\n")
670 self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout)
671 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
672 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
673
674 def _gem5_util(self, command):
675 """ Execute a gem5 utility command using the m5 binary on the device """
676 if self.m5_path is None:
677 raise TargetError('Path to m5 binary on simulated system is not set!')
678 self._gem5_shell('{} {}'.format(self.m5_path, command))
679
680 def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912
681 """
682 Execute a command in the gem5 shell
683
684 This wraps the telnet connection to gem5 and processes the raw output.
685
686 This method waits for the shell to return, and then will try and
687 separate the output from the command from the command itself. If this
688 fails, warn, but continue with the potentially wrong output.
689
690 The exit code is also checked by default, and non-zero exit codes will
691 raise a TargetError.
692 """
693 if sync:
694 self._sync_gem5_shell()
695
696 gem5_logger.debug("gem5_shell command: {}".format(command))
697
698 # Send the actual command
699 self.conn.send("{}\n".format(command))
700
701 # Wait for the response. We just sit here and wait for the prompt to
702 # appear, as gem5 might take a long time to provide the output. This
703 # avoids timeout issues.
704 command_index = -1
705 while command_index == -1:
706 if self.conn.prompt():
707 output = re.sub(r' \r([^\n])', r'\1', self.conn.before)
708 output = re.sub(r'[\b]', r'', output)
709 # Deal with line wrapping
710 output = re.sub(r'[\r].+?<', r'', output)
711 command_index = output.find(command)
712
713 # If we have -1, then we cannot match the command, but the
714 # prompt has returned. Hence, we have a bit of an issue. We
715 # warn, and return the whole output.
716 if command_index == -1:
717 gem5_logger.warn("gem5_shell: Unable to match command in "
718 "command output. Expect parsing errors!")
719 command_index = 0
720
721 output = output[command_index + len(command):].strip()
722
723 # It is possible that gem5 will echo the command. Therefore, we need to
724 # remove that too!
725 command_index = output.find(command)
726 if command_index != -1:
727 output = output[command_index + len(command):].strip()
728
729 gem5_logger.debug("gem5_shell output: {}".format(output))
730
731 # We get a second prompt. Hence, we need to eat one to make sure that we
732 # stay in sync. If we do not do this, we risk getting out of sync for
733 # slower simulations.
734 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
735
736 if check_exit_code:
737 exit_code_text = self._gem5_shell('echo $?', as_root=as_root,
738 timeout=timeout, check_exit_code=False,
739 sync=False)
740 try:
741 exit_code = int(exit_code_text.split()[0])
742 if exit_code:
743 message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
744 raise TargetError(message.format(exit_code, command, output))
745 except (ValueError, IndexError):
746 gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
747
748 return output
749
750 def _mount_virtio(self):
751 """
752 Mount the VirtIO device in the simulated system.
753 """
754 gem5_logger.info("Mounting VirtIO device in simulated system")
755
756 self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir))
757 mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir)
758 self._gem5_shell(mount_command)
759
760 def _move_to_temp_dir(self, source):
761 """
762 Move a file to the temporary directory on the host for copying to the
763 gem5 device
764 """
765 command = "cp {} {}".format(source, self.gem5_interact_dir)
766 gem5_logger.debug("Local copy command: {}".format(command))
767 subprocess.call(command.split())
768 subprocess.call("sync".split())
769
770 def _check_ready(self):
771 """
772 Check if the gem5 platform is ready
773 """
774 if not self.ready:
775 raise TargetError('Gem5 is not ready to interact yet')
776
777 def _wait_for_boot(self):
778 pass
779
780 def _probe_file(self, filepath):
781 """
782 Internal method to check if the target has a certain file
783 """
784 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
785 output = self.execute(command.format(filepath), as_root=self.is_rooted)
786 return boolean(output.strip())
787
788
789class LinuxGem5Connection(Gem5Connection):
790
791 def _login_to_device(self):
792 gem5_logger.info("Trying to log in to gem5 device")
793 login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:']
794 login_password_prompt = ['password:']
795 # Wait for the login prompt
796 prompt = login_prompt + [self.conn.UNIQUE_PROMPT]
797 i = self.conn.expect(prompt, timeout=10)
798 # Check if we are already at a prompt, or if we need to log in.
799 if i < len(prompt) - 1:
800 self.conn.sendline("{}".format(self.username))
801 password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT]
802 j = self.conn.expect(password_prompt, timeout=self.default_timeout)
803 if j < len(password_prompt) - 2:
804 self.conn.sendline("{}".format(self.password))
805 self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout)
806
807
808
809class AndroidGem5Connection(Gem5Connection):
810
811 def _wait_for_boot(self):
812 """
813 Wait for the system to boot
814
815 We monitor the sys.boot_completed and service.bootanim.exit system
816 properties to determine when the system has finished booting. In the
817 event that we cannot coerce the result of service.bootanim.exit to an
818 integer, we assume that the boot animation was disabled and do not wait
819 for it to finish.
820
821 """
822 gem5_logger.info("Waiting for Android to boot...")
823 while True:
824 booted = False
825 anim_finished = True # Assume boot animation was disabled on except
826 try:
827 booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1)
828 anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1)
829 except ValueError:
830 pass
831 if booted and anim_finished:
832 break
833 time.sleep(60)
834
835 gem5_logger.info("Android booted")
836
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100837def _give_password(password, command):
838 if not sshpass:
839 raise HostError('Must have sshpass installed on the host in order to use password-based auth.')
840 pass_string = "sshpass -p '{}' ".format(password)
841 return pass_string + command
842
843
844def _check_env():
845 global ssh, scp, sshpass # pylint: disable=global-statement
846 if not ssh:
847 ssh = which('ssh')
848 scp = which('scp')
849 sshpass = which('sshpass')
850 if not (ssh and scp):
851 raise HostError('OpenSSH must be installed on the host.')
852
853
854def process_backspaces(text):
855 chars = []
856 for c in text:
857 if c == chr(8) and chars: # backspace
858 chars.pop()
859 else:
860 chars.append(c)
861 return ''.join(chars)