webkitpy: Use partially disambiguated type in SimulatedDeviceManager._disambiguate_de...
[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         # Copy by value since we do not want to modify the DeviceType passed in.
198         full_device_type = DeviceType(
199             hardware_family=device_type.hardware_family,
200             hardware_type=device_type.hardware_type,
201             software_version=device_type.software_version,
202             software_variant=device_type.software_variant)
203
204         runtime = SimulatedDeviceManager.get_runtime_for_device_type(full_device_type)
205         assert runtime is not None
206         full_device_type.software_version = runtime.version
207
208         if full_device_type.hardware_family is None:
209             # We use the existing devices to determine a legal family if no family is specified
210             for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
211                 if device.platform_device.device_type == full_device_type:
212                     full_device_type.hardware_family = device.platform_device.device_type.hardware_family
213                     break
214
215         if full_device_type.hardware_type is None:
216             # Again, we use the existing devices to determine a legal hardware type
217             for device in SimulatedDeviceManager.AVAILABLE_DEVICES:
218                 if device.platform_device.device_type == full_device_type:
219                     full_device_type.hardware_type = device.platform_device.device_type.hardware_type
220                     break
221
222         full_device_type.check_consistency()
223         return full_device_type
224
225     @staticmethod
226     def _get_device_identifier_for_type(device_type):
227         for type_id, type_name in SimulatedDeviceManager._device_identifier_to_name.iteritems():
228             if type_name == '{} {}'.format(device_type.hardware_family, device_type.hardware_type):
229                 return type_id
230         return None
231
232     @staticmethod
233     def _create_or_find_device_for_request(request, host=SystemHost(), name_base='Managed'):
234         assert isinstance(request, DeviceRequest)
235
236         device = SimulatedDeviceManager._find_exisiting_device_for_request(request)
237         if device:
238             return device
239
240         name = SimulatedDeviceManager._find_available_name(name_base)
241         device_type = SimulatedDeviceManager._disambiguate_device_type(request.device_type)
242         runtime = SimulatedDeviceManager.get_runtime_for_device_type(device_type)
243         device_identifier = SimulatedDeviceManager._get_device_identifier_for_type(device_type)
244
245         assert runtime is not None
246         assert device_identifier is not None
247
248         for device in SimulatedDeviceManager.available_devices(host):
249             if device.platform_device.name == name:
250                 device.platform_device._delete()
251                 break
252
253         _log.debug("Creating device '{}', of type {}".format(name, device_type))
254         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'create', name, device_identifier, runtime.identifier])
255
256         # We just added a device, so our list of _available_devices needs to be re-synced.
257         SimulatedDeviceManager.populate_available_devices(host)
258         for device in SimulatedDeviceManager.available_devices(host):
259             if device.platform_device.name == name:
260                 device.platform_device.managed_by_script = True
261                 return device
262         return None
263
264     @staticmethod
265     def _does_fulfill_request(device, requests):
266         if not device.platform_device.is_booted_or_booting():
267             return None
268
269         # Exact match.
270         for request in requests:
271             if not request.use_booted_simulator:
272                 continue
273             if request.device_type == device.platform_device.device_type:
274                 _log.debug("The request for '{}' matched {} exactly".format(request.device_type, device))
275                 return request
276
277         # Contained-in match.
278         for request in requests:
279             if not request.use_booted_simulator:
280                 continue
281             if device.platform_device.device_type in request.device_type:
282                 _log.debug("The request for '{}' fuzzy-matched {}".format(request.device_type, device))
283                 return request
284
285         # DeviceRequests are compared by reference
286         requests_copy = [request for request in requests]
287
288         # Check for an incomplete match
289         # This is usually used when we don't want to take the time to start a simulator and would
290         # rather use the one the user has already started, even if it isn't quite what we're looking for.
291         for request in requests_copy:
292             if not request.use_booted_simulator or not request.allow_incomplete_match:
293                 continue
294             if request.device_type.software_variant == device.platform_device.device_type.software_variant:
295                 _log.warn("The request for '{}' incomplete-matched {}".format(request.device_type, device))
296                 _log.warn("This may cause unexpected behavior in code that expected the device type {}".format(request.device_type))
297                 return request
298         return None
299
300     @staticmethod
301     def _wait_until_device_in_state(device, state, deadline):
302         while device.platform_device.state(force_update=True) != state:
303             _log.debug('Waiting on {} to enter state {}...'.format(device, SimulatedDevice.NAME_FOR_STATE[state]))
304             time.sleep(1)
305             if time.time() > deadline:
306                 raise RuntimeError('Timed out while waiting for all devices to boot')
307
308     @staticmethod
309     def _boot_device(device, host=SystemHost()):
310         _log.debug("Booting device '{}'".format(device.udid))
311         device.platform_device.booted_by_script = True
312         host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
313         SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
314
315     @staticmethod
316     def initialize_devices(requests, host=SystemHost(), name_base='Managed', simulator_ui=True, timeout=60, **kwargs):
317         if SimulatedDeviceManager.INITIALIZED_DEVICES is not None:
318             return SimulatedDeviceManager.INITIALIZED_DEVICES
319
320         if not host.platform.is_mac():
321             return None
322
323         SimulatedDeviceManager.INITIALIZED_DEVICES = []
324         atexit.register(SimulatedDeviceManager.tear_down)
325
326         # Convert to iterable type
327         if not hasattr(requests, '__iter__'):
328             requests = [requests]
329
330         # Check running sims
331         for device in SimulatedDeviceManager.available_devices(host):
332             matched_request = SimulatedDeviceManager._does_fulfill_request(device, requests)
333             if matched_request is None:
334                 continue
335             requests.remove(matched_request)
336             _log.debug('Attached to running simulator {}'.format(device))
337             SimulatedDeviceManager.INITIALIZED_DEVICES.append(device)
338
339             # DeviceRequests are compared by reference
340             requests_copy = [request for request in requests]
341
342             # Merging requests means that if 4 devices are requested, but only one is running, these
343             # 4 requests will be fulfilled by the 1 running device.
344             for request in requests_copy:
345                 if not request.merge_requests:
346                     continue
347                 if not request.use_booted_simulator:
348                     continue
349                 if request.device_type != device.platform_device.device_type and not request.allow_incomplete_match:
350                     continue
351                 if request.device_type.software_variant != device.platform_device.device_type.software_variant:
352                     continue
353                 requests.remove(request)
354
355         for request in requests:
356             device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
357             assert device is not None
358
359             SimulatedDeviceManager._boot_device(device, host)
360
361         if simulator_ui and host.executive.run_command(['killall', '-0', 'Simulator'], return_exit_code=True) != 0:
362             SimulatedDeviceManager._managing_simulator_app = not host.executive.run_command(['open', '-g', '-b', SimulatedDeviceManager.simulator_bundle_id], return_exit_code=True)
363
364         deadline = time.time() + timeout
365         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
366             SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
367
368         return SimulatedDeviceManager.INITIALIZED_DEVICES
369
370     @staticmethod
371     @memoized
372     def max_supported_simulators(host=SystemHost()):
373         if not host.platform.is_mac():
374             return 0
375
376         try:
377             system_process_count_limit = int(host.executive.run_command(['/usr/bin/ulimit', '-u']).strip())
378             current_process_count = len(host.executive.run_command(['/bin/ps', 'aux']).strip().split('\n'))
379             _log.debug('Process limit: {}, current #processes: {}'.format(system_process_count_limit, current_process_count))
380         except (ValueError, ScriptError):
381             return 0
382
383         max_supported_simulators_for_hardware = min(host.executive.cpu_count() / 2, host.platform.total_bytes_memory() // SimulatedDeviceManager.MEMORY_ESTIMATE_PER_SIMULATOR_INSTANCE)
384         max_supported_simulators_locally = (system_process_count_limit - current_process_count) // SimulatedDeviceManager.PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE
385
386         if (max_supported_simulators_locally < max_supported_simulators_for_hardware):
387             _log.warn('This machine could support {} simulators, but is only configured for {}.'.format(max_supported_simulators_for_hardware, max_supported_simulators_locally))
388             _log.warn('Please see <https://trac.webkit.org/wiki/IncreasingKernelLimits>.')
389
390         if max_supported_simulators_locally == 0:
391             max_supported_simulators_locally = 1
392
393         return min(max_supported_simulators_locally, max_supported_simulators_for_hardware)
394
395     @staticmethod
396     def swap(device, request, host=SystemHost(), name_base='Managed', timeout=60):
397         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
398             raise RuntimeError('Cannot swap when there are no initialized devices')
399         if device not in SimulatedDeviceManager.INITIALIZED_DEVICES:
400             raise RuntimeError('{} is not initialized, cannot swap it'.format(device))
401
402         index = SimulatedDeviceManager.INITIALIZED_DEVICES.index(device)
403         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = None
404         device.platform_device._tear_down()
405
406         device = SimulatedDeviceManager._create_or_find_device_for_request(request, host, name_base)
407         assert device
408
409         if not device.platform_device.is_booted_or_booting(force_update=True):
410             device.platform_device.booted_by_script = True
411             _log.debug("Booting device '{}'".format(device.udid))
412             host.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'boot', device.udid])
413         SimulatedDeviceManager.INITIALIZED_DEVICES[index] = device
414
415         deadline = time.time() + timeout
416         SimulatedDeviceManager._wait_until_device_in_state(device, SimulatedDevice.DeviceState.BOOTED, deadline)
417
418     @staticmethod
419     def tear_down(host=SystemHost(), timeout=60):
420         if SimulatedDeviceManager._managing_simulator_app:
421             host.executive.run_command(['killall', '-9', 'Simulator'], return_exit_code=True)
422             SimulatedDeviceManager._managing_simulator_app = False
423
424         if SimulatedDeviceManager.INITIALIZED_DEVICES is None:
425             return
426
427         deadline = time.time() + timeout
428         while SimulatedDeviceManager.INITIALIZED_DEVICES:
429             device = SimulatedDeviceManager.INITIALIZED_DEVICES[0]
430             if device is None:
431                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(None)
432                 continue
433             device.platform_device._tear_down(deadline - time.time())
434
435         SimulatedDeviceManager.INITIALIZED_DEVICES = None
436
437
438 class SimulatedDevice(object):
439     class DeviceState:
440         CREATING = 0
441         SHUT_DOWN = 1
442         BOOTING = 2
443         BOOTED = 3
444         SHUTTING_DOWN = 4
445
446     NAME_FOR_STATE = [
447         'CREATING',
448         'SHUTDOWN',
449         'BOOTING',
450         'BOOTED',
451         'SHUTTING DOWN',
452     ]
453
454     def __init__(self, name, udid, host, device_type):
455         assert device_type.software_version
456
457         self.name = name
458         self.udid = udid
459         self.device_type = device_type
460         self._state = SimulatedDevice.DeviceState.SHUTTING_DOWN
461         self._last_updated_state = time.time()
462
463         self.executive = host.executive
464         self.filesystem = host.filesystem
465         self.platform = host.platform
466
467         # Determine tear down behavior
468         self.booted_by_script = False
469         self.managed_by_script = False
470
471     def state(self, force_update=False):
472         # Don't allow state to get stale
473         if not force_update and time.time() < self._last_updated_state + 1:
474             return self._state
475
476         device_plist = self.filesystem.expanduser(self.filesystem.join(SimulatedDeviceManager.simulator_device_path, self.udid, 'device.plist'))
477         self._state = int(plistlib.readPlist(self.filesystem.open_binary_file_for_reading(device_plist))['state'])
478         self._last_updated_state = time.time()
479         return self._state
480
481     def is_booted_or_booting(self, force_update=False):
482         if self.state(force_update=force_update) == SimulatedDevice.DeviceState.BOOTING or self.state() == SimulatedDevice.DeviceState.BOOTED:
483             return True
484         return False
485
486     def _shut_down(self, timeout=10.0):
487         deadline = time.time() + timeout
488
489         if self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN and self.state != SimulatedDevice.DeviceState.SHUTTING_DOWN:
490             self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'shutdown', self.udid])
491
492         while self.state(force_update=True) != SimulatedDevice.DeviceState.SHUT_DOWN:
493             time.sleep(.5)
494             if time.time() > deadline:
495                 raise RuntimeError('Timed out while waiting for {} to shut down'.format(self.udid))
496
497     def _delete(self, timeout=10.0):
498         deadline = time.time() + timeout
499         self._shut_down(deadline - time.time())
500         _log.debug("Removing device '{}'".format(self.name))
501         self.executive.run_command([SimulatedDeviceManager.xcrun, 'simctl', 'delete', self.udid])
502
503         # This will (by design) fail if run more than once on the same SimulatedDevice
504         SimulatedDeviceManager.AVAILABLE_DEVICES.remove(self)
505
506     def _tear_down(self, timeout=10.0):
507         deadline = time.time() + timeout
508         if self.booted_by_script:
509             self._shut_down(deadline - time.time())
510         if self.managed_by_script:
511             self._delete(deadline - time.time())
512
513         # One of the INITIALIZED_DEVICES may be None, so we can't just use __eq__
514         for device in SimulatedDeviceManager.INITIALIZED_DEVICES:
515             if isinstance(device, Device) and device.platform_device == self:
516                 SimulatedDeviceManager.INITIALIZED_DEVICES.remove(device)
517
518     def install_app(self, app_path, env=None):
519         return not self.executive.run_command(['xcrun', 'simctl', 'install', self.udid, app_path], return_exit_code=True)
520
521     # FIXME: Increase timeout for <rdar://problem/31331576>
522     def launch_app(self, bundle_id, args, env=None, timeout=300):
523         environment_to_use = {}
524         SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'
525         for value in (env or {}):
526             if not value.startswith(SIMCTL_ENV_PREFIX):
527                 environment_to_use[SIMCTL_ENV_PREFIX + value] = env[value]
528             else:
529                 environment_to_use[value] = env[value]
530
531         # FIXME: This is a workaround for <rdar://problem/30172453>.
532         def _log_debug_error(error):
533             _log.debug(error.message_with_output())
534
535         output = None
536
537         with Timeout(timeout, RuntimeError('Timed out waiting for process to open {} on {}'.format(bundle_id, self.udid))):
538             while True:
539                 output = self.executive.run_command(
540                     ['xcrun', 'simctl', 'launch', self.udid, bundle_id] + args,
541                     env=environment_to_use,
542                     error_handler=_log_debug_error,
543                 )
544                 match = re.match(r'(?P<bundle>[^:]+): (?P<pid>\d+)\n', output)
545                 # FIXME: We shouldn't need to check the PID <rdar://problem/31154075>.
546                 if match and self.executive.check_running_pid(int(match.group('pid'))):
547                     break
548                 if match:
549                     _log.debug('simctl launch reported pid {}, but this process is not running'.format(match.group('pid')))
550                 else:
551                     _log.debug('simctl launch did not report a pid')
552
553         if match.group('bundle') != bundle_id:
554             raise RuntimeError('Failed to find process id for {}: {}'.format(bundle_id, output))
555         _log.debug('Returning pid {} of launched process'.format(match.group('pid')))
556         return int(match.group('pid'))
557
558     def __eq__(self, other):
559         return self.udid == other.udid
560
561     def __ne__(self, other):
562         return not self.__eq__(other)
563
564     def __repr__(self):
565         return '<Device "{name}": {udid}. State: {state}. Type: {type}>'.format(
566             name=self.name,
567             udid=self.udid,
568             state=SimulatedDevice.NAME_FOR_STATE[self.state()],
569             type=self.device_type)