diff --git a/analysis_options.yaml b/analysis_options.yaml index c23adc72..afc8ba0c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -27,7 +27,6 @@ analyzer: exclude: - lib/src/generated/**.dart - - example/**.dart linter: rules: diff --git a/example/raw_data.dart b/example/raw_data.dart new file mode 100644 index 00000000..65cd2b77 --- /dev/null +++ b/example/raw_data.dart @@ -0,0 +1,19 @@ +import "dart:io"; +import "package:burt_network/burt_network.dart"; + +class TestSocket extends UdpSocket { + TestSocket({required super.port}); + + @override + void onData(Datagram packet) => logger.info("Received data: ${packet.data} from ${packet.port}"); +} + +void main() async { + final socket1 = TestSocket(port: 8001); + final socket2 = TestSocket(port: 8002); + + await socket1.init(); + await socket2.init(); + + socket1.sendData([1, 2, 3], SocketInfo(address: InternetAddress.loopbackIPv4, port: 8002)); +} diff --git a/example/server.dart b/example/server.dart new file mode 100644 index 00000000..bcd99e61 --- /dev/null +++ b/example/server.dart @@ -0,0 +1,15 @@ +import "package:burt_network/burt_network.dart"; + +class BasicServer extends ServerSocket { + BasicServer({required super.port, required super.device}); + + @override + void onMessage(WrappedMessage wrapper) => logger.info("Received ${wrapper.name} message: ${wrapper.data}"); +} + +void main() async { + final server = BasicServer(port: 8001, device: Device.SUBSYSTEMS); // Registers as the Subsystems Server on the Dashboard + final server2 = BasicServer(port: 8002, device: Device.VIDEO); // Registers as the Subsystems Server on the Dashboard + await server.init(); + await server2.init(); +} diff --git a/lib/burt_network.dart b/lib/burt_network.dart index 4da8ff45..66e8701d 100644 --- a/lib/burt_network.dart +++ b/lib/burt_network.dart @@ -15,14 +15,14 @@ /// not have to use [UdpSocket] directly as it has no Protobuf support. library; -export "src/server_socket.dart"; +import "src/proto_socket.dart"; +import "src/server_socket.dart"; +import "src/udp_socket.dart"; + export "src/log.dart"; export "src/proto_socket.dart"; +export "src/server_socket.dart"; export "src/socket_info.dart"; export "src/udp_socket.dart"; export "generated.dart"; - -import "src/server_socket.dart"; -import "src/proto_socket.dart"; -import "src/udp_socket.dart"; diff --git a/lib/src/log.dart b/lib/src/log.dart index aec896b5..2967f123 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -1,10 +1,27 @@ import "dart:io"; import "package:logger/logger.dart"; -/// A filter to decide which messages get logged. Set [LogFilter.level] to change. -final logFilter = ProductionFilter(); -/// The logger to use when running BURT programs. See [LoggerUtils] for usage. -Logger logger = Logger(printer: SimplePrinter(colors: stdout.supportsAnsiEscapes), filter: logFilter); +/// An alias for [Level]. +typedef LogLevel = Level; + +/// Holds the current [LogLevel] for [logger]. +class BurtLogger { + /// The current [LogLevel] for [logger]. + static LogLevel level = LogLevel.info; +} + +/// A custom filter to work around a bug with `package:logger`. +/// +/// See https://github.com/Bungeefan/logger/issues/38. +class BurtFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) => event.level.index >= BurtLogger.level.index; +} + +/// The logger to use when running BURT programs. +/// +/// See [LoggerUtils] for usage. To change the minimum log level, use [BurtLogger.level]. +final logger = Logger(printer: SimplePrinter(colors: stdout.supportsAnsiEscapes), filter: BurtFilter()); /// Helpful aliases for the [Logger] class. extension LoggerUtils on Logger { diff --git a/lib/src/proto_socket.dart b/lib/src/proto_socket.dart index 23c5b21a..17d019f5 100644 --- a/lib/src/proto_socket.dart +++ b/lib/src/proto_socket.dart @@ -6,7 +6,6 @@ import "package:burt_network/generated.dart"; import "udp_socket.dart"; import "socket_info.dart"; -import "log.dart"; /// A [UdpSocket] to send and receive Protobuf messages. /// @@ -87,13 +86,21 @@ abstract class ProtoSocket extends UdpSocket { } /// Wraps a [Message] and sends it to the [destination], or the given [socketOverride] if specified. + /// + /// If you have already wrapped a message yourself, use [sendWrapper]. void sendMessage(Message message, {SocketInfo? socketOverride}) { final wrapper = message.wrap(); final target = socketOverride ?? destination; - if (target == null) { - logger.critical("No destination or override was specificed"); - throw ArgumentError.notNull("socketOverride"); - } + if (target == null) return; + sendData(wrapper.writeToBuffer(), target); + } + + /// Sends an already-wrapped [WrappedMessage] to the [destination], or the given [socketOverride]. + /// + /// Use this function instead of [sendMessage] if you need to manually wrap a message yourself. + void sendWrapper(WrappedMessage wrapper, {SocketInfo? socketOverride}) { + final target = socketOverride ?? destination; + if (target == null) return; sendData(wrapper.writeToBuffer(), target); } diff --git a/lib/src/server_socket.dart b/lib/src/server_socket.dart index 15f80f78..bfa91604 100644 --- a/lib/src/server_socket.dart +++ b/lib/src/server_socket.dart @@ -72,7 +72,6 @@ abstract class ServerSocket extends ProtoSocket { /// 4. If we are not connected to any dashboard, call [onConnect] and respond to it. @override void onHeartbeat(Connect heartbeat, SocketInfo source) { - logger.debug("Received heartbeat from $source"); if (heartbeat.receiver != device) { // (1) logger.warning("Received a misaddressed heartbeat for ${heartbeat.receiver}"); } else if (isConnected) { diff --git a/lib/src/udp_socket.dart b/lib/src/udp_socket.dart index 4bcba4c8..1a3bce26 100644 --- a/lib/src/udp_socket.dart +++ b/lib/src/udp_socket.dart @@ -17,8 +17,11 @@ import "log.dart"; /// - Override [onData] to handle incoming data. /// - Call [dispose] to close the socket. Messages can no longer be sent or received after this. abstract class UdpSocket { + /// A collection of allowed [OSError] codes. + static const allowedErrors = {1234, 10054, 101, 10038, 9}; + /// The port this socket is listening on. See [RawDatagramSocket.bind]. - final int port; + int? port; /// Opens a UDP socket on the given port that can send and receive data. UdpSocket({required this.port}); @@ -33,20 +36,35 @@ abstract class UdpSocket { /// This must be cancelled in [dispose]. late StreamSubscription _subscription; - /// Initializes the socket. + /// Initializes the socket, and restarts it if a known "safe" error occurs (see [allowedErrors]). @mustCallSuper - Future init() async { - logger.verbose("Listening on port $port"); - _socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port); - _subscription = _socket.listenForData(onData); - } + Future init() async => runZonedGuarded>( + // This code cannot be a try/catch because the SocketException can be thrown at any time, + // even after this function has finished. It also cannot be caught by the caller of this function. + // Using [runZonedGuarded] ensures that the error is caught no matter when it is thrown. + () async { // Initialize the socket + _socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port ?? 0); + _subscription = _socket.listenForData(onData); + if (port == null || port == 0) port = _socket.port; + logger.info("Listening on port $port"); + }, + (Object error, StackTrace stack) async { // Catch errors and restart the socket + if (error is SocketException && allowedErrors.contains(error.osError!.errorCode)) { + logger.warning("Socket error ${error.osError!.errorCode} on port $port. Restarting..."); + await dispose(); + await init(); + } else { + Error.throwWithStackTrace(error, stack); + } + } + ); /// Closes the socket. @mustCallSuper Future dispose() async { - logger.verbose("Closed the socket on port $port"); await _subscription.cancel(); _socket.close(); + logger.info("Closed the socket on port $port"); } /// Sends data to the given destination. diff --git a/test/proto_test.dart b/test/proto_test.dart index 34e1e600..46361a5d 100644 --- a/test/proto_test.dart +++ b/test/proto_test.dart @@ -1,6 +1,5 @@ import "dart:io"; import "package:burt_network/burt_network.dart"; -import "package:burt_network/generated.dart"; import "package:test/test.dart"; final address = InternetAddress.loopbackIPv4; @@ -9,6 +8,7 @@ final serverInfo = SocketInfo(address: address, port: 8000); final clientInfo = SocketInfo(address: address, port: 8001); void main() => group("ProtoSocket:", () { + BurtLogger.level = LogLevel.debug; final server = TestServer(port: serverInfo.port, device: Device.SUBSYSTEMS); final client = TestClient( device: Device.DASHBOARD, @@ -77,7 +77,7 @@ class TestServer extends ServerSocket { } class TestClient extends ProtoSocket { - TestClient({required super.port, required super.device, super.destination}); + TestClient({required super.port, required super.device, super.destination}) : super(heartbeatInterval: const Duration(seconds: 1)); bool isConnected = false; bool shouldSendHeartbeats = true;