LCOV - code coverage report
Current view: top level - lib/src/services/impls - host_scanner_service_impl.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 78.9 % 128 101
Test Date: 2026-07-04 10:50:15 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:isolate';
       3              : import 'dart:math';
       4              : 
       5              : import 'package:dart_ping/dart_ping.dart';
       6              : import 'package:network_tools/network_tools.dart';
       7              : import 'package:network_tools/src/injection.dart';
       8              : import 'package:network_tools/src/network_tools_utils.dart';
       9              : import 'package:network_tools/src/repository/repository.dart';
      10              : import 'package:universal_io/io.dart';
      11              : 
      12              : /// Scans for all hosts in a subnet.
      13              : class HostScannerServiceImpl extends HostScannerService {
      14              :   /// Scans for all hosts in a particular subnet (e.g., 192.168.1.0/24)
      15              :   /// Set maxHost to higher value if you are not getting results.
      16              :   /// It won't firstHostId again unless previous scan is completed due to heavy
      17              :   /// resource consumption.
      18              :   /// Use hostIds to limit subnet scan to hosts given.
      19              :   /// [resultsInAddressAscendingOrder] = false will return results faster but not in
      20              :   /// ascending order and without [progressCallback].
      21            1 :   @override
      22              :   Stream<ActiveHost> getAllPingableDevices(
      23              :     String subnet, {
      24              :     int firstHostId = HostScannerService.defaultFirstHostId,
      25              :     int lastHostId = HostScannerService.defaultLastHostId,
      26              :     List<int> hostIds = const [],
      27              :     int timeoutInSeconds = 1,
      28              :     ProgressCallback? progressCallback,
      29              :     bool resultsInAddressAscendingOrder = true,
      30              :   }) async* {
      31            1 :     final stream = getAllSendablePingableDevices(
      32              :       subnet,
      33              :       firstHostId: firstHostId,
      34              :       lastHostId: lastHostId,
      35              :       hostIds: hostIds,
      36              :       timeoutInSeconds: timeoutInSeconds,
      37              :       progressCallback: progressCallback,
      38              :       resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
      39              :     );
      40            2 :     await for (final sendableActiveHost in stream) {
      41            1 :       final activeHost = ActiveHost.fromSendableActiveHost(
      42              :         sendableActiveHost: sendableActiveHost,
      43              :       );
      44              : 
      45            1 :       await activeHost.resolveInfo();
      46              : 
      47              :       yield activeHost;
      48              :     }
      49              :   }
      50              : 
      51              :   /// Same as [getAllPingableDevices] but can be called or run inside isolate.
      52            1 :   @override
      53              :   Stream<SendableActiveHost> getAllSendablePingableDevices(
      54              :     String subnet, {
      55              :     int firstHostId = HostScannerService.defaultFirstHostId,
      56              :     int lastHostId = HostScannerService.defaultLastHostId,
      57              :     List<int> hostIds = const [],
      58              :     int timeoutInSeconds = 1,
      59              :     ProgressCallback? progressCallback,
      60              :     bool resultsInAddressAscendingOrder = true,
      61              :   }) async* {
      62            1 :     final int lastValidSubnet = validateAndGetLastValidSubnet(
      63              :       subnet,
      64              :       firstHostId,
      65              :       lastHostId,
      66              :     );
      67            1 :     final List<Future<SendableActiveHost?>> activeHostsFuture = [];
      68              :     final StreamController<SendableActiveHost> activeHostsController =
      69            1 :         StreamController<SendableActiveHost>();
      70              : 
      71            1 :     final List<int> pinged = [];
      72            2 :     for (int i = firstHostId; i <= lastValidSubnet; i++) {
      73            2 :       if (hostIds.isEmpty || hostIds.contains(i)) {
      74            1 :         pinged.add(i);
      75            1 :         activeHostsFuture.add(
      76            1 :           getHostFromPing(
      77              :             activeHostsController: activeHostsController,
      78            1 :             host: '$subnet.$i',
      79              :             timeoutInSeconds: timeoutInSeconds,
      80              :           ),
      81              :         );
      82              :       }
      83              :     }
      84              : 
      85              :     if (!resultsInAddressAscendingOrder) {
      86            0 :       yield* activeHostsController.stream;
      87              :     }
      88              : 
      89              :     int i = 0;
      90            2 :     for (final Future<SendableActiveHost?> host in activeHostsFuture) {
      91              :       final SendableActiveHost? tempHost = await host;
      92              : 
      93            0 :       progressCallback?.call(
      94            0 :         (pinged[i] - firstHostId) * 100 / (lastValidSubnet - firstHostId),
      95              :       );
      96            1 :       i++;
      97              : 
      98              :       if (tempHost == null) {
      99              :         continue;
     100              :       }
     101              :       yield tempHost;
     102              :     }
     103              :   }
     104              : 
     105            1 :   static PingResponse? _extractPingResponse(Object? event) {
     106            1 :     if (event is PingResponse) {
     107              :       return event;
     108              :     }
     109              :     if (event == null) {
     110              :       return null;
     111              :     }
     112              : 
     113              :     // dart_ping changed its stream payload type across versions.
     114              :     // Some versions emit PingSummary or other payloads instead of a response.
     115              :     try {
     116              :       final dynamic dynamicEvent = event;
     117              :       // ignore: avoid_dynamic_calls
     118            1 :       final Object? response = dynamicEvent.response;
     119            0 :       return response is PingResponse ? response : null;
     120              :     } catch (error) {
     121              :       return null;
     122              :     }
     123              :   }
     124              : 
     125            1 :   Future<SendableActiveHost?> getHostFromPing({
     126              :     required String host,
     127              :     required StreamController<SendableActiveHost> activeHostsController,
     128              :     int timeoutInSeconds = 1,
     129              :   }) async {
     130              :     SendableActiveHost? tempSendableActivateHost;
     131              : 
     132            2 :     await for (final Object? event in Ping(
     133              :       host,
     134              :       count: 1,
     135              :       timeout: timeoutInSeconds,
     136            1 :       forceCodepage: Platform.isWindows,
     137            2 :     ).stream) {
     138            1 :       final PingResponse? pingResponse = _extractPingResponse(event);
     139            1 :       if (pingResponse != null && pingResponse.time != null) {
     140              :         // Check if ping succeeded
     141            3 :         logger.fine("Pingable device found: $host");
     142            1 :         tempSendableActivateHost = SendableActiveHost(
     143              :           host,
     144              :           pingData: pingResponse,
     145              :         );
     146              :       } else {
     147            3 :         logger.fine("Non pingable device found: $host");
     148              :       }
     149              : 
     150              :       if (tempSendableActivateHost == null) {
     151              :         // Check if it's there in arp table
     152              : 
     153            3 :         final data = await getIt<Repository<ARPData>>().entryFor(host);
     154              : 
     155              :         if (data != null) {
     156            0 :           logger.fine("Successfully fetched arp entry for $host as $data");
     157            0 :           tempSendableActivateHost = SendableActiveHost(
     158              :             host,
     159              :             pingData: pingResponse,
     160              :           );
     161              :         } else {
     162            3 :           logger.fine("Problem in fetching arp entry for $host");
     163              :         }
     164              :       }
     165              : 
     166              :       if (tempSendableActivateHost != null) {
     167            3 :         logger.fine("Successfully added to result $host");
     168            1 :         activeHostsController.add(tempSendableActivateHost);
     169              :       }
     170              :     }
     171              : 
     172              :     return tempSendableActivateHost;
     173              :   }
     174              : 
     175            1 :   @override
     176              :   int validateAndGetLastValidSubnet(
     177              :     String subnet,
     178              :     int firstHostId,
     179              :     int lastHostId,
     180              :   ) {
     181            1 :     final int maxEnd = maxHost;
     182            1 :     if (firstHostId > lastHostId ||
     183            1 :         firstHostId < HostScannerService.defaultFirstHostId ||
     184            1 :         lastHostId < HostScannerService.defaultFirstHostId ||
     185            1 :         firstHostId > maxEnd ||
     186            1 :         lastHostId > maxEnd) {
     187              :       throw 'Invalid subnet range or firstHostId < lastHostId is not true';
     188              :     }
     189            1 :     return min(lastHostId, maxEnd);
     190              :   }
     191              : 
     192              :   /// Works same as [getAllPingableDevices] but does everything inside
     193              :   /// isolate out of the box.
     194            1 :   @override
     195              :   Stream<ActiveHost> getAllPingableDevicesAsync(
     196              :     String subnet, {
     197              :     int firstHostId = HostScannerService.defaultFirstHostId,
     198              :     int lastHostId = HostScannerService.defaultLastHostId,
     199              :     List<int> hostIds = const [],
     200              :     int timeoutInSeconds = 1,
     201              :     ProgressCallback? progressCallback,
     202              :     bool resultsInAddressAscendingOrder = true,
     203              :   }) async* {
     204              :     const int scanRangeForIsolate = 51;
     205            1 :     final int lastValidSubnet = validateAndGetLastValidSubnet(
     206              :       subnet,
     207              :       firstHostId,
     208              :       lastHostId,
     209              :     );
     210              :     for (
     211              :       int i = firstHostId;
     212            1 :       i <= lastValidSubnet;
     213            2 :       i += scanRangeForIsolate + 1
     214              :     ) {
     215            2 :       final limit = min(i + scanRangeForIsolate, lastValidSubnet);
     216            3 :       logger.fine('Scanning from $i to $limit');
     217              : 
     218            1 :       final receivePort = ReceivePort();
     219            1 :       final isolate = await Isolate.spawn(
     220              :         _startSearchingDevices,
     221            1 :         receivePort.sendPort,
     222              :       );
     223              : 
     224            2 :       await for (final message in receivePort) {
     225            1 :         if (message is SendPort) {
     226            2 :           message.send(<String>[
     227              :             subnet,
     228            1 :             i.toString(),
     229            1 :             limit.toString(),
     230            1 :             timeoutInSeconds.toString(),
     231            1 :             resultsInAddressAscendingOrder.toString(),
     232            1 :             dbDirectory,
     233            2 :             enableDebugging.toString(),
     234            1 :             hostIds.join(','),
     235              :           ]);
     236            1 :         } else if (message is SendableActiveHost) {
     237            1 :           final activeHostFound = ActiveHost.fromSendableActiveHost(
     238              :             sendableActiveHost: message,
     239              :           );
     240            1 :           await activeHostFound.resolveInfo();
     241            2 :           final j = int.tryParse(activeHostFound.hostId) ?? i;
     242            0 :           progressCallback?.call(
     243            0 :             (j - firstHostId) * 100 / (lastValidSubnet - firstHostId),
     244              :           );
     245              :           yield activeHostFound;
     246            2 :         } else if (message is String && message == 'Done') {
     247            1 :           isolate.kill();
     248              :           break;
     249              :         }
     250              :       }
     251              :     }
     252              :   }
     253              : 
     254              :   /// Will search devices in the network inside new isolate
     255            1 :   @pragma('vm:entry-point')
     256              :   static Future<void> _startSearchingDevices(SendPort sendPort) async {
     257            1 :     final port = ReceivePort();
     258            2 :     sendPort.send(port.sendPort);
     259              : 
     260            2 :     await for (final message in port) {
     261            1 :       if (message is List<String>) {
     262            1 :         final String subnetIsolate = message[0];
     263            2 :         final int firstSubnetIsolate = int.parse(message[1]);
     264            2 :         final int lastSubnetIsolate = int.parse(message[2]);
     265            2 :         final int timeoutInSeconds = int.parse(message[3]);
     266            2 :         final bool resultsInAddressAscendingOrder = message[4] == "true";
     267            1 :         final String dbDirectory = message[5];
     268            2 :         final bool enableDebugging = message[6] == "true";
     269            1 :         final String joinedIds = message[7];
     270              :         final List<int> hostIds = joinedIds
     271            1 :             .split(',')
     272            3 :             .where((e) => e.isNotEmpty)
     273            1 :             .map(int.parse)
     274            1 :             .toList();
     275              :         // configure again
     276            1 :         await configureNetworkTools(
     277              :           dbDirectory,
     278              :           enableDebugging: enableDebugging,
     279              :         );
     280              : 
     281              :         /// Will contain all the hosts that got discovered in the network, will
     282              :         /// be use inorder to cancel on dispose of the page.
     283              :         final Stream<SendableActiveHost> hostsDiscoveredInNetwork =
     284            2 :             HostScannerService.instance.getAllSendablePingableDevices(
     285              :               subnetIsolate,
     286              :               firstHostId: firstSubnetIsolate,
     287              :               lastHostId: lastSubnetIsolate,
     288              :               hostIds: hostIds,
     289              :               timeoutInSeconds: timeoutInSeconds,
     290              :               resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
     291              :             );
     292              : 
     293            1 :         await for (final SendableActiveHost activeHostFound
     294            1 :             in hostsDiscoveredInNetwork) {
     295            1 :           sendPort.send(activeHostFound);
     296              :         }
     297            1 :         sendPort.send('Done');
     298              :       }
     299              :     }
     300              :   }
     301              : 
     302              :   /// Scans for all hosts that have the specific port that was given.
     303              :   /// [resultsInAddressAscendingOrder] = false will return results faster but not in
     304              :   /// ascending order and without [progressCallback].
     305            1 :   @override
     306              :   Stream<ActiveHost> scanDevicesForSinglePort(
     307              :     String subnet,
     308              :     int port, {
     309              :     int firstHostId = HostScannerService.defaultFirstHostId,
     310              :     int lastHostId = HostScannerService.defaultLastHostId,
     311              :     Duration timeout = const Duration(milliseconds: 2000),
     312              :     ProgressCallback? progressCallback,
     313              :     bool resultsInAddressAscendingOrder = true,
     314              :   }) async* {
     315            1 :     final int lastValidSubnet = validateAndGetLastValidSubnet(
     316              :       subnet,
     317              :       firstHostId,
     318              :       lastHostId,
     319              :     );
     320            1 :     final List<Future<ActiveHost?>> activeHostOpenPortList = [];
     321              :     final StreamController<ActiveHost> activeHostsController =
     322            1 :         StreamController<ActiveHost>();
     323              : 
     324            2 :     for (int i = firstHostId; i <= lastValidSubnet; i++) {
     325            1 :       final host = '$subnet.$i';
     326            1 :       activeHostOpenPortList.add(
     327            2 :         PortScannerService.instance.connectToPort(
     328              :           address: host,
     329              :           port: port,
     330              :           timeout: timeout,
     331              :           activeHostsController: activeHostsController,
     332              :         ),
     333              :       );
     334              :     }
     335              : 
     336              :     if (!resultsInAddressAscendingOrder) {
     337            0 :       yield* activeHostsController.stream;
     338              :     }
     339              : 
     340              :     int counter = firstHostId;
     341              :     for (final Future<ActiveHost?> openPortActiveHostFuture
     342            2 :         in activeHostOpenPortList) {
     343              :       final ActiveHost? activeHost = await openPortActiveHostFuture;
     344              :       if (activeHost != null) {
     345              :         yield activeHost;
     346              :       }
     347            0 :       progressCallback?.call(
     348            0 :         (counter - firstHostId) * 100 / (lastValidSubnet - firstHostId),
     349              :       );
     350            1 :       counter++;
     351              :     }
     352              :   }
     353              : 
     354              :   /// Defines total number of subnets in class A network
     355              :   final classASubnets = 16777216;
     356              : 
     357              :   /// Defines total number of subnets in class B network
     358              :   final classBSubnets = 65536;
     359              : 
     360              :   /// Defines total number of subnets in class C network
     361              :   final classCSubnets = 256;
     362              : 
     363              :   /// Minimum value of first octet in IPv4 address used by getMaxHost
     364              :   final int minNetworkId = 1;
     365              : 
     366              :   /// Maximum value of first octect in IPv4 address used by getMaxHost
     367              :   final int maxNetworkId = 223;
     368              : 
     369              :   /// returns the max number of hosts a subnet can have excluding network Id and broadcast Id
     370            0 :   @Deprecated(
     371              :     "Implementation is wrong, since we only append in last octet, max host can only be 254. Use maxHost getter",
     372              :   )
     373              :   int getMaxHost(String subnet) {
     374            0 :     if (subnet.isEmpty) {
     375            0 :       throw ArgumentError('Invalid subnet address, address can not be empty.');
     376              :     }
     377            0 :     final List<String> firstOctetStr = subnet.split('.');
     378            0 :     if (firstOctetStr.isEmpty) {
     379            0 :       throw ArgumentError(
     380              :         'Invalid subnet address, address should be in IPv4 format x.x.x',
     381              :       );
     382              :     }
     383              : 
     384            0 :     final int firstOctet = int.parse(firstOctetStr[0]);
     385              : 
     386            0 :     if (firstOctet >= minNetworkId && firstOctet < 128) {
     387            0 :       return classASubnets;
     388            0 :     } else if (firstOctet >= 128 && firstOctet < 192) {
     389            0 :       return classBSubnets;
     390            0 :     } else if (firstOctet >= 192 && firstOctet <= maxNetworkId) {
     391            0 :       return classCSubnets;
     392              :     }
     393              :     // Out of range for first octet
     394            0 :     throw RangeError.range(
     395              :       firstOctet,
     396            0 :       minNetworkId,
     397            0 :       maxNetworkId,
     398              :       'subnet',
     399              :       'Out of range for first octet',
     400              :     );
     401              :   }
     402              : 
     403            1 :   int get maxHost => HostScannerService.defaultLastHostId;
     404              : }
        

Generated by: LCOV version 2.0-1