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