@@ -194,6 +194,11 @@ class Xcode {
194
194
}
195
195
}
196
196
197
+ enum XCDeviceEvent {
198
+ attach,
199
+ detach,
200
+ }
201
+
197
202
/// A utility class for interacting with Xcode xcdevice command line tools.
198
203
class XCDevice {
199
204
XCDevice ({
@@ -218,14 +223,34 @@ class XCDevice {
218
223
platform: platform,
219
224
processManager: processManager,
220
225
),
221
- _xcode = xcode;
226
+ _xcode = xcode {
227
+
228
+ _setupDeviceIdentifierByEventStream ();
229
+ }
230
+
231
+ void dispose () {
232
+ _deviceObservationProcess? .kill ();
233
+ }
222
234
223
235
final ProcessUtils _processUtils;
224
236
final Logger _logger;
225
237
final IMobileDevice _iMobileDevice;
226
238
final IOSDeploy _iosDeploy;
227
239
final Xcode _xcode;
228
240
241
+ List <dynamic > _cachedListResults;
242
+ Process _deviceObservationProcess;
243
+ StreamController <Map <XCDeviceEvent , String >> _deviceIdentifierByEvent;
244
+
245
+ void _setupDeviceIdentifierByEventStream () {
246
+ // _deviceIdentifierByEvent Should always be available for listeners
247
+ // in case polling needs to be stopped and restarted.
248
+ _deviceIdentifierByEvent = StreamController <Map <XCDeviceEvent , String >>.broadcast (
249
+ onListen: _startObservingTetheredIOSDevices,
250
+ onCancel: _stopObservingTetheredIOSDevices,
251
+ );
252
+ }
253
+
229
254
bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null ;
230
255
231
256
String _xcdevicePath;
@@ -287,7 +312,99 @@ class XCDevice {
287
312
return null ;
288
313
}
289
314
290
- List <dynamic > _cachedListResults;
315
+ /// Observe identifiers (UDIDs) of devices as they attach and detach.
316
+ ///
317
+ /// Each attach and detach event is a tuple of one event type
318
+ /// and identifier.
319
+ Stream <Map <XCDeviceEvent , String >> observedDeviceEvents () {
320
+ if (! isInstalled) {
321
+ _logger.printTrace ("Xcode not found. Run 'flutter doctor' for more information." );
322
+ return null ;
323
+ }
324
+ return _deviceIdentifierByEvent.stream;
325
+ }
326
+
327
+ // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
328
+ // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
329
+ final RegExp _observationIdentifierPattern = RegExp (r'^(\w*): (\w*)$' );
330
+
331
+ Future <void > _startObservingTetheredIOSDevices () async {
332
+ try {
333
+ if (_deviceObservationProcess != null ) {
334
+ throw Exception ('xcdevice observe restart failed' );
335
+ }
336
+
337
+ // Run in interactive mode (via script) to convince
338
+ // xcdevice it has a terminal attached in order to redirect stdout.
339
+ _deviceObservationProcess = await _processUtils.start (
340
+ < String > [
341
+ 'script' ,
342
+ '-t' ,
343
+ '0' ,
344
+ '/dev/null' ,
345
+ 'xcrun' ,
346
+ 'xcdevice' ,
347
+ 'observe' ,
348
+ '--both' ,
349
+ ],
350
+ );
351
+
352
+ final StreamSubscription <String > stdoutSubscription = _deviceObservationProcess.stdout
353
+ .transform <String >(utf8.decoder)
354
+ .transform <String >(const LineSplitter ())
355
+ .listen ((String line) {
356
+
357
+ // xcdevice observe example output of UDIDs:
358
+ //
359
+ // Listening for all devices, on both interfaces.
360
+ // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
361
+ // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
362
+ // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
363
+ final RegExpMatch match = _observationIdentifierPattern.firstMatch (line);
364
+ if (match != null && match.groupCount == 2 ) {
365
+ final String verb = match.group (1 ).toLowerCase ();
366
+ final String identifier = match.group (2 );
367
+ if (verb.startsWith ('attach' )) {
368
+ _deviceIdentifierByEvent.add (< XCDeviceEvent , String > {
369
+ XCDeviceEvent .attach: identifier
370
+ });
371
+ } else if (verb.startsWith ('detach' )) {
372
+ _deviceIdentifierByEvent.add (< XCDeviceEvent , String > {
373
+ XCDeviceEvent .detach: identifier
374
+ });
375
+ }
376
+ }
377
+ });
378
+ final StreamSubscription <String > stderrSubscription = _deviceObservationProcess.stderr
379
+ .transform <String >(utf8.decoder)
380
+ .transform <String >(const LineSplitter ())
381
+ .listen ((String line) {
382
+ _logger.printTrace ('xcdevice observe error: $line ' );
383
+ });
384
+ unawaited (_deviceObservationProcess.exitCode.then ((int status) {
385
+ _logger.printTrace ('xcdevice exited with code $exitCode ' );
386
+ unawaited (stdoutSubscription.cancel ());
387
+ unawaited (stderrSubscription.cancel ());
388
+ }).whenComplete (() async {
389
+ if (_deviceIdentifierByEvent.hasListener) {
390
+ // Tell listeners the process died.
391
+ await _deviceIdentifierByEvent.close ();
392
+ }
393
+ _deviceObservationProcess = null ;
394
+
395
+ // Reopen it so new listeners can resume polling.
396
+ _setupDeviceIdentifierByEventStream ();
397
+ }));
398
+ } on ProcessException catch (exception, stackTrace) {
399
+ _deviceIdentifierByEvent.addError (exception, stackTrace);
400
+ } on ArgumentError catch (exception, stackTrace) {
401
+ _deviceIdentifierByEvent.addError (exception, stackTrace);
402
+ }
403
+ }
404
+
405
+ void _stopObservingTetheredIOSDevices () {
406
+ _deviceObservationProcess? .kill ();
407
+ }
291
408
292
409
/// [timeout] defaults to 2 seconds.
293
410
Future <List <IOSDevice >> getAvailableTetheredIOSDevices ({ Duration timeout }) async {
0 commit comments