webkitpy: Ignore errors when shutting down an already shutdown simulator
[WebKit-https.git] / Tools / Scripts / webkitpy / xcode / simulated_device.py
1 # Copyright (C) 2017-2019 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1.  Redistributions of source code must retain the above copyright
7 #     notice, this list of conditions and the following disclaimer.
8 # 2.  Redistributions in binary form must reproduce the above copyright
9 #     notice, this list of conditions and the following disclaimer in the
10 #     documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
13 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
16 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 import atexit
24 import json
25 import logging
26 import plistlib
27 import re
28 import time
29
30 from webkitpy.common.memoized import memoized
31 from webkitpy.common.system.executive import ScriptError
32 from webkitpy.common.system.systemhost import SystemHost
33 from webkitpy.common.timeout_context import Timeout
34 from webkitpy.common.version import Version
35 from webkitpy.port.device import Device
36 from webkitpy.xcode.device_type import DeviceType
37
38 _log = logging.getLogger(__name__)
39
40
41 class DeviceRequest(object):
42
43     def __init__(self, device_type, use_booted_simulator=True, use_existing_simulator=True, allow_incomplete_match=False, merge_requests=False):
44         self.device_type = device_type
45         self.use_booted_simulator = use_booted_simulator
46         self.use_existing_simulator = use_existing_simulator
47         self.allow_incomplete_match = allow_incomplete_match  # When matching booted simulators, only force the software_variant to match.
48         self.merge_requests = merge_requests  # Allow a single booted simulator to fullfil multiple requests.
49
50
51 class SimulatedDeviceManager(object):
52     class Runtime(object):
53         def __init__(self, runtime_dict):
54             self.build_version = runtime_dict['buildversion']
55             self.os_variant = runtime_dict['name'].split(' ')[0]
56             self.version = Version.from_string(runtime_dict['version'])
57             self.identifier = runtime_dict['identifier']
58             self.name = runtime_dict['name']
59
60     AVAILABLE_RUNTIMES = []
61     AVAILABLE_DEVICES = []
62     INITIALIZED_DEVICES = None
63
64     SIMULATOR_BOOT_TIMEOUT = 600
65
66     # FIXME: Simulators should only take up 2GB, but because of <rdar://problem/39393590> something in the OS thinks they're taking closer to 6GB
67     MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE = 6 * (1024 ** 3)  # 6GB a simulator.
68     PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 125
69
70     xcrun = '/usr/bin/xcrun'
71     simulator_device_path = '~/Library/Developer/CoreSimulator/Devices'
72     simulator_bundle_id = 'com.apple.iphonesimulator'
73     _device_identifier_to_name = {}
74     _managing_simulator_app = False
75
76     @staticmethod
77     def _create_runtimes(runtimes):
78         result = []
79         for runtime in runtimes:
80             if runtime.get('availability') != '(available)' and runtime.get('isAvailable') != 'YES' and runtime.get('isAvailable') != True:
81                 continue
82             try:
83                 result.append(SimulatedDeviceManager.Runtime(runtime))
84             except (ValueError, AssertionError):
85                 continue
86         return result
87
88     @staticmethod
89     def _create_device_with_runtime(host, runtime, device_info):
90         if device_info.get('availability') != '(available)' and device_info.get('isAvailable') != 'YES' and device_info.get('isAvailable') != True:
91             return None
92
93         # Check existing devices.
94         for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
95             if device.udid == device_info['udid']:
96                 return device
97
98         # Check that the device.plist exists
99         device_plist = host.filesystem.expanduser(host.filesystem.join(SimulatedDeviceManager.simulator_device_path, device_info['udid'], 'device.plist'))
100         if not host.filesystem.isfile(device_plist):
101             return None
102
103         # Find device type. If we can't parse the device type, ignore this device.
104         try:
105             device_type_string = SimulatedDeviceManager._device_identifier_to_name[plistlib.readPlist(host.filesystem.open_binary_file_for_reading(device_plist))['deviceType']]
106             device_type = DeviceType.from_string(device_type_string, runtime.version)
107             assert device_type.software_variant == runtime.os_variant
108         except (ValueError, AssertionError):
109             return None
110
111         result = Device(SimulatedDevice(
112             name=device_info['name'],
113             udid=device_info['udid'],
114             host=host,
115             device_type=device_type,
116             build_version=runtime.build_version,
117         ))
118         SimulatedDeviceManager.AVAILABLE_DEVICES.append(result)
119         return result
120
121     @staticmethod
122     def populate_available_devices(host=SystemHost()):
123         if not host.platform.is_mac():
124             return
125
126         try:
127             simctl_json = json.loads(host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'list', '--json']))
128         except (ValueError, ScriptError):
129             return
130
131         SimulatedDeviceManager._device_identifier_to_name = {device['identifier']: device['name'] for device in simctl_json['devicetypes']}
132         SimulatedDeviceManager.AVAILABLE_RUNTIMES = SimulatedDeviceManager._create_runtimes(simctl_json['runtimes'])
133
134         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
135             # Needed for <rdar://problem/47122965>
136             devices = []
137             if isinstance(simctl_json['devices'], list):
138                 for devices_for_runtime in simctl_json['devices']:
139                     if devices_for_runtime['name'] == runtime.name:
140                         devices = devices_for_runtime['devices']
141                         break
142             else:
143                 devices = simctl_json['devices'].get(runtime.name, None) or simctl_json['devices'].get(runtime.identifier, [])
144
145             for device_json in devices:
146                 device = SimulatedDeviceManager._create_device_with_runtime(host, runtime, device_json)
147                 if not device:
148                     continue
149
150                 # Update device state from simctl output.
151                 device.platform_device._state = SimulatedDevice.NAME_FOR_STATE.index(device_json['state'].upper())
152                 device.platform_device._last_updated_state = time.time()
153         return
154
155     @staticmethod
156     def available_devices(host=SystemHost()):
157         if SimulatedDeviceManager.AVAILABLE_DEVICES == []:
158             SimulatedDeviceManager.populate_available_devices(host)
159         return SimulatedDeviceManager.AVAILABLE_DEVICES
160
161     @staticmethod
162     def device_by_filter(filter, host=SystemHost()):
163         result = []
164         for device in SimulatedDeviceManager.available_devices(host):
165             if filter(device):
166                 result.append(device)
167         return result
168
169     @staticmethod
170     def _find_exisiting_device_for_request(request):
171         if not request.use_existing_simulator:
172             return None
173         for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
174             # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
175             for initialized_device in SimulatedDeviceManager.INITIALIZED_DEVICES:
176                 if isinstance(initialized_device, Device) and device == initialized_device:
177                     device = None
178                     break
179             if device and request.device_type == device.device_type:
180                 return device
181         return None
182
183     @staticmethod
184     def _find_available_name(name_base):
185         created_index = 0
186         while True:
187             name = '{} {}'.format(name_base, created_index)
188             created_index += 1
189             for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
190                 if device is None:
191                     continue
192                 if device.platform_device.name == name:
193                     break
194             else:
195                 return name
196
197     @staticmethod
198     def get_runtime_for_device_type(device_type):
199         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
200             if runtime.os_variant == device_type.software_variant and (device_type.software_version is None or device_type.software_version == runtime.version):
201                 return runtime
202
203         # Allow for a partial version match.
204         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
205             if runtime.os_variant == device_type.software_variant and runtime.version in device_type.software_version:
206                 return runtime
207         return None
208
209     @staticmethod
210     def _disambiguate_device_type(device_type):
211         # Copy by value since we do not want to modify the DeviceType passed in.
212         full_device_type = DeviceType(
213             hardware_family=device_type.hardware_family,
214             hardware_type=device_type.hardware_type,
215             software_version=device_type.software_version,
216             software_variant=device_type.software_variant)
217
218         runtime = SimulatedDeviceManager.get_runtime_for_device_type(full_device_type)
219         assert runtime is not None
220         full_device_type.software_version = runtime.version
221
222         if full_device_type.hardware_family is None:
223             # We use the existing devices to determine a legal family if no family is specified
224             for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
225                 if device.device_type == full_device_type:
226                     full_device_type.hardware_family = device.device_type.hardware_family
227                     break
228
229         if full_device_type.hardware_type is None:
230             # Again, we use the existing devices to determine a legal hardware type
231             for name in SimulatedDeviceManager._device_identifier_to_name.itervalues():
232                 type_from_name = DeviceType.from_string(name)
233                 if type_from_name == full_device_type:
234                     full_device_type.hardware_type = type_from_name.hardware_type
235                     break
236
237         full_device_type.check_consistency()
238         return full_device_type
239
240     @staticmethod
241     def _get_device_identifier_for_type(device_type):
242         for type_id, type_name in SimulatedDeviceManager._device_identifier_to_name.iteritems():
243             if type_name.lower() == '{} {}'.format(device_type.hardware_family.lower(), device_type.hardware_type.lower()):
244                 return type_id
245         return None
246
247     @staticmethod
248     def _create_or_find_device_for_request(request, host=SystemHost(), name_base='Managed'):
249         assert isinstance(request, DeviceRequest)
250
251         device = SimulatedDeviceManager._find_exisiting_device_for_request(request)
252         if device:
253             return device
254
255         name = SimulatedDeviceManager._find_available_name(name_base)
256         device_type = SimulatedDeviceManager._disambiguate_device_type(request.device_type)
257         runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
258         device_identifier = SimulatedDeviceManager._get_device_identifier_for_type(device_type)
259
260         assert runtime is not None
261         assert device_identifier is not None
262
263         for device in SimulatedDeviceManager.available_devices(host):
264             if device.platform_device.name == name:
265                 device.platform_device._delete()
266                 break
267
268         _log.debug("Creating device '{}', of type {}".format(name, device_type))
269         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'create', name, device_identifier, runtime.identifier])
270
271         # We just added a device, so our list of _available_devices needs to be re-synced.
272         SimulatedDeviceManager.populate_available_devices(host)
273         for device in SimulatedDeviceManager.available_devices(host):
274             if device.platform_device.name == name:
275                 device.platform_device.managed_by_script = True
276                 return device
277         return None
278
279     @staticmethod
280     def _does_fulfill_request(device, requests):
281         if not device.platform_device.is_booted_or_booting():
282             return None
283
284         # Exact match.
285         for request in requests:
286             if not request.use_booted_simulator:
287                 continue
288             if request.device_type == device.device_type:
289                 _log.debug("The request for '{}' matched {} exactly".format(request.device_type, device))
290                 return request
291
292         # Contained-in match.
293         for request in requests:
294             if not request.use_booted_simulator:
295                 continue
296             if device.device_type in request.device_type:
297                 _log.debug("The request for '{}' fuzzy-matched {}".format(request.device_type, device))
298                 return request
299
300         # DeviceRequests are compared by reference
301         requests_copy = [request for request in requests]
302
303         # Check for an incomplete match
304         # This is usually used when we don't want to take the time to start a simulator and would
305         # rather use the one the user has already started, even if it isn't quite what we're looking for.
306         for request in requests_copy:
307             if not request.use_booted_simulator or not request.allow_incomplete_match:
308                 continue
309             if request.device_type.software_variant == device.device_type.software_variant:
310                 _log.warn("The request for '{}' incomplete-matched {}".format(request.device_type, device))
311                 _log.warn("This may cause unexpected behavior in code that expected the device type {}".format(request.device_type))
312                 return request
313         return None
314
315     @staticmethod
316     def _wait_until_device_in_state(device, state, deadline):
317         while device.platform_device.state(force_update=True) != state:
318             _log.debug('Waiting on {} to enter state {}...'.format(device, SimulatedDevice.NAME_FOR_STATE[state]))
319             time.sleep(1)
320             if time.time() > deadline:
321                 raise RuntimeError('Timed out while waiting for all devices to boot')
322
323     @staticmethod
324     def _wait_until_device_is_usable(device, deadline):
325         _log.debug('Waiting until {} is usable'.format(device))
326         while not device.platform_device.is_usable(force_update=True):
327             if time.time() > deadline:
328                 raise RuntimeError('Timed out while waiting for {} to become usable'.format(device))
329             time.sleep(1)
330
331     @staticmethod
332     def _boot_device(device, host=SystemHost()):
333         _log.debug("Booting device '{}'".format(device.udid))
334         device.platform_device.booted_by_script = True
335         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
336         SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
337
338     @staticmethod
339     def device_count_for_type(device_type, host=SystemHost(), use_booted_simulator=True, **kwargs):
340         if not host.platform.is_mac():
341             return 0
342
343         if SimulatedDeviceManager.device_by_filter(lambda device: device.platform_device.is_booted_or_booting(), host=host) and use_booted_simulator:
344             filter = lambda device: device.platform_device.is_booted_or_booting() and device.device_type in device_type
345             return len(SimulatedDeviceManager.device_by_filter(filter, host=host))
346
347         for name in SimulatedDeviceManager._device_identifier_to_name.itervalues():
348             if DeviceType.from_string(name) in device_type:
349                 return SimulatedDeviceManager.max_supported_simulators(host)
350         return 0
351
352     @staticmethod
353     def initialize_devices(requests, host=SystemHost(), name_base='Managed', simulator_ui=True, timeout=SIMULATOR_BOOT_TIMEOUT, **kwargs):
354         if SimulatedDeviceManager.INITIALIZED_DEVICES is not None:
355             return SimulatedDeviceManager.INITIALIZED_DEVICES
356
357         if not host.platform.is_mac():
358             return None
359
360         SimulatedDeviceManager.INITIALIZED_DEVICES = []
361         atexit.register(SimulatedDeviceManager.tear_down)
362
363         # Convert to iterable type
364         if not hasattr(requests, '__iter__'):
365             requests = [requests]
366
367         # Check running sims
368         for device in SimulatedDeviceManager.available_devices(host):
369             matched_request = SimulatedDeviceManager._does_fulfill_request(device, requests)
370             if matched_request is None:
371                 continue
372             requests.remove(matched_request)
373             _log.debug('Attached to running simulator {}'.format(device))
374             SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
375
376             # DeviceRequests are compared by reference
377             requests_copy = [request for request in requests]
378
379             # Merging requests means that if 4 devices are requested, but only one is running, these
380             # 4 requests will be fulfilled by the 1 running device.
381             for request in requests_copy:
382                 if not request.merge_requests:
383                     continue
384                 if not request.use_booted_simulator:
385                     continue
386                 if request.device_type != device.device_type and not request.allow_incomplete_match:
387                     continue
388                 if request.device_type.software_variant != device.device_type.software_variant:
389                     continue
390                 requests.remove(request)
391
392         for request in requests:
393             device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
394             assert device is not None
395
396             SimulatedDeviceManager._boot_device(device, host)
397
398         if simulator_ui and host.executive.run_command(['killall', '-0', 'Simulator'], return_exit_code=True) != 0:
399             SimulatedDeviceManager._managing_simulator_app = not host.executive.run_command(['open', '-g', '-b', SimulatedDeviceManager.simulator_bundle_id], return_exit_code=True)
400
401         deadline = time.time() + timeout
402         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
403             SimulatedDeviceManager._wait_until_device_is_usable(device, deadline)
404
405         return SimulatedDeviceManager.INITIALIZED_DEVICES
406
407     @staticmethod
408     @memoized
409     def max_supported_simulators(host=SystemHost()):
410         if not host.platform.is_mac():
411             return 0
412
413         try:
414             system_process_count_limit = int(host.executive.run_command(['/usr/bin/ulimit', '-u']).strip())
415             current_process_count = len(host.executive.run_command(['/bin/ps', 'aux']).strip().split('\n'))
416             _log.debug('Process limit: {}, current #processes: {}'.format(system_process_count_limit, current_process_count))
417         except (ValueError, ScriptError):
418             return 0
419
420         max_supported_simulators_for_hardware = min(host.executive.cpu_count() / 2, host.platform.total_bytes_memory() // SimulatedDeviceManager.MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE)
421         max_supported_simulators_locally = (system_process_count_limit - current_process_count) // SimulatedDeviceManager.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
422
423         if (max_supported_simulators_locally < max_supported_simulators_for_hardware):
424             _log.warn('This machine could support {} simulators, but is only configured for {}.'.format(max_supported_simulators_for_hardware, max_supported_simulators_locally))
425             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
426
427         if max_supported_simulators_locally == 0:
428             max_supported_simulators_locally = 1
429
430         return min(max_supported_simulators_locally, max_supported_simulators_for_hardware)
431
432     @staticmethod
433     def swap(device, request, host=SystemHost(), name_base='Managed', timeout=SIMULATOR_BOOT_TIMEOUT):
434         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
435             raise RuntimeError('Cannot swap when there are no initialized devices')
436         if device not in SimulatedDeviceManager.INITIALIZED_DEVICES:
437             raise RuntimeError('{} is not initialized, cannot swap it'.format(device))
438
439         index = SimulatedDeviceManager.INITIALIZED_DEVICES.index(device)
440         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = None
441         device.platform_device._tear_down()
442
443         device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
444         assert device
445
446         if not device.platform_device.is_booted_or_booting(force_update=True):
447             device.platform_device.booted_by_script = True
448             _log.debug("Booting device '{}'".format(device.udid))
449             host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
450         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = device
451
452         deadline = time.time() + timeout
453         SimulatedDeviceManager._wait_until_device_is_usable(device, max(0, deadline - time.time()))
454
455     @staticmethod
456     def tear_down(host=SystemHost(), timeout=SIMULATOR_BOOT_TIMEOUT):
457         if SimulatedDeviceManager._managing_simulator_app:
458             host.executive.run_command(['killall', '-9', 'Simulator'], return_exit_code=True)
459             SimulatedDeviceManager._managing_simulator_app = False
460
461         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
462             return
463
464         deadline = time.time() + timeout
465         while SimulatedDeviceManager.INITIALIZED_DEVICES:
466             device = SimulatedDeviceManager.INITIALIZED_DEVICES[0]
467             if device is None:
468                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(None)
469                 continue
470             device.platform_device._tear_down(deadline - time.time())
471
472         SimulatedDeviceManager.INITIALIZED_DEVICES = None
473
474
475 class SimulatedDevice(object):
476     class DeviceState:
477         CREATING = 0
478         SHUT_DOWN = 1
479         BOOTING = 2
480         BOOTED = 3
481         SHUTTING_DOWN = 4
482
483     NUM_INSTALL_RETRIES = 5
484     NAME_FOR_STATE = [
485         'CREATING',
486         'SHUTDOWN',
487         'BOOTING',
488         'BOOTED',
489         'SHUTTING DOWN',
490     ]
491
492     def __init__(self, name, udid, host, device_type, build_version):
493         assert device_type.software_version
494
495         self.name = name
496         self.udid = udid
497         self.device_type = device_type
498         self.build_version = build_version
499         self._state = SimulatedDevice.DeviceState.SHUTTING_DOWN
500         self._last_updated_state = time.time()
501
502         self.executive = host.executive
503         self.filesystem = host.filesystem
504         self.platform = host.platform
505
506         # Determine tear down behavior
507         self.booted_by_script = False
508         self.managed_by_script = False
509
510     def state(self, force_update=False):
511         # Don't allow state to get stale
512         if not force_update and time.time() < self._last_updated_state + 1:
513             return self._state
514
515         device_plist = self.filesystem.expanduser(self.filesystem.join(SimulatedDeviceManager.simulator_device_path, self.udid, 'device.plist'))
516         self._state = int(plistlib.readPlist(self.filesystem.open_binary_file_for_reading(device_plist))['state'])
517         self._last_updated_state = time.time()
518         return self._state
519
520     def is_booted_or_booting(self, force_update=False):
521         if self.state(force_update=force_update) == SimulatedDevice.DeviceState.BOOTING or self.state() == SimulatedDevice.DeviceState.BOOTED:
522             return True
523         return False
524
525     def is_usable(self, force_update=False):
526         if self.state(force_update=force_update) != SimulatedDevice.DeviceState.BOOTED:
527             return False
528
529         if self.device_type.software_variant == 'iOS':
530             home_screen_service = 'com.apple.springboard.services'
531         elif self.device_type.software_variant == 'watchOS':
532             home_screen_service = 'com.apple.carousel.sessionservice'
533         else:
534             _log.debug('{} has no service to check if the device is usable'.format(self.device_type.software_variant))
535             return True
536
537         for line in self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'spawn', self.udid, 'launchctl', 'print', 'system']).splitlines():
538             if home_screen_service in line:
539                 return True
540         return False
541
542     def _shut_down(self, timeout=30.0):
543         deadline = time.time() + timeout
544
545         # Either shutdown is successful, or the device was already shutdown when we attempted to shut it down.
546         exit_code = self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'shutdown', self.udid], return_exit_code=True)
547         if exit_code != 0 and self.state() != SimulatedDevice.DeviceState.SHUT_DOWN:
548             raise RuntimeError('Failed to shutdown {} with exit code {}'.format(self.udid, exit_code))
549
550         while self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN:
551             time.sleep(.5)
552             if time.time() > deadline:
553                 raise RuntimeError('Timed out while waiting for {} to shut down'.format(self.udid))
554
555     def _delete(self, timeout=10.0):
556         deadline = time.time() + timeout
557         self._shut_down(deadline - time.time())
558         _log.debug("Removing device '{}'".format(self.name))
559         self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'delete', self.udid])
560
561         # This will (by design) fail if run more than once on the same SimulatedDevice
562         SimulatedDeviceManager.AVAILABLE_DEVICES.remove(self)
563
564     def _tear_down(self, timeout=10.0):
565         deadline = time.time() + timeout
566         if self.booted_by_script:
567             self._shut_down(deadline - time.time())
568         if self.managed_by_script:
569             self._delete(deadline - time.time())
570
571         # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
572         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
573             if isinstance(device, Device) and device.platform_device == self:
574                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(device)
575
576     def install_app(self, app_path, env=None):
577         # Even after carousel is running, it takes a few seconds for watchOS to allow installs.
578         for i in xrange(self.NUM_INSTALL_RETRIES):
579             exit_code = self.executive.run_command(['xcrun', 'simctl', 'install', self.udid, app_path], return_exit_code=True)
580             if exit_code == 0:
581                 return True
582
583             # Return code 204 indicates that the device is booting, a retry may be successful.
584             if exit_code == 204:
585                 time.sleep(5)
586                 continue
587             return False
588         return False
589
590     # FIXME: Increase timeout for <rdar://problem/31331576>
591     def launch_app(self, bundle_id, args, env=None, timeout=300):
592         environment_to_use = {}
593         SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'
594         for value in (env or {}):
595             if not value.startswith(SIMCTL_ENV_PREFIX):
596                 environment_to_use[SIMCTL_ENV_PREFIX + value] = env[value]
597             else:
598                 environment_to_use[value] = env[value]
599
600         # FIXME: This is a workaround for <rdar://problem/30172453>.
601         def _log_debug_error(error):
602             _log.debug(error.message_with_output())
603
604         output = None
605
606         with Timeout(timeout, RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))):
607             while True:
608                 output = self.executive.run_command(
609                     ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
610                     env=environment_to_use,
611                     error_handler=_log_debug_error,
612                 )
613                 match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
614                 # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
615                 if match and self.executive.check_running_pid(int(match.group('pid'))):
616                     break
617                 if match:
618                     _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
619                 else:
620                     _log.debug('simctl launch did not report a pid')
621
622         if match.group('bundle') != bundle_id:
623             raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
624         _log.debug('Returning pid {} of launched process'.format(match.group('pid')))
625         return int(match.group('pid'))
626
627     def __eq__(self, other):
628         return self.udid == other.udid
629
630     def __ne__(self, other):
631         return not self.__eq__(other)
632
633     def __repr__(self):
634         return '<Device "{name}": {udid}. State: {state}. Type: {type}>'.format(
635             name=self.name,
636             udid=self.udid,
637             state=SimulatedDevice.NAME_FOR_STATE[self.state()],
638             type=self.device_type)