| # Copyright (C) 2017-2019 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| import os |
| import time |
| |
| from webkitpy.common.timeout_context import Timeout |
| from webkitpy.port.server_process import ServerProcess |
| |
| |
| class SimulatorProcess(ServerProcess): |
| |
| class Popen(object): |
| |
| def __init__(self, pid, stdin, stdout, stderr, device): |
| self.stdin = stdin |
| self.stdout = stdout |
| self.stderr = stderr |
| self.pid = pid |
| self.returncode = None |
| self._device = device |
| |
| def poll(self): |
| if self.returncode: |
| return self.returncode |
| if self._device.executive.check_running_pid(self.pid): |
| self.returncode = None |
| else: |
| self.returncode = 1 |
| return self.returncode |
| |
| def wait(self): |
| while not self.poll(): |
| time.sleep(0.01) # In seconds |
| return self.returncode |
| |
| # Python 2's implementation of makefile does not return a non-blocking file. |
| class NonBlockingFileFromSocket(object): |
| |
| def __init__(self, sock, type): |
| self.socket = sock |
| self._file = os.fdopen(sock.fileno(), type, 0) |
| ServerProcess._set_file_nonblocking(self._file) |
| |
| def __getattr__(self, name): |
| return getattr(self._file, name) |
| |
| def close(self): |
| result = self._file.close() |
| self.socket.close() |
| return result |
| |
| def __init__(self, port_obj, name, cmd, env=None, universal_newlines=False, treat_no_data_as_crash=False, target_host=None): |
| env['PORT'] = str(target_host.listening_port()) # The target_host should be a device. |
| super(SimulatorProcess, self).__init__(port_obj, name, cmd, env, universal_newlines, treat_no_data_as_crash, target_host) |
| |
| self._bundle_id = port_obj.app_identifier_from_bundle(cmd[0]) |
| |
| def process_name(self): |
| return self._port.app_executable_from_bundle(self._cmd[0]) |
| |
| @staticmethod |
| def _accept_connection_create_file(server, type): |
| connection, address = server.accept() |
| assert address[0] == '127.0.0.1' |
| return SimulatorProcess.NonBlockingFileFromSocket(connection, type) |
| |
| def _start(self): |
| if self._proc: |
| raise ValueError('{} already running'.format(self._name)) |
| self._reset() |
| |
| # Each device has a listening socket intitilaized during the port's setup_test_run. |
| # 3 client connections will be accepted for stdin, stdout and stderr in that order. |
| self._target_host.listening_socket.listen(3) |
| self._pid = self._target_host.launch_app(self._bundle_id, self._cmd[1:], env=self._env) |
| self._system_pid = self._pid |
| |
| # FIXME <rdar://problem/57032042>: This timeout should be 15 seconds |
| with Timeout(30, RuntimeError('Timed out waiting for pid {} to connect at port {}'.format(self._pid, self._target_host.listening_port()))): |
| stdin = None |
| stdout = None |
| stderr = None |
| try: |
| # This order matches the client side connections in Tools/TestRunnerShared/IOSLayoutTestCommunication.cpp setUpIOSLayoutTestCommunication() |
| stdin = SimulatorProcess._accept_connection_create_file(self._target_host.listening_socket, 'w') |
| stdout = SimulatorProcess._accept_connection_create_file(self._target_host.listening_socket, 'rb') |
| stderr = SimulatorProcess._accept_connection_create_file(self._target_host.listening_socket, 'rb') |
| except: |
| # We set self._proc as _reset() and _kill() depend on it. |
| self._proc = SimulatorProcess.Popen(self._pid, stdin, stdout, stderr, self._target_host) |
| if self._proc.poll() is not None: |
| self._reset() |
| raise Exception('App {} with pid {} crashed before stdin could be attached'.format(os.path.basename(self._cmd[0]), self._pid)) |
| self._kill() |
| self._reset() |
| raise |
| |
| self._proc = SimulatorProcess.Popen(self._pid, stdin, stdout, stderr, self._target_host) |
| |
| def stop(self, timeout_secs=3.0): |
| # Only bother to check for leaks or stderr if the process is still running. |
| if self.poll() is None: |
| self._port.check_for_leaks(self.process_name(), self.pid()) |
| for child_process_name in self._child_processes.keys(): |
| for child_process_id in self._child_processes[child_process_name]: |
| self._port.check_for_leaks(child_process_name, child_process_id) |
| |
| if self._proc and self._proc.pid: |
| self._target_host.executive.kill_process(self._proc.pid) |
| |
| return self._wait_for_stop(timeout_secs) |