chan150 / flutter_blue_plus_windows

MIT License
11 stars 7 forks source link

fix: Canceling a connectionState subscription results in hang #14

Open jefflongo opened 2 weeks ago

jefflongo commented 2 weeks ago

Description

Awaiting a connectionState subscription cancellation never completes.

Steps To Reproduce

subscription = device.connectionState.listen(onConnectionState);
...
await subscription.cancel(); // <-- Hangs here
await device.disconnect();

Expected Behavior Expected behavior is that the subscription can be canceled so that the app can disconnect from the device without calling onConnectionState.

Additional Context

This code works on Android using the same version of Flutter Blue Plus (1.33.2)

chan150 commented 2 weeks ago

subscription = device.connectionState.listen(onConnectionState);

...

await subscription.cancel(); // <-- Hangs here

await device.disconnect();

Can I get more information about this.

onConnectionState function is not called in my case.

jefflongo commented 2 weeks ago

Sure, in my app I connect as follows:

Future<void> connect(
    {Function? onDisconnect, Duration timeout = const Duration(seconds: 35)}) async {
  try {
    // perform the connection
    Stopwatch stopwatch = Stopwatch()..start();
    await _device.connect(timeout: timeout, mtu: 517);
    stopwatch.stop();

    // listen for mtu changes
    final mtuSubscription = _device.mtu.listen((mtu) => _mtu = mtu);
    _device.cancelWhenDisconnected(mtuSubscription);

    // discover services
    _services = await _device.discoverServices(timeout: (timeout - stopwatch.elapsed).inSeconds);
  } catch (e) {
    await _device.disconnect(queue: false).catchError((e) {});
    log.w(e);
    throw Exception('FBP connect failed: $e');
  }

  // listen for disconnect
  _disconnectSubscription = _device.connectionState.listen((state) {
    if (state == fbp.BluetoothConnectionState.disconnected) {
      onDisconnect?.call();
    }
  });
  _device.cancelWhenDisconnected(_disconnectSubscription!, next: true, delayed: true);
}

After connecting, I test several characteristic writes. If they fail, I call this disconnect function:

Future<void> disconnect({bool withCallback = true}) async {
  try {
    if (!withCallback) {
      await _disconnectSubscription?.cancel();
    }
    await _device.disconnect();
  } catch (e) {
    log.w(e);
  }
}

The point here is to cancel the connectionState subscription before calling _device.disconnect() to prevent the connectionState callback from being called. Specifically, awaiting _disconnectSubscription?.cancel() never completes the future. I think this could be related to the stream being a broadcast stream and the stream controller not handling cancellation requests because the stream never closes.

chan150 commented 2 weeks ago

It may be related with this post: https://github.com/rohitsangwan01/win_ble/issues/44

I'll take a deeper look into the issues.

jefflongo commented 1 week ago

Could be related to this code? https://github.com/chan150/flutter_blue_plus_windows/blob/master/lib/src/windows/bluetooth_device_windows.dart#L181-L183

A StreamSubscription cancellation future will not complete if a stream generator doesn't yield or complete.