• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#    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
25import socket
26import time
27
28import 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
34from 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
38from devlib.utils.types import boolean
39
40
41ssh = None
42scp = None
43sshpass = None
44
45
46logger = logging.getLogger('ssh')
47gem5_logger = logging.getLogger('gem5-connection')
48
49def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None):
50    _check_env()
51    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.')
56            conn = TelnetPxssh(original_prompt=original_prompt)
57        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
73    conn.setwinsize(500,200)
74    conn.sendline('')
75    conn.prompt()
76    conn.setecho(False)
77    return conn
78
79
80class TelnetPxssh(pxssh.pxssh):
81    # pylint: disable=arguments-differ
82
83    def __init__(self, original_prompt):
84        super(TelnetPxssh, self).__init__()
85        self.original_prompt = original_prompt or r'[#$]'
86
87    def login(self, server, username, password='', login_timeout=10,
88              auto_prompt_reset=True, sync_multiplier=1, port=23):
89        args = ['telnet']
90        if username is not None:
91            args += ['-l', username]
92        args += [server, str(port)]
93        cmd = ' '.join(args)
94
95        spawn._spawn(self, cmd)  # pylint: disable=protected-access
96
97        try:
98            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)
102            if i:
103                raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
104        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')
111
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
147    default_timeout=10
148
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,
159                 timeout=None,
160                 telnet=False,
161                 password_prompt=None,
162                 original_prompt=None,
163                 platform=None
164                 ):
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))
173        timeout = timeout if timeout is not None else self.default_timeout
174        self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None)
175
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
184    def execute(self, command, timeout=None, check_exit_code=True,
185                as_root=False, strip_colors=True): #pylint: disable=unused-argument
186        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 ''
190        try:
191            with self.lock:
192                _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)
196                if check_exit_code:
197                    try:
198                        exit_code = int(exit_code_text)
199                        if exit_code:
200                            message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
201                            raise TargetError(message.format(exit_code, command, output))
202                    except (ValueError, IndexError):
203                        logger.warning(
204                            'Could not get exit code for "{}",\ngot: "{}"'\
205                            .format(command, exit_code_text))
206                return output
207        except EOF:
208            raise TargetError('Connection lost.')
209
210    def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
211        try:
212            port_string = '-p {}'.format(self.port) if self.port else ''
213            keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
214            if as_root:
215                command = "sudo -- sh -c '{}'".format(command)
216            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.')
223
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.
239        if self.username == 'root':
240            # As we're already root, there is no need to use sudo.
241            as_root = False
242        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
297class 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,
307                 platform=None):
308        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
320class 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
442        result = self._gem5_shell("ls {}".format(source))
443        files = result.split()
444
445        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))
462
463            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)
467
468            # 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.")
476
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
485        output = self._gem5_shell(command,
486                                  check_exit_code=check_exit_code,
487                                  as_root=as_root)
488        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
833def _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)
858