// Run a heartbeat to let supabase know that the client is still active. import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:waylume_server/config/supabase_config.dart'; import 'package:waylume_server/services/rolling_codes_service.dart'; import 'package:waylume_server/core/utils.dart'; void initHeartbeat() { // Run this on a separate thread. Isolate.spawn((_) async { // Initialize rolling codes service in this isolate await RollingCodesService.initialize(); // To avoid server deadlock and wait for main registration await Future.delayed(Duration(seconds: 10 + Random().nextInt(5))); // The heartbeat isolate needs to have the operational seed // Since isolates don't share memory, we need to register again in this isolate // This will get the same operational seed since the server_id is the same try { await _registerInHeartbeatIsolate(); } catch (e) { print("Failed to register in heartbeat isolate: $e"); return; } while (true) { DateTime now = DateTime.now().toUtc(); try { // Wait until rolling codes service has operational seed if (!RollingCodesService.isRegistered) { print("Server not registered yet, skipping heartbeat"); await Future.delayed(Duration(seconds: 10)); // Check more frequently during startup continue; } // Generate operational rolling code for heartbeat authentication String authCode = RollingCodesService.generateOperationalCode(); // Get basic server status (only active connections) Map serverStatus = await _getServerStatus(); // Send heartbeat to server-manager endpoint String serverManagerUrl = 'https://lsdrctuvnwdrzrdyoqzu.supabase.co/functions/v1/server-manager'; Map heartbeatData = { 'server_id': fromEnivronment("SERVER_ID")!, 'timestamp': now.toIso8601String(), 'auth_code': authCode, 'status': serverStatus }; final response = await http.post( Uri.parse(serverManagerUrl), headers: { 'Content-Type': 'application/json', }, body: jsonEncode(heartbeatData), ); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); if (responseData['success']) { print("Heartbeat sent successfully at ${now.toIso8601String()}"); } else { print("Heartbeat failed: ${responseData['error']}"); } } else { print("Heartbeat request failed: ${response.statusCode}"); } } catch (e) { print("Error sending heartbeat: $e"); } // Wait 90 seconds (1.5 minutes) before sending the next heartbeat await Future.delayed(Duration(seconds: 90)); } }, null); } Future _registerInHeartbeatIsolate() async { // Generate registration rolling code String registrationAuth = RollingCodesService.generateRegistrationCode(); // Call server-manager registration endpoint String serverManagerUrl = 'https://lsdrctuvnwdrzrdyoqzu.supabase.co/functions/v1/server-manager'; Map requestBody = { 'server_id': fromEnivronment('SERVER_ID')!, 'registration_auth': registrationAuth, }; final response = await http.post( Uri.parse(serverManagerUrl), headers: { 'Content-Type': 'application/json', }, body: jsonEncode(requestBody), ); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); if (responseData['success']) { // Store operational seed in memory for this isolate RollingCodesService.setOperationalSeed(responseData['operational_seed']); print('Heartbeat isolate registered successfully'); } else { throw Exception('Heartbeat isolate registration failed: ${responseData['error']}'); } } else { throw Exception('Heartbeat isolate registration request failed: ${response.statusCode}'); } } Future> _getServerStatus() async { int activeConnections = 0; try { // Get active WireGuard connections count ProcessResult wgShowResult = await Process.run('wg', ['show', 'wg0']); if (wgShowResult.exitCode == 0) { // Count peer entries (simplified - each peer has multiple lines) String output = wgShowResult.stdout as String; activeConnections = 'peer:'.allMatches(output).length; } } catch (e) { print("Error getting WireGuard status: $e"); } return { 'active_connections': activeConnections, }; }