1#    Copyright 2016 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
15import os
16import re
17import subprocess
18import sys
19import shutil
20import time
21import types
22
23from devlib.exception import TargetError
24from devlib.host import PACKAGE_BIN_DIRECTORY
25from devlib.platform import Platform
26from devlib.utils.ssh import AndroidGem5Connection, LinuxGem5Connection
27
28class Gem5SimulationPlatform(Platform):
29
30    def __init__(self, name,
31                 host_output_dir,
32                 gem5_bin,
33                 gem5_args,
34                 gem5_virtio,
35                 core_names=None,
36                 core_clusters=None,
37                 big_core=None,
38                 model=None,
39                 modules=None,
40                 gem5_telnet_port=None):
41
42        # First call the parent class
43        super(Gem5SimulationPlatform, self).__init__(name, core_names, core_clusters,
44                                                     big_core, model, modules)
45
46        # Start setting up the gem5 parameters/directories
47        # The gem5 subprocess
48        self.gem5 = None
49        self.gem5_port = gem5_telnet_port or None
50        self.stats_directory = host_output_dir
51        self.gem5_out_dir = os.path.join(self.stats_directory, "gem5")
52        self.gem5_interact_dir = '/tmp' # Host directory
53        self.executable_dir = None # Device directory
54        self.working_dir = None # Device directory
55        self.stdout_file = None
56        self.stderr_file = None
57        self.stderr_filename = None
58        if self.gem5_port is None:
59            # Allows devlib to pick up already running simulations
60            self.start_gem5_simulation = True
61        else:
62            self.start_gem5_simulation = False
63
64        # Find the first one that does not exist. Ensures that we do not re-use
65        # the directory used by someone else.
66        for i in xrange(sys.maxint):
67            directory = os.path.join(self.gem5_interact_dir, "wa_{}".format(i))
68            try:
69                os.stat(directory)
70                continue
71            except OSError:
72                break
73        self.gem5_interact_dir = directory
74        self.logger.debug("Using {} as the temporary directory."
75                          .format(self.gem5_interact_dir))
76
77        # Parameters passed onto gem5
78        self.gem5args_binary = gem5_bin
79        self.gem5args_args = gem5_args
80        self.gem5args_virtio = gem5_virtio
81        self._check_gem5_command()
82
83        # Start the interaction with gem5
84        self._start_interaction_gem5()
85
86    def _check_gem5_command(self):
87        """
88        Check if the command to start gem5 makes sense
89        """
90        if self.gem5args_binary is None:
91            raise TargetError('Please specify a gem5 binary.')
92        if self.gem5args_args is None:
93            raise TargetError('Please specify the arguments passed on to gem5.')
94        self.gem5args_virtio = str(self.gem5args_virtio).format(self.gem5_interact_dir)
95        if self.gem5args_virtio is None:
96            raise TargetError('Please specify arguments needed for virtIO.')
97
98    def _start_interaction_gem5(self):
99        """
100        Starts the interaction of devlib with gem5.
101        """
102
103        # First create the input and output directories for gem5
104        if self.start_gem5_simulation:
105            # Create the directory to send data to/from gem5 system
106            self.logger.info("Creating temporary directory for interaction "
107                             " with gem5 via virtIO: {}"
108                             .format(self.gem5_interact_dir))
109            os.mkdir(self.gem5_interact_dir)
110
111            # Create the directory for gem5 output (stats files etc)
112            if not os.path.exists(self.stats_directory):
113                os.mkdir(self.stats_directory)
114            if os.path.exists(self.gem5_out_dir):
115                raise TargetError("The gem5 stats directory {} already "
116                                  "exists.".format(self.gem5_out_dir))
117            else:
118                os.mkdir(self.gem5_out_dir)
119
120            # We need to redirect the standard output and standard error for the
121            # gem5 process to a file so that we can debug when things go wrong.
122            f = os.path.join(self.gem5_out_dir, 'stdout')
123            self.stdout_file = open(f, 'w')
124            f = os.path.join(self.gem5_out_dir, 'stderr')
125            self.stderr_file = open(f, 'w')
126            # We need to keep this so we can check which port to use for the
127            # telnet connection.
128            self.stderr_filename = f
129
130            # Start gem5 simulation
131            self.logger.info("Starting the gem5 simulator")
132
133            command_line = "{} --outdir={} {} {}".format(self.gem5args_binary,
134                                                         self.gem5_out_dir,
135                                                         self.gem5args_args,
136                                                         self.gem5args_virtio)
137            self.logger.debug("gem5 command line: {}".format(command_line))
138            self.gem5 = subprocess.Popen(command_line.split(),
139                                         stdout=self.stdout_file,
140                                         stderr=self.stderr_file)
141
142        else:
143            # The simulation should already be running
144            # Need to dig up the (1) gem5 simulation in question (2) its input
145            # and output directories (3) virtio setting
146            self._intercept_existing_gem5()
147
148        # As the gem5 simulation is running now or was already running
149        # we now need to find out which telnet port it uses
150        self._intercept_telnet_port()
151
152    def _intercept_existing_gem5(self):
153        """
154        Intercept the information about a running gem5 simulation
155        e.g. pid, input directory etc
156        """
157        self.logger("This functionality is not yet implemented")
158        raise TargetError()
159
160    def _intercept_telnet_port(self):
161        """
162        Intercept the telnet port of a running gem5 simulation
163        """
164
165        if self.gem5 is None:
166            raise TargetError('The platform has no gem5 simulation! '
167                              'Something went wrong')
168        while self.gem5_port is None:
169            # Check that gem5 is running!
170            if self.gem5.poll():
171                raise TargetError("The gem5 process has crashed with error code {}!".format(self.gem5.poll()))
172
173            # Open the stderr file
174            with open(self.stderr_filename, 'r') as f:
175                for line in f:
176                    m = re.search(r"Listening for system connection on port (?P<port>\d+)", line)
177                    if m:
178                        port = int(m.group('port'))
179                        if port >= 3456 and port < 5900:
180                            self.gem5_port = port
181                            break
182                    # Check if the sockets are not disabled
183                    m = re.search(r"Sockets disabled, not accepting terminal connections", line)
184                    if m:
185                        raise TargetError("The sockets have been disabled!"
186                                          "Pass --listener-mode=on to gem5")
187                else:
188                    time.sleep(1)
189
190    def init_target_connection(self, target):
191        """
192        Update the type of connection in the target from here
193        """
194        if target.os == 'linux':
195            target.conn_cls = LinuxGem5Connection
196        else:
197            target.conn_cls = AndroidGem5Connection
198
199    def setup(self, target):
200        """
201        Deploy m5 if not yet installed
202        """
203        m5_path = target.get_installed('m5')
204        if m5_path is None:
205            m5_path = self._deploy_m5(target)
206        target.conn.m5_path = m5_path
207
208        # Set the terminal settings for the connection to gem5
209        self._resize_shell(target)
210
211    def update_from_target(self, target):
212        """
213        Set the m5 path and if not yet installed, deploy m5
214        Overwrite certain methods in the target that either can be done
215        more efficiently by gem5 or don't exist in gem5
216        """
217        m5_path = target.get_installed('m5')
218        if m5_path is None:
219            m5_path = self._deploy_m5(target)
220        target.conn.m5_path = m5_path
221
222        # Overwrite the following methods (monkey-patching)
223        self.logger.debug("Overwriting the 'capture_screen' method in target")
224        # Housekeeping to prevent recursion
225        setattr(target, 'target_impl_capture_screen', target.capture_screen)
226        target.capture_screen = types.MethodType(_overwritten_capture_screen, target)
227        self.logger.debug("Overwriting the 'reset' method in target")
228        target.reset = types.MethodType(_overwritten_reset, target)
229        self.logger.debug("Overwriting the 'reboot' method in target")
230        target.reboot = types.MethodType(_overwritten_reboot, target)
231
232        # Call the general update_from_target implementation
233        super(Gem5SimulationPlatform, self).update_from_target(target)
234
235    def gem5_capture_screen(self, filepath):
236        file_list = os.listdir(self.gem5_out_dir)
237        screen_caps = []
238        for f in file_list:
239            if '.bmp' in f:
240                screen_caps.append(f)
241
242        successful_capture = False
243        if len(screen_caps) == 1:
244            # Bail out if we do not have image, and resort to the slower, built
245            # in method.
246            try:
247                import Image
248                gem5_image = os.path.join(self.gem5_out_dir, screen_caps[0])
249                temp_image = os.path.join(self.gem5_out_dir, "file.png")
250                im = Image.open(gem5_image)
251                im.save(temp_image, "PNG")
252                shutil.copy(temp_image, filepath)
253                os.remove(temp_image)
254                gem5_logger.info("capture_screen: using gem5 screencap")
255                successful_capture = True
256
257            except (shutil.Error, ImportError, IOError):
258                pass
259
260        return successful_capture
261
262    def _deploy_m5(self, target):
263        # m5 is not yet installed so install it
264        host_executable = os.path.join(PACKAGE_BIN_DIRECTORY,
265                                       target.abi, 'm5')
266        return target.install(host_executable)
267
268    def _resize_shell(self, target):
269        """
270        Resize the shell to avoid line wrapping issues.
271
272        """
273        # Try and avoid line wrapping as much as possible.
274        target.execute('{} stty columns 1024'.format(target.busybox))
275        target.execute('reset', check_exit_code=False)
276
277# Methods that will be monkey-patched onto the target
278def _overwritten_reset(self):
279    raise TargetError('Resetting is not allowed on gem5 platforms!')
280
281def _overwritten_reboot(self):
282    raise TargetError('Rebooting is not allowed on gem5 platforms!')
283
284def _overwritten_capture_screen(self, filepath):
285    connection_screencapped = self.platform.gem5_capture_screen(filepath)
286    if connection_screencapped == False:
287        # The connection was not able to capture the screen so use the target
288        # implementation
289        self.logger.debug('{} was not able to screen cap, using the original target implementation'.format(self.platform.__class__.__name__))
290        self.target_impl_capture_screen(filepath)
291
292
293