LCOV - code coverage report
Current view: top level - lib/src/services/impls - host_scanner_service_impl.dart (source / functions) Coverage Total Hit
Test: coverage.lcov Lines: 78.4 % 125 98
Test Date: 2025-08-17 13:02:53 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 :   Future<SendableActiveHost?> getHostFromPing({
     106              :     required String host,
     107              :     required StreamController<SendableActiveHost> activeHostsController,
     108              :     int timeoutInSeconds = 1,
     109              :   }) async {
     110              :     SendableActiveHost? tempSendableActivateHost;
     111              : 
     112            2 :     await for (final PingData pingData in Ping(
     113              :       host,
     114              :       count: 1,
     115              :       timeout: timeoutInSeconds,
     116            1 :       forceCodepage: Platform.isWindows,
     117            2 :     ).stream) {
     118            1 :       final PingResponse? response = pingData.response;
     119            1 :       final PingError? pingError = pingData.error;
     120              :       if (response != null) {
     121              :         // Check if ping succeeded
     122              :         if (pingError == null) {
     123            1 :           final Duration? time = response.time;
     124              :           if (time != null) {
     125            3 :             logger.fine("Pingable device found: $host");
     126            1 :             tempSendableActivateHost = SendableActiveHost(
     127              :               host,
     128              :               pingData: pingData,
     129              :             );
     130              :           } else {
     131            0 :             logger.fine("Non pingable device found: $host");
     132              :           }
     133              :         }
     134              :       }
     135              :       if (tempSendableActivateHost == null) {
     136              :         // Check if it's there in arp table
     137              : 
     138            3 :         final data = await getIt<Repository<ARPData>>().entryFor(host);
     139              : 
     140              :         if (data != null) {
     141            0 :           logger.fine("Successfully fetched arp entry for $host as $data");
     142            0 :           tempSendableActivateHost = SendableActiveHost(
     143              :             host,
     144              :             pingData: pingData,
     145              :           );
     146              :         } else {
     147            3 :           logger.fine("Problem in fetching arp entry for $host");
     148              :         }
     149              :       }
     150              : 
     151              :       if (tempSendableActivateHost != null) {
     152            3 :         logger.fine("Successfully added to result $host");
     153            1 :         activeHostsController.add(tempSendableActivateHost);
     154              :       }
     155              :     }
     156              : 
     157              :     return tempSendableActivateHost;
     158              :   }
     159              : 
     160            1 :   @override
     161              :   int validateAndGetLastValidSubnet(
     162              :     String subnet,
     163              :     int firstHostId,
     164              :     int lastHostId,
     165              :   ) {
     166            1 :     final int maxEnd = maxHost;
     167            1 :     if (firstHostId > lastHostId ||
     168            1 :         firstHostId < HostScannerService.defaultFirstHostId ||
     169            1 :         lastHostId < HostScannerService.defaultFirstHostId ||
     170            1 :         firstHostId > maxEnd ||
     171            1 :         lastHostId > maxEnd) {
     172              :       throw 'Invalid subnet range or firstHostId < lastHostId is not true';
     173              :     }
     174            1 :     return min(lastHostId, maxEnd);
     175              :   }
     176              : 
     177              :   /// Works same as [getAllPingableDevices] but does everything inside
     178              :   /// isolate out of the box.
     179            1 :   @override
     180              :   Stream<ActiveHost> getAllPingableDevicesAsync(
     181              :     String subnet, {
     182              :     int firstHostId = HostScannerService.defaultFirstHostId,
     183              :     int lastHostId = HostScannerService.defaultLastHostId,
     184              :     List<int> hostIds = const [],
     185              :     int timeoutInSeconds = 1,
     186              :     ProgressCallback? progressCallback,
     187              :     bool resultsInAddressAscendingOrder = true,
     188              :   }) async* {
     189              :     const int scanRangeForIsolate = 51;
     190            1 :     final int lastValidSubnet = validateAndGetLastValidSubnet(
     191              :       subnet,
     192              :       firstHostId,
     193              :       lastHostId,
     194              :     );
     195              :     for (
     196              :       int i = firstHostId;
     197            1 :       i <= lastValidSubnet;
     198            2 :       i += scanRangeForIsolate + 1
     199              :     ) {
     200            2 :       final limit = min(i + scanRangeForIsolate, lastValidSubnet);
     201            3 :       logger.fine('Scanning from $i to $limit');
     202              : 
     203            1 :       final receivePort = ReceivePort();
     204            1 :       final isolate = await Isolate.spawn(
     205              :         _startSearchingDevices,
     206            1 :         receivePort.sendPort,
     207              :       );
     208              : 
     209            2 :       await for (final message in receivePort) {
     210            1 :         if (message is SendPort) {
     211            2 :           message.send(<String>[
     212              :             subnet,
     213            1 :             i.toString(),
     214            1 :             limit.toString(),
     215            1 :             timeoutInSeconds.toString(),
     216            1 :             resultsInAddressAscendingOrder.toString(),
     217            1 :             dbDirectory,
     218            2 :             enableDebugging.toString(),
     219            1 :             hostIds.join(','),
     220              :           ]);
     221            1 :         } else if (message is SendableActiveHost) {
     222            1 :           final activeHostFound = ActiveHost.fromSendableActiveHost(
     223              :             sendableActiveHost: message,
     224              :           );
     225            1 :           await activeHostFound.resolveInfo();
     226            2 :           final j = int.tryParse(activeHostFound.hostId) ?? i;
     227            0 :           progressCallback?.call(
     228            0 :             (j - firstHostId) * 100 / (lastValidSubnet - firstHostId),
     229              :           );
     230              :           yield activeHostFound;
     231            2 :         } else if (message is String && message == 'Done') {
     232            1 :           isolate.kill();
     233              :           break;
     234              :         }
     235              :       }
     236              :     }
     237              :   }
     238              : 
     239              :   /// Will search devices in the network inside new isolate
     240            1 :   @pragma('vm:entry-point')
     241              :   static Future<void> _startSearchingDevices(SendPort sendPort) async {
     242            1 :     final port = ReceivePort();
     243            2 :     sendPort.send(port.sendPort);
     244              : 
     245            2 :     await for (final message in port) {
     246            1 :       if (message is List<String>) {
     247            1 :         final String subnetIsolate = message[0];
     248            2 :         final int firstSubnetIsolate = int.parse(message[1]);
     249            2 :         final int lastSubnetIsolate = int.parse(message[2]);
     250            2 :         final int timeoutInSeconds = int.parse(message[3]);
     251            2 :         final bool resultsInAddressAscendingOrder = message[4] == "true";
     252            1 :         final String dbDirectory = message[5];
     253            2 :         final bool enableDebugging = message[6] == "true";
     254            1 :         final String joinedIds = message[7];
     255              :         final List<int> hostIds = joinedIds
     256            1 :             .split(',')
     257            3 :             .where((e) => e.isNotEmpty)
     258            1 :             .map(int.parse)
     259            1 :             .toList();
     260              :         // configure again
     261            1 :         await configureNetworkTools(
     262              :           dbDirectory,
     263              :           enableDebugging: enableDebugging,
     264              :         );
     265              : 
     266              :         /// Will contain all the hosts that got discovered in the network, will
     267              :         /// be use inorder to cancel on dispose of the page.
     268              :         final Stream<SendableActiveHost> hostsDiscoveredInNetwork =
     269            2 :             HostScannerService.instance.getAllSendablePingableDevices(
     270              :               subnetIsolate,
     271              :               firstHostId: firstSubnetIsolate,
     272              :               lastHostId: lastSubnetIsolate,
     273              :               hostIds: hostIds,
     274              :               timeoutInSeconds: timeoutInSeconds,
     275              :               resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
     276              :             );
     277              : 
     278            1 :         await for (final SendableActiveHost activeHostFound
     279            1 :             in hostsDiscoveredInNetwork) {
     280            1 :           sendPort.send(activeHostFound);
     281              :         }
     282            1 :         sendPort.send('Done');
     283              :       }
     284              :     }
     285              :   }
     286              : 
     287              :   /// Scans for all hosts that have the specific port that was given.
     288              :   /// [resultsInAddressAscendingOrder] = false will return results faster but not in
     289              :   /// ascending order and without [progressCallback].
     290            1 :   @override
     291              :   Stream<ActiveHost> scanDevicesForSinglePort(
     292              :     String subnet,
     293              :     int port, {
     294              :     int firstHostId = HostScannerService.defaultFirstHostId,
     295              :     int lastHostId = HostScannerService.defaultLastHostId,
     296              :     Duration timeout = const Duration(milliseconds: 2000),
     297              :     ProgressCallback? progressCallback,
     298              :     bool resultsInAddressAscendingOrder = true,
     299              :   }) async* {
     300            1 :     final int lastValidSubnet = validateAndGetLastValidSubnet(
     301              :       subnet,
     302              :       firstHostId,
     303              :       lastHostId,
     304              :     );
     305            1 :     final List<Future<ActiveHost?>> activeHostOpenPortList = [];
     306              :     final StreamController<ActiveHost> activeHostsController =
     307            1 :         StreamController<ActiveHost>();
     308              : 
     309            2 :     for (int i = firstHostId; i <= lastValidSubnet; i++) {
     310            1 :       final host = '$subnet.$i';
     311            1 :       activeHostOpenPortList.add(
     312            2 :         PortScannerService.instance.connectToPort(
     313              :           address: host,
     314              :           port: port,
     315              :           timeout: timeout,
     316              :           activeHostsController: activeHostsController,
     317              :         ),
     318              :       );
     319              :     }
     320              : 
     321              :     if (!resultsInAddressAscendingOrder) {
     322            0 :       yield* activeHostsController.stream;
     323              :     }
     324              : 
     325              :     int counter = firstHostId;
     326              :     for (final Future<ActiveHost?> openPortActiveHostFuture
     327            2 :         in activeHostOpenPortList) {
     328              :       final ActiveHost? activeHost = await openPortActiveHostFuture;
     329              :       if (activeHost != null) {
     330              :         yield activeHost;
     331              :       }
     332            0 :       progressCallback?.call(
     333            0 :         (counter - firstHostId) * 100 / (lastValidSubnet - firstHostId),
     334              :       );
     335            1 :       counter++;
     336              :     }
     337              :   }
     338              : 
     339              :   /// Defines total number of subnets in class A network
     340              :   final classASubnets = 16777216;
     341              : 
     342              :   /// Defines total number of subnets in class B network
     343              :   final classBSubnets = 65536;
     344              : 
     345              :   /// Defines total number of subnets in class C network
     346              :   final classCSubnets = 256;
     347              : 
     348              :   /// Minimum value of first octet in IPv4 address used by getMaxHost
     349              :   final int minNetworkId = 1;
     350              : 
     351              :   /// Maximum value of first octect in IPv4 address used by getMaxHost
     352              :   final int maxNetworkId = 223;
     353              : 
     354              :   /// returns the max number of hosts a subnet can have excluding network Id and broadcast Id
     355            0 :   @Deprecated(
     356              :     "Implementation is wrong, since we only append in last octet, max host can only be 254. Use maxHost getter",
     357              :   )
     358              :   int getMaxHost(String subnet) {
     359            0 :     if (subnet.isEmpty) {
     360            0 :       throw ArgumentError('Invalid subnet address, address can not be empty.');
     361              :     }
     362            0 :     final List<String> firstOctetStr = subnet.split('.');
     363            0 :     if (firstOctetStr.isEmpty) {
     364            0 :       throw ArgumentError(
     365              :         'Invalid subnet address, address should be in IPv4 format x.x.x',
     366              :       );
     367              :     }
     368              : 
     369            0 :     final int firstOctet = int.parse(firstOctetStr[0]);
     370              : 
     371            0 :     if (firstOctet >= minNetworkId && firstOctet < 128) {
     372            0 :       return classASubnets;
     373            0 :     } else if (firstOctet >= 128 && firstOctet < 192) {
     374            0 :       return classBSubnets;
     375            0 :     } else if (firstOctet >= 192 && firstOctet <= maxNetworkId) {
     376            0 :       return classCSubnets;
     377              :     }
     378              :     // Out of range for first octet
     379            0 :     throw RangeError.range(
     380              :       firstOctet,
     381            0 :       minNetworkId,
     382            0 :       maxNetworkId,
     383              :       'subnet',
     384              :       'Out of range for first octet',
     385              :     );
     386              :   }
     387              : 
     388            1 :   int get maxHost => HostScannerService.defaultLastHostId;
     389              : }
        

Generated by: LCOV version 2.0-1