blob: 8704008675aa7b0b776f402ee7739eb818d928a7 [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,
Anouk Van Laer29a79402017-01-31 12:48:58 +0000163 platform=None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100164 ):
165 self.host = host
166 self.username = username
167 self.password = password
168 self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
169 self.port = port
170 self.lock = threading.Lock()
171 self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
172 logger.debug('Logging in {}@{}'.format(username, host))
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100173 timeout = timeout if timeout is not None else self.default_timeout
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000174 self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100175
176 def push(self, source, dest, timeout=30):
177 dest = '{}@{}:{}'.format(self.username, self.host, dest)
178 return self._scp(source, dest, timeout)
179
180 def pull(self, source, dest, timeout=30):
181 source = '{}@{}:{}'.format(self.username, self.host, source)
182 return self._scp(source, dest, timeout)
183
Anouk Van Laer29a79402017-01-31 12:48:58 +0000184 def execute(self, command, timeout=None, check_exit_code=True,
185 as_root=False, strip_colors=True): #pylint: disable=unused-argument
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100186 if command == '':
187 # Empty command is valid but the __devlib_ec stuff below will
188 # produce a syntax error with bash. Treat as a special case.
189 return ''
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000190 try:
191 with self.lock:
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100192 _command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command)
193 raw_output = self._execute_and_wait_for_prompt(
194 _command, timeout, as_root, strip_colors)
195 output, exit_code_text, _ = raw_output.rsplit('\r\n', 2)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000196 if check_exit_code:
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000197 try:
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100198 exit_code = int(exit_code_text)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000199 if exit_code:
200 message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
201 raise TargetError(message.format(exit_code, command, output))
202 except (ValueError, IndexError):
Brendan Jackman0a20cec2017-05-03 10:53:43 +0100203 logger.warning(
204 'Could not get exit code for "{}",\ngot: "{}"'\
205 .format(command, exit_code_text))
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000206 return output
207 except EOF:
208 raise TargetError('Connection lost.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100209
Michele Di Giorgio3bf30172016-03-01 11:12:10 +0000210 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000211 try:
212 port_string = '-p {}'.format(self.port) if self.port else ''
213 keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
Michele Di Giorgio3bf30172016-03-01 11:12:10 +0000214 if as_root:
215 command = "sudo -- sh -c '{}'".format(command)
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000216 command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
217 logger.debug(command)
218 if self.password:
219 command = _give_password(self.password, command)
220 return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
221 except EOF:
222 raise TargetError('Connection lost.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100223
224 def close(self):
225 logger.debug('Logging out {}@{}'.format(self.username, self.host))
226 self.conn.logout()
227
228 def cancel_running_command(self):
229 # simulate impatiently hitting ^C until command prompt appears
230 logger.debug('Sending ^C')
231 for _ in xrange(self.max_cancel_attempts):
232 self.conn.sendline(chr(3))
233 if self.conn.prompt(0.1):
234 return True
235 return False
236
237 def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
238 self.conn.prompt(0.1) # clear an existing prompt if there is one.
Sergei Trofimovbd27de12017-04-20 17:00:17 +0100239 if self.username == 'root':
240 # As we're already root, there is no need to use sudo.
241 as_root = False
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100242 if as_root:
243 command = "sudo -- sh -c '{}'".format(escape_single_quotes(command))
244 if log:
245 logger.debug(command)
246 self.conn.sendline(command)
247 if self.password:
248 index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5)
249 if index == 0:
250 self.conn.sendline(self.password)
251 else: # not as_root
252 if log:
253 logger.debug(command)
254 self.conn.sendline(command)
255 timed_out = self._wait_for_prompt(timeout)
256 # the regex removes line breaks potential introduced when writing
257 # command to shell.
258 output = process_backspaces(self.conn.before)
259 output = re.sub(r'\r([^\n])', r'\1', output)
260 if '\r\n' in output: # strip the echoed command
261 output = output.split('\r\n', 1)[1]
262 if timed_out:
263 self.cancel_running_command()
264 raise TimeoutError(command, output)
265 if strip_colors:
266 output = strip_bash_colors(output)
267 return output
268
269 def _wait_for_prompt(self, timeout=None):
270 if timeout:
271 return not self.conn.prompt(timeout)
272 else: # cannot timeout; wait forever
273 while not self.conn.prompt(1):
274 pass
275 return False
276
277 def _scp(self, source, dest, timeout=30):
278 # NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely)
279 # fails to connect to a device if port is explicitly specified using -P
280 # option, even if it is the default port, 22. To minimize this problem,
281 # only specify -P for scp if the port is *not* the default.
282 port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else ''
283 keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
284 command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest)
285 pass_string = ''
286 logger.debug(command)
287 if self.password:
288 command = _give_password(self.password, command)
289 try:
290 check_output(command, timeout=timeout, shell=True)
291 except subprocess.CalledProcessError as e:
292 raise subprocess.CalledProcessError(e.returncode, e.cmd.replace(pass_string, ''), e.output)
293 except TimeoutError as e:
294 raise TimeoutError(e.command.replace(pass_string, ''), e.output)
295
296
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000297class TelnetConnection(SshConnection):
298
299 def __init__(self,
300 host,
301 username,
302 password=None,
303 port=None,
304 timeout=None,
305 password_prompt=None,
306 original_prompt=None,
Anouk Van Laer29a79402017-01-31 12:48:58 +0000307 platform=None):
Sergei Trofimovb3cea0c2016-12-08 11:48:41 +0000308 self.host = host
309 self.username = username
310 self.password = password
311 self.port = port
312 self.keyfile = None
313 self.lock = threading.Lock()
314 self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
315 logger.debug('Logging in {}@{}'.format(username, host))
316 timeout = timeout if timeout is not None else self.default_timeout
317 self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt)
318
319
Anouk Van Laer29a79402017-01-31 12:48:58 +0000320class Gem5Connection(TelnetConnection):
321
322 def __init__(self,
323 platform,
324 host=None,
325 username=None,
326 password=None,
327 port=None,
328 timeout=None,
329 password_prompt=None,
330 original_prompt=None,
331 ):
332 if host is not None:
333 host_system = socket.gethostname()
334 if host_system != host:
335 raise TargetError("Gem5Connection can only connect to gem5 "
336 "simulations on your current host, which "
337 "differs from the one given {}!"
338 .format(host_system, host))
339 if username is not None and username != 'root':
340 raise ValueError('User should be root in gem5!')
341 if password is not None and password != '':
342 raise ValueError('No password needed in gem5!')
343 self.username = 'root'
344 self.is_rooted = True
345 self.password = None
346 self.port = None
347 # Long timeouts to account for gem5 being slow
348 # Can be overriden if the given timeout is longer
349 self.default_timeout = 3600
350 if timeout is not None:
351 if timeout > self.default_timeout:
352 logger.info('Overwriting the default timeout of gem5 ({})'
353 ' to {}'.format(self.default_timeout, timeout))
354 self.default_timeout = timeout
355 else:
356 logger.info('Ignoring the given timeout --> gem5 needs longer timeouts')
357 self.ready_timeout = self.default_timeout * 3
358 # Counterpart in gem5_interact_dir
359 self.gem5_input_dir = '/mnt/host/'
360 # Location of m5 binary in the gem5 simulated system
361 self.m5_path = None
362 # Actual telnet connection to gem5 simulation
363 self.conn = None
364 # Flag to indicate the gem5 device is ready to interact with the
365 # outer world
366 self.ready = False
367 # Lock file to prevent multiple connections to same gem5 simulation
368 # (gem5 does not allow this)
369 self.lock_directory = '/tmp/'
370 self.lock_file_name = None # Will be set once connected to gem5
371
372 # These parameters will be set by either the method to connect to the
373 # gem5 platform or directly to the gem5 simulation
374 # Intermediate directory to push things to gem5 using VirtIO
375 self.gem5_interact_dir = None
376 # Directory to store output from gem5 on the host
377 self.gem5_out_dir = None
378 # Actual gem5 simulation
379 self.gem5simulation = None
380
381 # Connect to gem5
382 if platform:
383 self._connect_gem5_platform(platform)
384
385 # Wait for boot
386 self._wait_for_boot()
387
388 # Mount the virtIO to transfer files in/out gem5 system
389 self._mount_virtio()
390
391 def set_hostinteractdir(self, indir):
392 logger.info('Setting hostinteractdir from {} to {}'
393 .format(self.gem5_input_dir, indir))
394 self.gem5_input_dir = indir
395
396 def push(self, source, dest, timeout=None):
397 """
398 Push a file to the gem5 device using VirtIO
399
400 The file to push to the device is copied to the temporary directory on
401 the host, before being copied within the simulation to the destination.
402 Checks, in the form of 'ls' with error code checking, are performed to
403 ensure that the file is copied to the destination.
404 """
405 # First check if the connection is set up to interact with gem5
406 self._check_ready()
407
408 filename = os.path.basename(source)
409 logger.debug("Pushing {} to device.".format(source))
410 logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir))
411 logger.debug("dest: {}".format(dest))
412 logger.debug("filename: {}".format(filename))
413
414 # We need to copy the file to copy to the temporary directory
415 self._move_to_temp_dir(source)
416
417 # Dest in gem5 world is a file rather than directory
418 if os.path.basename(dest) != filename:
419 dest = os.path.join(dest, filename)
420 # Back to the gem5 world
421 self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename))
422 self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir,
423 filename,
424 dest))
425 self._gem5_shell("sync")
426 self._gem5_shell("ls -al {}".format(dest))
427 self._gem5_shell("ls -al {}".format(self.gem5_input_dir))
428 logger.debug("Push complete.")
429
430 def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument
431 """
432 Pull a file from the gem5 device using m5 writefile
433
434 The file is copied to the local directory within the guest as the m5
435 writefile command assumes that the file is local. The file is then
436 written out to the host system using writefile, prior to being moved to
437 the destination on the host.
438 """
439 # First check if the connection is set up to interact with gem5
440 self._check_ready()
441
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100442 result = self._gem5_shell("ls {}".format(source))
443 files = result.split()
Anouk Van Laer29a79402017-01-31 12:48:58 +0000444
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100445 for filename in files:
446 dest_file = os.path.basename(filename)
447 logger.debug("pull_file {} {}".format(filename, dest_file))
448 # writefile needs the file to be copied to be in the current
449 # working directory so if needed, copy to the working directory
450 # We don't check the exit code here because it is non-zero if the
451 # source and destination are the same. The ls below will cause an
452 # error if the file was not where we expected it to be.
453 if os.path.isabs(source):
454 if os.path.dirname(source) != self.execute('pwd',
455 check_exit_code=False):
456 self._gem5_shell("cat '{}' > '{}'".format(filename,
457 dest_file))
458 self._gem5_shell("sync")
459 self._gem5_shell("ls -la {}".format(dest_file))
460 logger.debug('Finished the copy in the simulator')
461 self._gem5_util("writefile {}".format(dest_file))
Anouk Van Laer29a79402017-01-31 12:48:58 +0000462
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100463 if 'cpu' not in filename:
464 while not os.path.exists(os.path.join(self.gem5_out_dir,
465 dest_file)):
466 time.sleep(1)
Anouk Van Laer29a79402017-01-31 12:48:58 +0000467
Ionela Voinescucac70cb2017-05-23 16:45:29 +0100468 # Perform the local move
469 if os.path.exists(os.path.join(dest, dest_file)):
470 logger.warning(
471 'Destination file {} already exists!'\
472 .format(dest_file))
473 else:
474 shutil.move(os.path.join(self.gem5_out_dir, dest_file), dest)
475 logger.debug("Pull complete.")
Anouk Van Laer29a79402017-01-31 12:48:58 +0000476
477 def execute(self, command, timeout=1000, check_exit_code=True,
478 as_root=False, strip_colors=True):
479 """
480 Execute a command on the gem5 platform
481 """
482 # First check if the connection is set up to interact with gem5
483 self._check_ready()
484
Sascha Bischofff5906cb2017-05-11 10:28:05 +0100485 output = self._gem5_shell(command,
486 check_exit_code=check_exit_code,
487 as_root=as_root)
Anouk Van Laer29a79402017-01-31 12:48:58 +0000488 if strip_colors:
489 output = strip_bash_colors(output)
490 return output
491
492 def background(self, command, stdout=subprocess.PIPE,
493 stderr=subprocess.PIPE, as_root=False):
494 # First check if the connection is set up to interact with gem5
495 self._check_ready()
496
497 # Create the logfile for stderr/stdout redirection
498 command_name = command.split(' ')[0].split('/')[-1]
499 redirection_file = 'BACKGROUND_{}.log'.format(command_name)
500 trial = 0
501 while os.path.isfile(redirection_file):
502 # Log file already exists so add to name
503 redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial)
504 trial += 1
505
506 # Create the command to pass on to gem5 shell
507 complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file)
508 output = self._gem5_shell(complete_command, as_root=as_root)
509 output = strip_bash_colors(output)
510 gem5_logger.info('STDERR/STDOUT of background command will be '
511 'redirected to {}. Use target.pull() to '
512 'get this file'.format(redirection_file))
513 return output
514
515 def close(self):
516 """
517 Close and disconnect from the gem5 simulation. Additionally, we remove
518 the temporary directory used to pass files into the simulation.
519 """
520 gem5_logger.info("Gracefully terminating the gem5 simulation.")
521 try:
522 self._gem5_util("exit")
523 self.gem5simulation.wait()
524 except EOF:
525 pass
526 gem5_logger.info("Removing the temporary directory")
527 try:
528 shutil.rmtree(self.gem5_interact_dir)
529 except OSError:
530 gem5_logger.warn("Failed to remove the temporary directory!")
531
532 # Delete the lock file
533 os.remove(self.lock_file_name)
534
535 # Functions only to be called by the Gem5 connection itself
536 def _connect_gem5_platform(self, platform):
537 port = platform.gem5_port
538 gem5_simulation = platform.gem5
539 gem5_interact_dir = platform.gem5_interact_dir
540 gem5_out_dir = platform.gem5_out_dir
541
542 self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir)
543
544 # This function connects to the gem5 simulation
545 def connect_gem5(self, port, gem5_simulation, gem5_interact_dir,
546 gem5_out_dir):
547 """
548 Connect to the telnet port of the gem5 simulation.
549
550 We connect, and wait for the prompt to be found. We do not use a timeout
551 for this, and wait for the prompt in a while loop as the gem5 simulation
552 can take many hours to reach a prompt when booting the system. We also
553 inject some newlines periodically to try and force gem5 to show a
554 prompt. Once the prompt has been found, we replace it with a unique
555 prompt to ensure that we are able to match it properly. We also disable
556 the echo as this simplifies parsing the output when executing commands
557 on the device.
558 """
559 host = socket.gethostname()
560 gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port))
561
562 # Check if there is no on-going connection yet
563 lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port)
564 if os.path.isfile(lock_file_name):
565 # There is already a connection to this gem5 simulation
566 raise TargetError('There is already a connection to the gem5 '
567 'simulation using port {} on {}!'
568 .format(port, host))
569
570 # Connect to the gem5 telnet port. Use a short timeout here.
571 attempts = 0
572 while attempts < 10:
573 attempts += 1
574 try:
575 self.conn = TelnetPxssh(original_prompt=None)
576 self.conn.login(host, self.username, port=port,
577 login_timeout=10, auto_prompt_reset=False)
578 break
579 except pxssh.ExceptionPxssh:
580 pass
581 else:
582 gem5_simulation.kill()
583 raise TargetError("Failed to connect to the gem5 telnet session.")
584
585 gem5_logger.info("Connected! Waiting for prompt...")
586
587 # Create the lock file
588 self.lock_file_name = lock_file_name
589 open(self.lock_file_name, 'w').close() # Similar to touch
590 gem5_logger.info("Created lock file {} to prevent reconnecting to "
591 "same simulation".format(self.lock_file_name))
592
593 # We need to find the prompt. It might be different if we are resuming
594 # from a checkpoint. Therefore, we test multiple options here.
595 prompt_found = False
596 while not prompt_found:
597 try:
598 self._login_to_device()
599 except TIMEOUT:
600 pass
601 try:
602 # Try and force a prompt to be shown
603 self.conn.send('\n')
604 self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60)
605 prompt_found = True
606 except TIMEOUT:
607 pass
608
609 gem5_logger.info("Successfully logged in")
610 gem5_logger.info("Setting unique prompt...")
611
612 self.conn.set_unique_prompt()
613 self.conn.prompt()
614 gem5_logger.info("Prompt found and replaced with a unique string")
615
616 # We check that the prompt is what we think it should be. If not, we
617 # need to update the regex we use to match.
618 self._find_prompt()
619
620 self.conn.setecho(False)
621 self._sync_gem5_shell()
622
623 # Fully connected to gem5 simulation
624 self.gem5_interact_dir = gem5_interact_dir
625 self.gem5_out_dir = gem5_out_dir
626 self.gem5simulation = gem5_simulation
627
628 # Ready for interaction now
629 self.ready = True
630
631 def _login_to_device(self):
632 """
633 Login to device, will be overwritten if there is an actual login
634 """
635 pass
636
637 def _find_prompt(self):
638 prompt = r'\[PEXPECT\][\\\$\#]+ '
639 synced = False
640 while not synced:
641 self.conn.send('\n')
642 i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout)
643 if i == 0:
644 synced = True
645 elif i == 1:
646 prompt = self.conn.UNIQUE_PROMPT
647 synced = True
648 else:
649 prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip())
650 prompt = re.sub(r'\#', r'\\\#', prompt)
651 prompt = re.sub(r'\[', r'\[', prompt)
652 prompt = re.sub(r'\]', r'\]', prompt)
653
654 self.conn.PROMPT = prompt
655
656 def _sync_gem5_shell(self):
657 """
658 Synchronise with the gem5 shell.
659
660 Write some unique text to the gem5 device to allow us to synchronise
661 with the shell output. We actually get two prompts so we need to match
662 both of these.
663 """
664 gem5_logger.debug("Sending Sync")
665 self.conn.send("echo \*\*sync\*\*\n")
666 self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout)
667 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
668 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
669
670 def _gem5_util(self, command):
671 """ Execute a gem5 utility command using the m5 binary on the device """
672 if self.m5_path is None:
673 raise TargetError('Path to m5 binary on simulated system is not set!')
674 self._gem5_shell('{} {}'.format(self.m5_path, command))
675
676 def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912
677 """
678 Execute a command in the gem5 shell
679
680 This wraps the telnet connection to gem5 and processes the raw output.
681
682 This method waits for the shell to return, and then will try and
683 separate the output from the command from the command itself. If this
684 fails, warn, but continue with the potentially wrong output.
685
686 The exit code is also checked by default, and non-zero exit codes will
687 raise a TargetError.
688 """
689 if sync:
690 self._sync_gem5_shell()
691
692 gem5_logger.debug("gem5_shell command: {}".format(command))
693
694 # Send the actual command
695 self.conn.send("{}\n".format(command))
696
697 # Wait for the response. We just sit here and wait for the prompt to
698 # appear, as gem5 might take a long time to provide the output. This
699 # avoids timeout issues.
700 command_index = -1
701 while command_index == -1:
702 if self.conn.prompt():
703 output = re.sub(r' \r([^\n])', r'\1', self.conn.before)
704 output = re.sub(r'[\b]', r'', output)
705 # Deal with line wrapping
706 output = re.sub(r'[\r].+?<', r'', output)
707 command_index = output.find(command)
708
709 # If we have -1, then we cannot match the command, but the
710 # prompt has returned. Hence, we have a bit of an issue. We
711 # warn, and return the whole output.
712 if command_index == -1:
713 gem5_logger.warn("gem5_shell: Unable to match command in "
714 "command output. Expect parsing errors!")
715 command_index = 0
716
717 output = output[command_index + len(command):].strip()
718
719 # It is possible that gem5 will echo the command. Therefore, we need to
720 # remove that too!
721 command_index = output.find(command)
722 if command_index != -1:
723 output = output[command_index + len(command):].strip()
724
725 gem5_logger.debug("gem5_shell output: {}".format(output))
726
727 # We get a second prompt. Hence, we need to eat one to make sure that we
728 # stay in sync. If we do not do this, we risk getting out of sync for
729 # slower simulations.
730 self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
731
732 if check_exit_code:
733 exit_code_text = self._gem5_shell('echo $?', as_root=as_root,
734 timeout=timeout, check_exit_code=False,
735 sync=False)
736 try:
737 exit_code = int(exit_code_text.split()[0])
738 if exit_code:
739 message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
740 raise TargetError(message.format(exit_code, command, output))
741 except (ValueError, IndexError):
742 gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
743
744 return output
745
746 def _mount_virtio(self):
747 """
748 Mount the VirtIO device in the simulated system.
749 """
750 gem5_logger.info("Mounting VirtIO device in simulated system")
751
752 self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir))
753 mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir)
754 self._gem5_shell(mount_command)
755
756 def _move_to_temp_dir(self, source):
757 """
758 Move a file to the temporary directory on the host for copying to the
759 gem5 device
760 """
761 command = "cp {} {}".format(source, self.gem5_interact_dir)
762 gem5_logger.debug("Local copy command: {}".format(command))
763 subprocess.call(command.split())
764 subprocess.call("sync".split())
765
766 def _check_ready(self):
767 """
768 Check if the gem5 platform is ready
769 """
770 if not self.ready:
771 raise TargetError('Gem5 is not ready to interact yet')
772
773 def _wait_for_boot(self):
774 pass
775
776 def _probe_file(self, filepath):
777 """
778 Internal method to check if the target has a certain file
779 """
780 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
781 output = self.execute(command.format(filepath), as_root=self.is_rooted)
782 return boolean(output.strip())
783
784
785class LinuxGem5Connection(Gem5Connection):
786
787 def _login_to_device(self):
788 gem5_logger.info("Trying to log in to gem5 device")
789 login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:']
790 login_password_prompt = ['password:']
791 # Wait for the login prompt
792 prompt = login_prompt + [self.conn.UNIQUE_PROMPT]
793 i = self.conn.expect(prompt, timeout=10)
794 # Check if we are already at a prompt, or if we need to log in.
795 if i < len(prompt) - 1:
796 self.conn.sendline("{}".format(self.username))
797 password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT]
798 j = self.conn.expect(password_prompt, timeout=self.default_timeout)
799 if j < len(password_prompt) - 2:
800 self.conn.sendline("{}".format(self.password))
801 self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout)
802
803
804
805class AndroidGem5Connection(Gem5Connection):
806
807 def _wait_for_boot(self):
808 """
809 Wait for the system to boot
810
811 We monitor the sys.boot_completed and service.bootanim.exit system
812 properties to determine when the system has finished booting. In the
813 event that we cannot coerce the result of service.bootanim.exit to an
814 integer, we assume that the boot animation was disabled and do not wait
815 for it to finish.
816
817 """
818 gem5_logger.info("Waiting for Android to boot...")
819 while True:
820 booted = False
821 anim_finished = True # Assume boot animation was disabled on except
822 try:
823 booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1)
824 anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1)
825 except ValueError:
826 pass
827 if booted and anim_finished:
828 break
829 time.sleep(60)
830
831 gem5_logger.info("Android booted")
832
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100833def _give_password(password, command):
834 if not sshpass:
835 raise HostError('Must have sshpass installed on the host in order to use password-based auth.')
836 pass_string = "sshpass -p '{}' ".format(password)
837 return pass_string + command
838
839
840def _check_env():
841 global ssh, scp, sshpass # pylint: disable=global-statement
842 if not ssh:
843 ssh = which('ssh')
844 scp = which('scp')
845 sshpass = which('sshpass')
846 if not (ssh and scp):
847 raise HostError('OpenSSH must be installed on the host.')
848
849
850def process_backspaces(text):
851 chars = []
852 for c in text:
853 if c == chr(8) and chars: # backspace
854 chars.pop()
855 else:
856 chars.append(c)
857 return ''.join(chars)