Diff
Modified: trunk/Tools/ChangeLog (220482 => 220483)
--- trunk/Tools/ChangeLog 2017-08-09 21:57:01 UTC (rev 220482)
+++ trunk/Tools/ChangeLog 2017-08-09 21:58:18 UTC (rev 220483)
@@ -1,3 +1,54 @@
+2017-08-09 Jonathan Bedard <jbed...@apple.com>
+
+ Allow nested timeouts in webkitpy
+ https://bugs.webkit.org/show_bug.cgi?id=175390
+ <rdar://problem/33803003>
+
+ Reviewed by David Kilzer.
+
+ We need to be able to nest timeouts in webkitpy. In particular, we have a few cases where functions
+ which use timeouts also call the executive. For on-device testing, we need to have timeouts inside
+ the executive to detect and recover from any issues connecting with devices.
+
+ * Scripts/webkitpy/benchmark_runner/utils.py:
+ (TimeoutError): Deleted.
+ (timeout): Deleted.
+ * Scripts/webkitpy/common/timeout_context.py: Added.
+ (Timeout): A timeout context designed to be nested.
+ (Timeout.TimeoutData): The data required to construct an alarm for a given timeout.
+ (Timeout.TimeoutData.__init__):
+ (Timeout.default_handler): Timeout handler used if none is specified.
+ (Timeout.current): Access data about the most urgent timeout.
+ (Timeout.__init__): Construct a Timeout object with seconds and an optional handler.
+ (Timeout._bind_timeout_data_to_alarm): Given data about a timeout, initialize an alarm for that timeout.
+ (Timeout.__enter__): Un-bind all alarms. Add data for this timeout to the ordered list and bind the most
+ urgent timeout data.
+ (Timeout.__exit__): Un-bind all alarms. Remove data for this timeout from the ordered list and bind the
+ most urgent timeout data, if such data exists.
+ * Scripts/webkitpy/common/timeout_context_unittest.py: Added.
+ (TimeoutContextTests):
+ (TimeoutContextTests.test_current_timeout): Test that accessing the nearest timeout works as expected.
+ (TimeoutContextTests.test_invalid_timeout): Test a timeout of 0.
+ (TimeoutContextTests.test_timeout_data): Confirm that timeouts are constructed correctly.
+ (TimeoutContextTests.test_nested_inner_precedence): Check that a more urgent inner timeout takes precedence
+ over a less urgent outer timeout.
+ (TimeoutContextTests.test_nested_outer_precedence): Check that a more urgent outer timeout takes precedence
+ over a less urgent inner timeout.
+ (TimeoutContextTests.test_no_timeout): Test a block of code without a timeout.
+ (TimeoutContextTests.test_basic_timeout): Test a block of code expected to timeout.
+ (TimeoutContextTests.test_exception_constructor_timeout): Test a timeout where the handler is an exception.
+ (TimeoutContextTests.test_nested_inner_timeout): Confirm that a more urgent inner timeout is triggered.
+ (TimeoutContextTests.test_nested_outer_timeout): Confirm that a more urgent outer timeout is triggered.
+ * Scripts/webkitpy/port/simulator_process.py:
+ (SimulatorProcess._start): Use Timeout class.
+ * Scripts/webkitpy/xcode/simulated_device.py:
+ (SimulatedDevice.launch_app._log_debug_error): Use Timeout class.
+ (SimulatedDevice.launch_app): Ditto.
+ (SimulatedDevice.launch_app._install_timeout): Deleted.
+ * Scripts/webkitpy/xcode/simulator.py:
+ (Simulator.wait_until_device_is_booted):Use Timeout class.
+ (Simulator.wait_until_device_is_in_state): Ditto.
+
2017-08-09 Wenson Hsieh <wenson_hs...@apple.com>
[iOS DnD] ENABLE_DRAG_SUPPORT should be turned off for iOS 10 and enabled by default
Modified: trunk/Tools/Scripts/webkitpy/benchmark_runner/utils.py (220482 => 220483)
--- trunk/Tools/Scripts/webkitpy/benchmark_runner/utils.py 2017-08-09 21:57:01 UTC (rev 220482)
+++ trunk/Tools/Scripts/webkitpy/benchmark_runner/utils.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -4,9 +4,7 @@
import inspect
import logging
import os
-import signal
import shutil
-import sys
from webkitpy.common.memoized import memoized
@@ -77,26 +75,3 @@
defaults.setPersistentDomain_forName_(mutable_defaults_for_domain, domain)
defaults.synchronize()
return True
-
-
-# Borrow this code from
-# 'http://stackoverflow.com/questions/2281850/timeout-function-if-it-takes-too-long-to-finish'
-class TimeoutError(Exception):
- pass
-
-
-class timeout:
-
- def __init__(self, seconds=1, error_message='Timeout'):
- self.seconds = seconds
- self.error_message = error_message
-
- def handle_timeout(self, signum, frame):
- raise TimeoutError(self.error_message)
-
- def __enter__(self):
- signal.signal(signal.SIGALRM, self.handle_timeout)
- signal.alarm(self.seconds)
-
- def __exit__(self, type, value, traceback):
- signal.alarm(0)
Added: trunk/Tools/Scripts/webkitpy/common/timeout_context.py (0 => 220483)
--- trunk/Tools/Scripts/webkitpy/common/timeout_context.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/common/timeout_context.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -0,0 +1,119 @@
+# Copyright (C) 2017 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 logging
+import math
+import os
+import signal
+import time
+import threading
+
+_log = logging.getLogger(__name__)
+
+
+class Timeout(object):
+
+ thread_exception = RuntimeError('Timeout originates from a different thread')
+ _process_to_timeout_map = {}
+
+ class TimeoutData(object):
+
+ def __init__(self, alarm_time, handler):
+ self.alarm_time = alarm_time
+ self.handler = handler
+ self.thread_id = threading.current_thread().ident
+
+ @staticmethod
+ def default_handler(signum, frame):
+ raise RuntimeError('Timeout alarm was triggered')
+
+ @staticmethod
+ def current():
+ result = Timeout._process_to_timeout_map.get(os.getpid(), [])
+ if not result:
+ return None
+ if result[0].thread_id != threading.current_thread().ident:
+ _log.critical('Using both alarms and threading in the same process, this is unsupported')
+ raise Timeout.thread_exception
+ return result[0]
+
+ def __init__(self, seconds=1, handler=None):
+ if seconds == 0:
+ raise RuntimeError('Cannot have a timeout of 0 seconds')
+
+ if isinstance(handler, BaseException):
+ exception = handler
+
+ def exception_handler(signum, frame):
+ raise exception
+
+ handler = exception_handler
+
+ self._timeout = seconds
+ self._handler = handler if handler else Timeout.default_handler
+ self.data = ""
+
+ @staticmethod
+ def _bind_timeout_data_to_alarm(data):
+ def handler(signum, frame):
+ assert signum == signal.SIGALRM
+ if data.thread_id != threading.current_thread().ident:
+ raise Timeout.thread_exception
+ data.handler(signum, frame)
+
+ current_time = time.time()
+ if data.alarm_time <= current_time:
+ handler(signal.SIGALRM, None)
+
+ signal.signal(signal.SIGALRM, handler)
+ signal.alarm(int(math.ceil(data.alarm_time - current_time)))
+
+ def __enter__(self):
+ signal.alarm(0) # Imiediatly disable the alarm so we aren't interupted.
+ self.data = "" + self._timeout, self._handler)
+ current_timeout = Timeout.current()
+
+ # Another timeout is more urgent.
+ if current_timeout and current_timeout.alarm_time < self.data.alarm_time:
+ for i in xrange(len(Timeout._process_to_timeout_map[os.getpid()]) - 1):
+ if self.data.alarm_time < Timeout._process_to_timeout_map[os.getpid()][i + 1].alarm_time:
+ Timeout._process_to_timeout_map[os.getpid()].insert(i, self.data)
+ break
+ Timeout._process_to_timeout_map[os.getpid()].append(self.data)
+
+ # This is the most urgent timeout
+ else:
+ Timeout._process_to_timeout_map[os.getpid()] = [self.data] + Timeout._process_to_timeout_map.get(os.getpid(), [])
+
+ Timeout._bind_timeout_data_to_alarm(Timeout.current())
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ signal.alarm(0) # Imiediatly disable the alarm so we aren't interupted.
+
+ if not Timeout._process_to_timeout_map[os.getpid()]:
+ raise RuntimeError('No timeout registered')
+ Timeout._process_to_timeout_map[os.getpid()].remove(self.data)
+ self.data = ""
+
+ if Timeout._process_to_timeout_map[os.getpid()]:
+ Timeout._bind_timeout_data_to_alarm(Timeout.current())
Added: trunk/Tools/Scripts/webkitpy/common/timeout_context_unittest.py (0 => 220483)
--- trunk/Tools/Scripts/webkitpy/common/timeout_context_unittest.py (rev 0)
+++ trunk/Tools/Scripts/webkitpy/common/timeout_context_unittest.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -0,0 +1,98 @@
+# Copyright (C) 2017 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 time
+import threading
+import unittest
+
+from webkitpy.common.timeout_context import Timeout
+
+
+class TimeoutContextTests(unittest.TestCase):
+
+ def test_current_timeout(self):
+ self.assertEqual(None, Timeout.current())
+ with Timeout(1) as tmp:
+ self.assertEqual(tmp.data, Timeout.current())
+ self.assertEqual(None, Timeout.current())
+
+ def test_invalid_timeout(self):
+ self.assertRaises(RuntimeError, Timeout, 0)
+
+ def test_timeout_data(self):
+ tmp = Timeout(1)
+ self.assertEqual(None, tmp.data)
+ with tmp:
+ self.assertNotEqual(None, tmp.data)
+ self.assertEqual(threading.current_thread().ident, tmp.data.thread_id)
+ self.assertGreater(time.time() + 1, tmp.data.alarm_time)
+ self.assertEqual(None, tmp.data)
+
+ def test_nested_inner_precedence(self):
+ tmp_outer = Timeout(2)
+ tmp_inner = Timeout(1)
+ with tmp_outer:
+ self.assertEqual(tmp_outer.data, Timeout.current())
+ with tmp_inner:
+ self.assertEqual(tmp_inner.data, Timeout.current())
+ self.assertEqual(tmp_outer.data, Timeout.current())
+ self.assertEqual(None, Timeout.current())
+
+ def test_nested_outer_precedence(self):
+ tmp_outer = Timeout(1)
+ tmp_inner = Timeout(2)
+ with tmp_outer:
+ self.assertEqual(tmp_outer.data, Timeout.current())
+ with tmp_inner:
+ self.assertEqual(tmp_outer.data, Timeout.current())
+ self.assertEqual(tmp_outer.data, Timeout.current())
+ self.assertEqual(None, Timeout.current())
+
+ def test_no_timeout(self):
+ with Timeout(2):
+ time.sleep(1)
+
+ def test_basic_timeout(self):
+ def should_timeout():
+ with Timeout(1):
+ time.sleep(2)
+ self.assertRaises(RuntimeError, should_timeout)
+
+ def test_exception_constructor_timeout(self):
+ def should_timeout():
+ with Timeout(1, Exception('This should be raised')):
+ time.sleep(2)
+ self.assertRaises(Exception, should_timeout)
+
+ def test_nested_inner_timeout(self):
+ def should_timeout():
+ with Timeout(3, Exception("This shouldn't be raised")):
+ with Timeout(1):
+ time.sleep(2)
+ self.assertRaises(RuntimeError, should_timeout)
+
+ def test_nested_outer_timeout(self):
+ def should_timeout():
+ with Timeout(1):
+ with Timeout(3, Exception("This shouldn't be raised")):
+ time.sleep(2)
+ self.assertRaises(RuntimeError, should_timeout)
Modified: trunk/Tools/Scripts/webkitpy/port/simulator_process.py (220482 => 220483)
--- trunk/Tools/Scripts/webkitpy/port/simulator_process.py 2017-08-09 21:57:01 UTC (rev 220482)
+++ trunk/Tools/Scripts/webkitpy/port/simulator_process.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -22,12 +22,11 @@
import os
-import signal
import time
+from webkitpy.common.timeout_context import Timeout
from webkitpy.port.server_process import ServerProcess
-
class SimulatorProcess(ServerProcess):
class Popen(object):
@@ -95,30 +94,24 @@
self._target_host.listening_socket.listen(3)
self._pid = self._target_host.launch_app(self._bundle_id, self._cmd[1:], env=self._env)
- def handler(signum, frame):
- assert signum == signal.SIGALRM
- raise RuntimeError('Timed out waiting for pid {} to connect at port {}'.format(self._pid, self._target_host.listening_port()))
- signal.signal(signal.SIGALRM, handler)
- signal.alarm(6) # In seconds
-
- 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:
+ with Timeout(6, 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 Exception('App {} with pid {} crashed before stdin could be attached'.format(os.path.basename(self._cmd[0]), self._pid))
- self._kill()
- self._reset()
- raise
- signal.alarm(0) # Cancel alarm
+ raise
self._proc = SimulatorProcess.Popen(self._pid, stdin, stdout, stderr, self._target_host)
Modified: trunk/Tools/Scripts/webkitpy/xcode/simulated_device.py (220482 => 220483)
--- trunk/Tools/Scripts/webkitpy/xcode/simulated_device.py 2017-08-09 21:57:01 UTC (rev 220482)
+++ trunk/Tools/Scripts/webkitpy/xcode/simulated_device.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -22,12 +22,12 @@
import logging
import re
-import signal
import subprocess
+from webkitpy.common.host import Host
from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.timeout_context import Timeout
from webkitpy.xcode.simulator import Simulator
-from webkitpy.common.host import Host
_log = logging.getLogger(__name__)
@@ -170,29 +170,23 @@
def _log_debug_error(error):
_log.debug(error.message_with_output())
- def _install_timeout(signum, frame):
- assert signum == signal.SIGALRM
- raise RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))
-
output = None
- signal.signal(signal.SIGALRM, _install_timeout)
- signal.alarm(timeout) # In seconds
- while True:
- output = self._host.executive.run_command(
- ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
- env=environment_to_use,
- error_handler=_log_debug_error,
- )
- match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
- # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
- if match and self.executive.check_running_pid(int(match.group('pid'))):
- break
- if match:
- _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
- else:
- _log.debug('simctl launch did not report a pid')
- signal.alarm(0) # Cancel alarm
+ with Timeout(timeout, RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))):
+ while True:
+ output = self._host.executive.run_command(
+ ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
+ env=environment_to_use,
+ error_handler=_log_debug_error,
+ )
+ match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
+ # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
+ if match and self.executive.check_running_pid(int(match.group('pid'))):
+ break
+ if match:
+ _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
+ else:
+ _log.debug('simctl launch did not report a pid')
if match.group('bundle') != bundle_id:
raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
Modified: trunk/Tools/Scripts/webkitpy/xcode/simulator.py (220482 => 220483)
--- trunk/Tools/Scripts/webkitpy/xcode/simulator.py 2017-08-09 21:57:01 UTC (rev 220482)
+++ trunk/Tools/Scripts/webkitpy/xcode/simulator.py 2017-08-09 21:58:18 UTC (rev 220483)
@@ -28,7 +28,7 @@
import subprocess
import time
-from webkitpy.benchmark_runner.utils import timeout
+from webkitpy.common.timeout_context import Timeout
from webkitpy.common.host import Host
_log = logging.getLogger(__name__)
@@ -242,7 +242,7 @@
@staticmethod
def wait_until_device_is_booted(udid, timeout_seconds=60 * 15):
Simulator.wait_until_device_is_in_state(udid, Simulator.DeviceState.BOOTED, timeout_seconds)
- with timeout(seconds=timeout_seconds):
+ with Timeout(seconds=timeout_seconds):
while True:
try:
state = subprocess.check_output(['xcrun', 'simctl', 'spawn', udid, 'launchctl', 'print', 'system']).strip()
@@ -260,7 +260,7 @@
@staticmethod
def wait_until_device_is_in_state(udid, wait_until_state, timeout_seconds=60 * 15):
_log.debug('waiting for device %s to enter state %s with timeout %s', udid, Simulator.device_state_description(wait_until_state), timeout_seconds)
- with timeout(seconds=timeout_seconds):
+ with Timeout(seconds=timeout_seconds):
device_state = Simulator.device_state(udid)
while (device_state != wait_until_state):
device_state = Simulator.device_state(udid)