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