33debde5beb7717351fe35060c6b15ac13a7dc4f
[WebKit-https.git] / Tools / Scripts / webkitpy / xcode / new_simulated_device.py
1 # Copyright (C) 2017 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     MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE = 2 * (1024 ** 3)  # 2GB a simulator.
65     PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 125
66
67     xcrun = '/usr/bin/xcrun'
68     simulator_device_path = '~/Library/Developer/CoreSimulator/Devices'
69     simulator_bundle_id = 'com.apple.iphonesimulator'
70     _device_identifier_to_name = {}
71     _managing_simulator_app = False
72
73     @staticmethod
74     def _create_runtimes(runtimes):
75         result = []
76         for runtime in runtimes:
77             if runtime['availability'] != '(available)':
78                 continue
79             try:
80                 result.append(SimulatedDeviceManager.Runtime(runtime))
81             except (ValueError, AssertionError):
82                 continue
83         return result
84
85     @staticmethod
86     def _create_device_with_runtime(host, runtime, device_info):
87         if device_info['availability'] != '(available)':
88             return None
89
90         # Check existing devices.
91         for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
92             if device.udid == device_info['udid']:
93                 return device
94
95         # Check that the device.plist exists
96         device_plist = host.filesystem.expanduser(host.filesystem.join(SimulatedDeviceManager.simulator_device_path, device_info['udid'], 'device.plist'))
97         if not host.filesystem.isfile(device_plist):
98             return None
99
100         # Find device type. If we can't parse the device type, ignore this device.
101         try:
102             device_type_string = SimulatedDeviceManager._device_identifier_to_name[plistlib.readPlist(host.filesystem.open_binary_file_for_reading(device_plist))['deviceType']]
103             device_type = DeviceType.from_string(device_type_string, runtime.version)
104             assert device_type.software_variant == runtime.os_variant
105         except (ValueError, AssertionError):
106             return None
107
108         result = Device(SimulatedDevice(
109             name=device_info['name'],
110             udid=device_info['udid'],
111             host=host,
112             device_type=device_type,
113         ))
114         SimulatedDeviceManager.AVAILABLE_DEVICES.append(result)
115         return result
116
117     @staticmethod
118     def populate_available_devices(host=SystemHost()):
119         if not host.platform.is_mac():
120             return
121
122         try:
123             simctl_json = json.loads(host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'list', '--json']))
124         except (ValueError, ScriptError):
125             return
126
127         SimulatedDeviceManager._device_identifier_to_name = {device['identifier']: device['name'] for device in simctl_json['devicetypes']}
128         SimulatedDeviceManager.AVAILABLE_RUNTIMES = SimulatedDeviceManager._create_runtimes(simctl_json['runtimes'])
129
130         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
131             for device_json in simctl_json['devices'][runtime.name]:
132                 device = SimulatedDeviceManager._create_device_with_runtime(host, runtime, device_json)
133                 if not device:
134                     continue
135
136                 # Update device state from simctl output.
137                 device.platform_device._state = SimulatedDevice.NAME_FOR_STATE.index(device_json['state'].upper())
138                 device.platform_device._last_updated_state = time.time()
139         return
140
141     @staticmethod
142     def available_devices(host=SystemHost()):
143         if SimulatedDeviceManager.AVAILABLE_DEVICES == []:
144             SimulatedDeviceManager.populate_available_devices(host)
145         return SimulatedDeviceManager.AVAILABLE_DEVICES
146
147     @staticmethod
148     def device_by_filter(filter, host=SystemHost()):
149         result = []
150         for device in SimulatedDeviceManager.available_devices(host):
151             if filter(device):
152                 result.append(device)
153         return result
154
155     @staticmethod
156     def _find_exisiting_device_for_request(request):
157         if not request.use_existing_simulator:
158             return None
159         for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
160             # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
161             for initialized_device in SimulatedDeviceManager.INITIALIZED_DEVICES:
162                 if isinstance(initialized_device, Device) and device == initialized_device:
163                     device = None
164                     break
165             if device and request.device_type == device.platform_device.device_type:
166                 return device
167         return None
168
169     @staticmethod
170     def _find_available_name(name_base):
171         created_index = 0
172         while True:
173             name = '{} {}'.format(name_base, created_index)
174             created_index += 1
175             for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
176                 if device is None:
177                     continue
178                 if device.platform_device.name == name:
179                     break
180             else:
181                 return name
182
183     @staticmethod
184     def get_runtime_for_device_type(device_type):
185         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
186             if runtime.os_variant == device_type.software_variant and (device_type.software_version is None or device_type.software_version == runtime.version):
187                 return runtime
188
189         # Allow for a partial version match.
190         for runtime in SimulatedDeviceManager.AVAILABLE_RUNTIMES:
191             if runtime.os_variant == device_type.software_variant and runtime.version in device_type.software_version:
192                 return runtime
193         return None
194
195     @staticmethod
196     def _disambiguate_device_type(device_type):
197         full_type = DeviceType(
198             hardware_family=device_type.hardware_family,
199             hardware_type=device_type.hardware_type,
200             software_version=device_type.software_version,
201             software_variant=device_type.software_variant)
202
203         runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
204         assert runtime is not None
205         full_type.software_version = runtime.version
206
207         if device_type.hardware_family is None:
208             # We use the existing devices to determine a legal family if no family is specified
209             for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
210                 if device.platform_device.device_type == device_type:
211                     full_type.hardware_family = device.platform_device.device_type.hardware_family
212                     break
213
214         if device_type.hardware_type is None:
215             # Again, we use the existing devices to determine a legal hardware type
216             for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
217                 if device.platform_device.device_type == device_type:
218                     full_type.hardware_type = device.platform_device.device_type.hardware_type
219                     break
220
221         full_type.check_consistency()
222         return full_type
223
224     @staticmethod
225     def _get_device_identifier_for_type(device_type):
226         for type_id, type_name in SimulatedDeviceManager._device_identifier_to_name.iteritems():
227             if type_name == '{} {}'.format(device_type.hardware_family, device_type.hardware_type):
228                 return type_id
229         return None
230
231     @staticmethod
232     def _create_or_find_device_for_request(request, host=SystemHost(), name_base='Managed'):
233         assert isinstance(request, DeviceRequest)
234
235         device = SimulatedDeviceManager._find_exisiting_device_for_request(request)
236         if device:
237             return device
238
239         name = SimulatedDeviceManager._find_available_name(name_base)
240         device_type = SimulatedDeviceManager._disambiguate_device_type(request.device_type)
241         runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
242         device_identifier = SimulatedDeviceManager._get_device_identifier_for_type(device_type)
243
244         assert runtime is not None
245         assert device_identifier is not None
246
247         for device in SimulatedDeviceManager.available_devices(host):
248             if device.platform_device.name == name:
249                 device.platform_device._delete()
250                 break
251
252         _log.debug("Creating device '{}', of type {}".format(name, device_type))
253         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'create', name, device_identifier, runtime.identifier])
254
255         # We just added a device, so our list of _available_devices needs to be re-synced.
256         SimulatedDeviceManager.populate_available_devices(host)
257         for device in SimulatedDeviceManager.available_devices(host):
258             if device.platform_device.name == name:
259                 device.platform_device.managed_by_script = True
260                 return device
261         return None
262
263     @staticmethod
264     def _does_fulfill_request(device, requests):
265         if not device.platform_device.is_booted_or_booting():
266             return None
267
268         # Exact match.
269         for request in requests:
270             if not request.use_booted_simulator:
271                 continue
272             if request.device_type == device.platform_device.device_type:
273                 _log.debug("The request for '{}' matched {} exactly".format(request.device_type, device))
274                 return request
275
276         # Contained-in match.
277         for request in requests:
278             if not request.use_booted_simulator:
279                 continue
280             if device.platform_device.device_type in request.device_type:
281                 _log.debug("The request for '{}' fuzzy-matched {}".format(request.device_type, device))
282                 return request
283
284         # DeviceRequests are compared by reference
285         requests_copy = [request for request in requests]
286
287         # Check for an incomplete match
288         # This is usually used when we don't want to take the time to start a simulator and would
289         # rather use the one the user has already started, even if it isn't quite what we're looking for.
290         for request in requests_copy:
291             if not request.use_booted_simulator or not request.allow_incomplete_match:
292                 continue
293             if request.device_type.software_variant == device.platform_device.device_type.software_variant:
294                 _log.warn("The request for '{}' incomplete-matched {}".format(request.device_type, device))
295                 _log.warn("This may cause unexpected behavior in code that expected the device type {}".format(request.device_type))
296                 return request
297         return None
298
299     @staticmethod
300     def _wait_until_device_in_state(device, state, deadline):
301         while device.platform_device.state(force_update=True) != state:
302             _log.debug('Waiting on {} to enter state {}...'.format(device, SimulatedDevice.NAME_FOR_STATE[state]))
303             time.sleep(1)
304             if time.time() > deadline:
305                 raise RuntimeError('Timed out while waiting for all devices to boot')
306
307     @staticmethod
308     def _boot_device(device, host=SystemHost()):
309         _log.debug("Booting device '{}'".format(device.udid))
310         device.platform_device.booted_by_script = True
311         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
312         SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
313
314     @staticmethod
315     def initialize_devices(requests, host=SystemHost(), name_base='Managed', simulator_ui=True, timeout=60, **kwargs):
316         if SimulatedDeviceManager.INITIALIZED_DEVICES is not None:
317             return SimulatedDeviceManager.INITIALIZED_DEVICES
318
319         if not host.platform.is_mac():
320             return None
321
322         SimulatedDeviceManager.INITIALIZED_DEVICES = []
323         atexit.register(SimulatedDeviceManager.tear_down)
324
325         # Convert to iterable type
326         if not hasattr(requests, '__iter__'):
327             requests = [requests]
328
329         # Check running sims
330         for device in SimulatedDeviceManager.available_devices(host):
331             matched_request = SimulatedDeviceManager._does_fulfill_request(device, requests)
332             if matched_request is None:
333                 continue
334             requests.remove(matched_request)
335             _log.debug('Attached to running simulator {}'.format(device))
336             SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
337
338             # DeviceRequests are compared by reference
339             requests_copy = [request for request in requests]
340
341             # Merging requests means that if 4 devices are requested, but only one is running, these
342             # 4 requests will be fulfilled by the 1 running device.
343             for request in requests_copy:
344                 if not request.merge_requests:
345                     continue
346                 if not request.use_booted_simulator:
347                     continue
348                 if request.device_type != device.platform_device.device_type and not request.allow_incomplete_match:
349                     continue
350                 if request.device_type.software_variant != device.platform_device.device_type.software_variant:
351                     continue
352                 requests.remove(request)
353
354         for request in requests:
355             device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
356             assert device is not None
357
358             SimulatedDeviceManager._boot_device(device, host)
359
360         if simulator_ui and host.executive.run_command(['killall', '-0', 'Simulator'], return_exit_code=True) != 0:
361             SimulatedDeviceManager._managing_simulator_app = not host.executive.run_command(['open', '-g', '-b', SimulatedDeviceManager.simulator_bundle_id], return_exit_code=True)
362
363         deadline = time.time() + timeout
364         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
365             SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
366
367         return SimulatedDeviceManager.INITIALIZED_DEVICES
368
369     @staticmethod
370     @memoized
371     def max_supported_simulators(host=SystemHost()):
372         if not host.platform.is_mac():
373             return 0
374
375         try:
376             system_process_count_limit = int(host.executive.run_command(['/usr/bin/ulimit', '-u']).strip())
377             current_process_count = len(host.executive.run_command(['/bin/ps', 'aux']).strip().split('\n'))
378             _log.debug('Process limit: {}, current #processes: {}'.format(system_process_count_limit, current_process_count))
379         except (ValueError, ScriptError):
380             return 0
381
382         max_supported_simulators_for_hardware = min(host.executive.cpu_count() / 2, host.platform.total_bytes_memory() // SimulatedDeviceManager.MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE)
383         max_supported_simulators_locally = (system_process_count_limit - current_process_count) // SimulatedDeviceManager.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
384
385         if (max_supported_simulators_locally < max_supported_simulators_for_hardware):
386             _log.warn('This machine could support {} simulators, but is only configured for {}.'.format(max_supported_simulators_for_hardware, max_supported_simulators_locally))
387             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
388
389         if max_supported_simulators_locally == 0:
390             max_supported_simulators_locally = 1
391
392         return min(max_supported_simulators_locally, max_supported_simulators_for_hardware)
393
394     @staticmethod
395     def swap(device, request, host=SystemHost(), name_base='Managed', timeout=60):
396         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
397             raise RuntimeError('Cannot swap when there are no initialized devices')
398         if device not in SimulatedDeviceManager.INITIALIZED_DEVICES:
399             raise RuntimeError('{} is not initialized, cannot swap it'.format(device))
400
401         index = SimulatedDeviceManager.INITIALIZED_DEVICES.index(device)
402         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = None
403         device.platform_device._tear_down()
404
405         device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
406         assert device
407
408         if not device.platform_device.is_booted_or_booting(force_update=True):
409             device.platform_device.booted_by_script = True
410             _log.debug("Booting device '{}'".format(device.udid))
411             host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
412         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = device
413
414         deadline = time.time() + timeout
415         SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
416
417     @staticmethod
418     def tear_down(host=SystemHost(), timeout=60):
419         if SimulatedDeviceManager._managing_simulator_app:
420             host.executive.run_command(['killall', '-9', 'Simulator'], return_exit_code=True)
421             SimulatedDeviceManager._managing_simulator_app = False
422
423         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
424             return
425
426         deadline = time.time() + timeout
427         while SimulatedDeviceManager.INITIALIZED_DEVICES:
428             device = SimulatedDeviceManager.INITIALIZED_DEVICES[0]
429             if device is None:
430                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(None)
431                 continue
432             device.platform_device._tear_down(deadline - time.time())
433
434         SimulatedDeviceManager.INITIALIZED_DEVICES = None
435
436
437 class SimulatedDevice(object):
438     class DeviceState:
439         CREATING = 0
440         SHUT_DOWN = 1
441         BOOTING = 2
442         BOOTED = 3
443         SHUTTING_DOWN = 4
444
445     NAME_FOR_STATE = [
446         'CREATING',
447         'SHUTDOWN',
448         'BOOTING',
449         'BOOTED',
450         'SHUTTING DOWN',
451     ]
452
453     def __init__(self, name, udid, host, device_type):
454         assert device_type.software_version
455
456         self.name = name
457         self.udid = udid
458         self.device_type = device_type
459         self._state = SimulatedDevice.DeviceState.SHUTTING_DOWN
460         self._last_updated_state = time.time()
461
462         self.executive = host.executive
463         self.filesystem = host.filesystem
464         self.platform = host.platform
465
466         # Determine tear down behavior
467         self.booted_by_script = False
468         self.managed_by_script = False
469
470     def state(self, force_update=False):
471         # Don't allow state to get stale
472         if not force_update and time.time() < self._last_updated_state + 1:
473             return self._state
474
475         device_plist = self.filesystem.expanduser(self.filesystem.join(SimulatedDeviceManager.simulator_device_path, self.udid, 'device.plist'))
476         self._state = int(plistlib.readPlist(self.filesystem.open_binary_file_for_reading(device_plist))['state'])
477         self._last_updated_state = time.time()
478         return self._state
479
480     def is_booted_or_booting(self, force_update=False):
481         if self.state(force_update=force_update) == SimulatedDevice.DeviceState.BOOTING or self.state() == SimulatedDevice.DeviceState.BOOTED:
482             return True
483         return False
484
485     def _shut_down(self, timeout=10.0):
486         deadline = time.time() + timeout
487
488         if self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN and self.state != SimulatedDevice.DeviceState.SHUTTING_DOWN:
489             self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'shutdown', self.udid])
490
491         while self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN:
492             time.sleep(.5)
493             if time.time() > deadline:
494                 raise RuntimeError('Timed out while waiting for {} to shut down'.format(self.udid))
495
496     def _delete(self, timeout=10.0):
497         deadline = time.time() + timeout
498         self._shut_down(deadline - time.time())
499         _log.debug("Removing device '{}'".format(self.name))
500         self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'delete', self.udid])
501
502         # This will (by design) fail if run more than once on the same SimulatedDevice
503         SimulatedDeviceManager.AVAILABLE_DEVICES.remove(self)
504
505     def _tear_down(self, timeout=10.0):
506         deadline = time.time() + timeout
507         if self.booted_by_script:
508             self._shut_down(deadline - time.time())
509         if self.managed_by_script:
510             self._delete(deadline - time.time())
511
512         # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
513         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
514             if isinstance(device, Device) and device.platform_device == self:
515                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(device)
516
517     def install_app(self, app_path, env=None):
518         return not self.executive.run_command(['xcrun', 'simctl', 'install', self.udid, app_path], return_exit_code=True)
519
520     # FIXME: Increase timeout for <rdar://problem/31331576>
521     def launch_app(self, bundle_id, args, env=None, timeout=300):
522         environment_to_use = {}
523         SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'
524         for value in (env or {}):
525             if not value.startswith(SIMCTL_ENV_PREFIX):
526                 environment_to_use[SIMCTL_ENV_PREFIX + value] = env[value]
527             else:
528                 environment_to_use[value] = env[value]
529
530         # FIXME: This is a workaround for <rdar://problem/30172453>.
531         def _log_debug_error(error):
532             _log.debug(error.message_with_output())
533
534         output = None
535
536         with Timeout(timeout, RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))):
537             while True:
538                 output = self.executive.run_command(
539                     ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
540                     env=environment_to_use,
541                     error_handler=_log_debug_error,
542                 )
543                 match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
544                 # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
545                 if match and self.executive.check_running_pid(int(match.group('pid'))):
546                     break
547                 if match:
548                     _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
549                 else:
550                     _log.debug('simctl launch did not report a pid')
551
552         if match.group('bundle') != bundle_id:
553             raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
554         _log.debug('Returning pid {} of launched process'.format(match.group('pid')))
555         return int(match.group('pid'))
556
557     def __eq__(self, other):
558         return self.udid == other.udid
559
560     def __ne__(self, other):
561         return not self.__eq__(other)
562
563     def __repr__(self):
564         return '<Device "{name}": {udid}. State: {state}. Type: {type}>'.format(
565             name=self.name,
566             udid=self.udid,
567             state=SimulatedDevice.NAME_FOR_STATE[self.state()],
568             type=self.device_type)