LCOV - code coverage report
Current view: top level - manager - monitor.dart (source / functions) Hit Total Coverage
Test: coverage.lcov Lines: 1 120 0.8 %
Date: 2022-01-19 17:54:05 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:collection';
       3             : import 'dart:convert';
       4             : import 'dart:io';
       5             : import 'dart:typed_data';
       6             : 
       7             : import 'package:at_client/at_client.dart';
       8             : import 'package:at_client/src/preference/monitor_preference.dart';
       9             : import 'package:at_client/src/response/default_response_parser.dart';
      10             : import 'package:at_client/src/util/network_util.dart';
      11             : import 'package:at_commons/at_builders.dart';
      12             : import 'package:at_commons/at_commons.dart';
      13             : import 'package:at_lookup/at_lookup.dart';
      14             : import 'package:at_utils/at_logger.dart';
      15             : import 'package:crypton/crypton.dart';
      16             : 
      17             : ///
      18             : /// A [Monitor] object is used to receive notifications from the secondary server.
      19             : ///
      20             : class Monitor {
      21             :   // Regex on with what the monitor is started
      22             :   String? _regex;
      23             : 
      24             :   /// Capacity is represented in bytes.
      25             :   /// Throws [BufferOverFlowException] if data size exceeds 10MB.
      26             :   final _buffer = ByteBuffer(capacity: 10240000);
      27             : 
      28             :   // Time epoch milliseconds of the last notification received on this monitor
      29             :   int? _lastNotificationTime;
      30             : 
      31             :   final _monitorVerbResponseQueue = Queue();
      32             : 
      33             :   // Status on the monitor
      34             :   MonitorStatus status = MonitorStatus.notStarted;
      35             : 
      36             :   final _logger = AtSignLogger('Monitor');
      37             : 
      38             :   bool _keepAlive = false;
      39             : 
      40             :   late String _atSign;
      41             : 
      42             :   late Function _onError;
      43             : 
      44             :   late Function _onResponse;
      45             : 
      46             :   late Function _retryCallBack;
      47             : 
      48             :   late AtClientPreference _preference;
      49             : 
      50             :   OutboundConnection? _monitorConnection;
      51             : 
      52             :   RemoteSecondary? _remoteSecondary;
      53             : 
      54             :   final DefaultResponseParser _defaultResponseParser = DefaultResponseParser();
      55             : 
      56             :   ///
      57             :   /// Creates a [Monitor] object.
      58             :   ///
      59             :   /// [onResponse] function is called when a new batch of notifications are received from the server.
      60             :   /// This cannot be null.
      61             :   /// Example [onResponse] callback
      62             :   /// ```
      63             :   /// void onResponse(String notificationResponse) {
      64             :   /// // add your notification processing logic
      65             :   ///}
      66             :   ///```
      67             :   /// [onError] function is called when is some thing goes wrong with the processing.
      68             :   /// For example this could be:
      69             :   ///    - Unavailability of the network
      70             :   ///    - Exception while running the code
      71             :   /// This cannot be null.
      72             :   /// Example [onError] callback
      73             :   /// ```
      74             :   /// void onError(Monitor monitor, Exception e) {
      75             :   ///  // add your error handling logic
      76             :   /// }
      77             :   /// ```
      78             :   /// After calling [onError] monitor would stop sending any more notifications. If the error is recoverable
      79             :   /// and if [retry] is true the [Monitor] would continue and waits to recover from the error condition and not call [onError].
      80             :   ///
      81             :   /// For example if the app loses internet connection then [Monitor] would wait till the internet comes back and not call
      82             :   /// [onError]
      83             :   ///
      84             :   /// When the [regex] is passed only those notifications matching the [regex] will be notified
      85             :   /// When the [lastNotificationTime] is passed only those notifications AFTER the time value are notified.
      86             :   /// This is expressed as EPOCH time milliseconds.
      87             :   /// When [retry] is true
      88             :   ////
      89           0 :   Monitor(
      90             :       Function onResponse,
      91             :       Function onError,
      92             :       String atSign,
      93             :       AtClientPreference preference,
      94             :       MonitorPreference monitorPreference,
      95             :       Function retryCallBack) {
      96           0 :     _onResponse = onResponse;
      97           0 :     _onError = onError;
      98           0 :     _preference = preference;
      99           0 :     _atSign = atSign;
     100           0 :     _regex = monitorPreference.regex;
     101           0 :     _keepAlive = monitorPreference.keepAlive;
     102           0 :     _lastNotificationTime = monitorPreference.lastNotificationTime;
     103           0 :     _remoteSecondary ??= RemoteSecondary(atSign, preference);
     104           0 :     _retryCallBack = retryCallBack;
     105             :   }
     106             : 
     107             :   /// Starts the monitor by establishing a new TCP/IP connection with the secondary server
     108             :   /// If [lastNotificationTime] expressed as EPOCH milliseconds is passed, only those notifications occurred after
     109             :   /// that time are notified.
     110             :   /// Calling start on already started monitor would not cause any exceptions and it will have no side affects.
     111             :   /// Calling start on monitor that is not started or erred will be started again.
     112             :   /// Calling [Monitor#getStatus] would return the status of the [Monitor]
     113           0 :   Future<void> start({int? lastNotificationTime}) async {
     114           0 :     if (status == MonitorStatus.started) {
     115             :       // Monitor already started
     116           0 :       _logger.finer('Monitor is already running');
     117             :       return;
     118             :     }
     119             :     // This enables start method to be called with lastNotificationTime on the same instance of Monitor
     120             :     if (lastNotificationTime != null) {
     121           0 :       _logger.finer(
     122           0 :           'starting monitor for $_atSign with lastnotificationTime: $lastNotificationTime');
     123           0 :       _lastNotificationTime = lastNotificationTime;
     124             :     }
     125             :     try {
     126           0 :       await _checkConnectivity();
     127             :       //1. Get a new outbound connection dedicated to monitor verb.
     128           0 :       _monitorConnection = await _createNewConnection(
     129           0 :           _atSign, _preference.rootDomain, _preference.rootPort);
     130           0 :       _monitorConnection!.getSocket().listen(_messageHandler, onDone: () {
     131           0 :         _logger.finer('monitor done');
     132           0 :         _monitorConnection!.getSocket().destroy();
     133           0 :         status = MonitorStatus.stopped;
     134           0 :         _retryCallBack();
     135           0 :       }, onError: (error) {
     136           0 :         _logger.severe('error in monitor $error');
     137           0 :         _handleError(error);
     138             :       });
     139           0 :       await _authenticateConnection();
     140           0 :       await _monitorConnection!.write(_buildMonitorCommand());
     141           0 :       status = MonitorStatus.started;
     142           0 :       _logger.finer(
     143           0 :           'monitor started for $_atSign with last notification time: $_lastNotificationTime');
     144             : 
     145             :       return;
     146           0 :     } on Exception catch (e) {
     147           0 :       _handleError(e);
     148             :     }
     149             :   }
     150             : 
     151           0 :   Future<void> _authenticateConnection() async {
     152           0 :     await _monitorConnection!.write('from:$_atSign\n');
     153           0 :     var fromResponse = await _getQueueResponse();
     154           0 :     if (fromResponse.isEmpty) {
     155           0 :       throw UnAuthenticatedException('From response is empty');
     156             :     }
     157           0 :     _logger.finer(
     158           0 :         'Authenticating the monitor connection: from result:$fromResponse');
     159           0 :     var key = RSAPrivateKey.fromString(_preference.privateKey!);
     160             :     var sha256signature =
     161           0 :         key.createSHA256Signature(utf8.encode(fromResponse) as Uint8List);
     162           0 :     var signature = base64Encode(sha256signature);
     163           0 :     _logger.finer('Authenticating the monitor connection: pkam:$signature');
     164           0 :     await _monitorConnection!.write('pkam:$signature\n');
     165           0 :     var pkamResponse = await _getQueueResponse();
     166           0 :     if (!pkamResponse.contains('success')) {
     167           0 :       throw UnAuthenticatedException(
     168             :           'Monitor connection authentication failed');
     169             :     }
     170           0 :     _logger.finer('Monitor connection authentication successful');
     171             :   }
     172             : 
     173           0 :   Future<OutboundConnection> _createNewConnection(
     174             :       String toAtSign, String rootDomain, int rootPort) async {
     175             :     //1. find secondary url for atsign from lookup library
     176             :     var secondaryUrl =
     177           0 :         await AtLookupImpl.findSecondary(toAtSign, rootDomain, rootPort);
     178             :     if (secondaryUrl == null) {
     179           0 :       throw Exception('Secondary url not found');
     180             :     }
     181           0 :     var secondaryInfo = _getSecondaryInfo(secondaryUrl);
     182           0 :     var host = secondaryInfo[0];
     183           0 :     var port = secondaryInfo[1];
     184             : 
     185             :     //2. create a connection to secondary server
     186           0 :     var secureSocket = await SecureSocket.connect(host, int.parse(port));
     187             :     OutboundConnection _monitorConnection =
     188           0 :         OutboundConnectionImpl(secureSocket);
     189             :     return _monitorConnection;
     190             :   }
     191             : 
     192           0 :   List<String> _getSecondaryInfo(String url) {
     193           0 :     var result = <String>[];
     194           0 :     if (url.contains(':')) {
     195           0 :       var arr = url.split(':');
     196           0 :       result.add(arr[0]);
     197           0 :       result.add(arr[1]);
     198             :     }
     199             :     return result;
     200             :   }
     201             : 
     202             :   ///Returns the response of the monitor verb queue.
     203           0 :   Future<String> _getQueueResponse() async {
     204             :     dynamic monitorResponse;
     205             :     //waits for 30 seconds
     206           0 :     for (var i = 0; i < 6000; i++) {
     207           0 :       if (_monitorVerbResponseQueue.isNotEmpty) {
     208             :         // result from another secondary is either data or a @<atSign>@ denoting complete
     209             :         // of the handshake
     210           0 :         monitorResponse = _defaultResponseParser
     211           0 :             .parse(_monitorVerbResponseQueue.removeFirst());
     212             :         break;
     213             :       }
     214           0 :       await Future.delayed(Duration(milliseconds: 5));
     215             :     }
     216             :     // If monitor response contains error, return error
     217           0 :     if (monitorResponse.isError) {
     218           0 :       return '${monitorResponse.errorCode}: ${monitorResponse.errorDescription}';
     219             :     }
     220           0 :     return monitorResponse.response;
     221             :   }
     222             : 
     223           0 :   String _buildMonitorCommand() {
     224           0 :     var monitorVerbBuilder = MonitorVerbBuilder();
     225           0 :     if (_regex != null && _regex!.isNotEmpty) {
     226           0 :       monitorVerbBuilder.regex = _regex;
     227             :     }
     228           0 :     if (_lastNotificationTime != null) {
     229           0 :       monitorVerbBuilder.lastNotificationTime = _lastNotificationTime;
     230             :     }
     231           0 :     return monitorVerbBuilder.buildCommand();
     232             :   }
     233             : 
     234             :   /// Stops the monitor. Call [Monitor#start] to start it again.
     235           0 :   void stop() {
     236           0 :     status = MonitorStatus.stopped;
     237           0 :     if (_monitorConnection != null) {
     238           0 :       _monitorConnection!.close();
     239             :     }
     240             :   }
     241             : 
     242             : // Stops the monitor from receiving notification
     243           0 :   MonitorStatus getStatus() {
     244           0 :     return status;
     245             :   }
     246             : 
     247           0 :   void _handleResponse(String response, Function callback) {
     248           0 :     if (response.toString().startsWith('notification')) {
     249           0 :       callback(response);
     250             :     } else {
     251           0 :       _monitorVerbResponseQueue.add(response);
     252             :     }
     253             :   }
     254             : 
     255           0 :   void _handleError(e) {
     256           0 :     _monitorConnection?.close();
     257           0 :     status = MonitorStatus.errored;
     258             :     // Pass monitor and error
     259             :     // TBD : If retry = true should the onError needs to be called?
     260           0 :     if (_keepAlive) {
     261             :       // We will use a strategy here
     262           0 :       _logger.finer('Retrying start monitor due to error');
     263           0 :       _retryCallBack();
     264             :     } else {
     265           0 :       _onError(e);
     266             :     }
     267             :   }
     268             : 
     269           0 :   Future<void> _checkConnectivity() async {
     270           0 :     if (!(await NetworkUtil.isNetworkAvailable())) {
     271           0 :       throw AtConnectException('Internet connection unavailable to sync');
     272             :     }
     273           0 :     if (!(await _remoteSecondary!.isAvailable())) {
     274           0 :       throw AtConnectException('Secondary server is unavailable');
     275             :     }
     276             :     return;
     277             :   }
     278             : 
     279             :   /// Handles messages on the inbound client's connection and calls the verb executor
     280             :   /// Closes the inbound connection in case of any error.
     281             :   /// Throw a [BufferOverFlowException] if buffer is unable to hold incoming data
     282           0 :   Future<void> _messageHandler(data) async {
     283             :     String result;
     284           0 :     if (!_buffer.isOverFlow(data)) {
     285             :       // skip @ prompt. byte code for @ is 64
     286           0 :       if (data.length == 1 && data.first == 64) {
     287             :         return;
     288             :       }
     289             :       //ignore prompt(@ or @<atSign>@) after '\n'. byte code for \n is 10
     290           0 :       if (data.last == 64 && data.contains(10)) {
     291           0 :         data = data.sublist(0, data.lastIndexOf(10) + 1);
     292           0 :         _buffer.append(data);
     293           0 :       } else if (data.length > 1 && data.first == 64 && data.last == 64) {
     294             :         // pol responses do not end with '\n'. Add \n for buffer completion
     295           0 :         _buffer.append(data);
     296           0 :         _buffer.addByte(10);
     297             :       } else {
     298           0 :         _buffer.append(data);
     299             :       }
     300             :     } else {
     301           0 :       _buffer.clear();
     302           0 :       throw BufferOverFlowException('Buffer overflow on outbound connection');
     303             :     }
     304           0 :     if (_buffer.isEnd()) {
     305           0 :       result = utf8.decode(_buffer.getData());
     306           0 :       result = result.trim();
     307           0 :       _buffer.clear();
     308           0 :       _handleResponse(result, _onResponse);
     309             :     }
     310             :   }
     311             : }
     312             : 
     313           7 : enum MonitorStatus { notStarted, started, stopped, errored }

Generated by: LCOV version 1.13