From bd53b5f15a10e0e3b2258c5e76c02d713a519027 Mon Sep 17 00:00:00 2001 From: Kadan Stadelmann Date: Mon, 27 Oct 2025 01:41:09 +0100 Subject: [PATCH] add FD monitoring --- ios/Runner/AppDelegate.swift | 57 +++++++ ios/Runner/FdMonitor.swift | 214 +++++++++++++++++++++++++++ lib/services/fd_monitor_service.dart | 189 +++++++++++++++++++++++ 3 files changed, 460 insertions(+) create mode 100644 ios/Runner/FdMonitor.swift create mode 100644 lib/services/fd_monitor_service.dart diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 8be1cecd13..e8f0f3928b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -7,7 +7,64 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + NSLog("🔴 AppDelegate: didFinishLaunchingWithOptions REACHED") GeneratedPluginRegistrant.register(with: self) + + NSLog("AppDelegate: Setting up FD Monitor channel...") + setupFdMonitorChannel() + + #if DEBUG + NSLog("AppDelegate: DEBUG build detected, auto-starting FD Monitor...") + FdMonitor.shared.start(intervalSeconds: 60.0) + #else + NSLog("AppDelegate: RELEASE build, FD Monitor NOT auto-started (use Flutter to start manually)") + #endif + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + private func setupFdMonitorChannel() { + guard let controller = window?.rootViewController as? FlutterViewController else { + return + } + + let channel = FlutterMethodChannel( + name: "com.komodo.wallet/fd_monitor", + binaryMessenger: controller.binaryMessenger + ) + + channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in + self?.handleFdMonitorMethodCall(call: call, result: result) + } + } + + private func handleFdMonitorMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "start": + let intervalSeconds: TimeInterval + if let args = call.arguments as? [String: Any], + let interval = args["intervalSeconds"] as? Double { + intervalSeconds = interval + } else { + intervalSeconds = 60.0 + } + FdMonitor.shared.start(intervalSeconds: intervalSeconds) + result(["success": true, "message": "FD Monitor started with interval: \(intervalSeconds)s"]) + + case "stop": + FdMonitor.shared.stop() + result(["success": true, "message": "FD Monitor stopped"]) + + case "getCurrentCount": + let count = FdMonitor.shared.getCurrentCount() + result(count) + + case "logDetailedStatus": + FdMonitor.shared.logDetailedStatus() + result(["success": true, "message": "Detailed FD status logged"]) + + default: + result(FlutterMethodNotImplemented) + } + } } diff --git a/ios/Runner/FdMonitor.swift b/ios/Runner/FdMonitor.swift new file mode 100644 index 0000000000..a5738fef3f --- /dev/null +++ b/ios/Runner/FdMonitor.swift @@ -0,0 +1,214 @@ +import Foundation +import OSLog + +class FdMonitor { + + + private var timer: DispatchSourceTimer? + private let queue = DispatchQueue(label: "com.komodo.wallet.fdmonitor", qos: .utility) + private let logger = Logger(subsystem: "com.komodo.wallet", category: "fd-monitor") + private var isRunning = false + private var intervalSeconds: TimeInterval = 60.0 + private var lastCount: Int = 0 + private let detailThresholdPercent: Double = 0.8 + + + static let shared = FdMonitor() + + private init() { + NSLog("FDMonitor: Singleton initialized") + } + + + func start(intervalSeconds: TimeInterval = 60.0) { + NSLog("FDMonitor: start() called with interval=%.1f", intervalSeconds) + + queue.async { [weak self] in + guard let self = self else { return } + + if self.isRunning { + NSLog("FDMonitor: Already running, ignoring start request") + self.logger.info("FD Monitor already running") + return + } + + self.intervalSeconds = intervalSeconds + self.isRunning = true + + NSLog("FDMonitor: Logging initial FD status...") + self.logFileDescriptorStatus(detailed: false) + + NSLog("FDMonitor: Creating and scheduling timer...") + let timer = DispatchSource.makeTimerSource(queue: self.queue) + timer.schedule(deadline: .now() + intervalSeconds, repeating: intervalSeconds) + timer.setEventHandler { [weak self] in + self?.logFileDescriptorStatus(detailed: false) + } + timer.resume() + + self.timer = timer + + NSLog("FDMonitor: Started successfully with interval=%.1f seconds", intervalSeconds) + self.logger.notice("FD Monitor started with interval: \(intervalSeconds, privacy: .public) seconds") + + NSLog("FDMonitor: Logging detailed status for immediate verification...") + self.logFileDescriptorStatus(detailed: true) + } + } + + func stop() { + queue.async { [weak self] in + guard let self = self else { return } + + if !self.isRunning { + self.logger.info("FD Monitor not running") + return + } + + self.timer?.cancel() + self.timer = nil + self.isRunning = false + + self.logger.info("FD Monitor stopped") + } + } + + func getCurrentCount() -> [String: Any] { + var result: [String: Any] = [:] + + queue.sync { + let fdInfo = self.getFileDescriptorInfo() + result = [ + "openCount": fdInfo.openCount, + "tableSize": fdInfo.tableSize, + "softLimit": fdInfo.softLimit, + "hardLimit": fdInfo.hardLimit, + "percentUsed": fdInfo.percentUsed, + "timestamp": ISO8601DateFormatter().string(from: Date()) + ] + } + + return result + } + + func logDetailedStatus() { + queue.async { [weak self] in + self?.logFileDescriptorStatus(detailed: true) + } + } + + + private struct FdInfo { + let openCount: Int + let tableSize: Int + let softLimit: Int + let hardLimit: Int + let percentUsed: Double + } + + private func getFileDescriptorInfo() -> FdInfo { + let tableSize = Int(getdtablesize()) + + var rlimit = rlimit() + getrlimit(RLIMIT_NOFILE, &rlimit) + let softLimit = Int(rlimit.rlim_cur) + let hardLimit = Int(rlimit.rlim_max) + + var openCount = 0 + for fd in 0.. 0 ? (Double(openCount) / Double(softLimit)) * 100.0 : 0.0 + + return FdInfo( + openCount: openCount, + tableSize: tableSize, + softLimit: softLimit, + hardLimit: hardLimit, + percentUsed: percentUsed + ) + } + + private func logFileDescriptorStatus(detailed: Bool) { + let fdInfo = getFileDescriptorInfo() + + let statusMsg = String(format: "FD Status: open=%d/%d (%.1f%%), table_size=%d, soft_limit=%d, hard_limit=%d", + fdInfo.openCount, fdInfo.softLimit, fdInfo.percentUsed, + fdInfo.tableSize, fdInfo.softLimit, fdInfo.hardLimit) + + NSLog("FDMonitor: %@", statusMsg) + logger.info("\(statusMsg, privacy: .public)") + + let shouldLogDetails = detailed || + fdInfo.percentUsed > (detailThresholdPercent * 100.0) || + (fdInfo.openCount - lastCount) > 50 + + if shouldLogDetails { + NSLog("FDMonitor: FD count approaching limit or significant increase detected, logging details...") + logger.info("FD count approaching limit or significant increase detected, logging details...") + logDetailedFileDescriptors(maxSamples: 50) + } + + lastCount = fdInfo.openCount + } + + private func logDetailedFileDescriptors(maxSamples: Int) { + let tableSize = Int(getdtablesize()) + var logged = 0 + var fdsByType: [String: Int] = [:] + + for fd in 0.." + + var st = stat() + let fstatResult = fstat(fd32, &st) + + var typeStr = "unknown" + if fstatResult == 0 { + let mode = st.st_mode + if (mode & S_IFMT) == S_IFREG { + typeStr = "file" + } else if (mode & S_IFMT) == S_IFDIR { + typeStr = "dir" + } else if (mode & S_IFMT) == S_IFSOCK { + typeStr = "socket" + } else if (mode & S_IFMT) == S_IFIFO { + typeStr = "pipe" + } else if (mode & S_IFMT) == S_IFCHR { + typeStr = "char_dev" + } else if (mode & S_IFMT) == S_IFBLK { + typeStr = "block_dev" + } + } + + fdsByType[typeStr, default: 0] += 1 + + if logged < 20 { // Only log first 20 individual FDs to avoid spam + logger.debug(" FD \(fd): type=\(typeStr) path=\(path)") + } + + logged += 1 + } + + logger.info("FD breakdown by type:") + for (type, count) in fdsByType.sorted(by: { $0.value > $1.value }) { + logger.info(" \(type): \(count)") + } + } +} diff --git a/lib/services/fd_monitor_service.dart b/lib/services/fd_monitor_service.dart new file mode 100644 index 0000000000..df9cf56f3b --- /dev/null +++ b/lib/services/fd_monitor_service.dart @@ -0,0 +1,189 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class FdMonitorService { + static const MethodChannel _channel = + MethodChannel('com.komodo.wallet/fd_monitor'); + + static FdMonitorService? _instance; + + factory FdMonitorService() { + _instance ??= FdMonitorService._internal(); + return _instance!; + } + + FdMonitorService._internal(); + + bool _isMonitoring = false; + + bool get isMonitoring => _isMonitoring; + + Future> start({double intervalSeconds = 60.0}) async { + try { + final result = await _channel.invokeMethod>( + 'start', + {'intervalSeconds': intervalSeconds}, + ); + + if (result != null) { + _isMonitoring = true; + return Map.from(result); + } + + return {'success': false, 'message': 'No response from native code'}; + } on PlatformException catch (e) { + return { + 'success': false, + 'message': 'Platform error: ${e.message}', + 'code': e.code, + }; + } on MissingPluginException { + return { + 'success': false, + 'message': 'FD monitoring not available on this platform', + }; + } catch (e) { + return { + 'success': false, + 'message': 'Unexpected error: $e', + }; + } + } + + Future> stop() async { + try { + final result = + await _channel.invokeMethod>('stop'); + + if (result != null) { + _isMonitoring = false; + return Map.from(result); + } + + return {'success': false, 'message': 'No response from native code'}; + } on PlatformException catch (e) { + return { + 'success': false, + 'message': 'Platform error: ${e.message}', + 'code': e.code, + }; + } on MissingPluginException { + return { + 'success': false, + 'message': 'FD monitoring not available on this platform', + }; + } catch (e) { + return { + 'success': false, + 'message': 'Unexpected error: $e', + }; + } + } + + Future getCurrentCount() async { + try { + final result = + await _channel.invokeMethod>('getCurrentCount'); + + if (result != null) { + return FdMonitorStats.fromMap(Map.from(result)); + } + + return null; + } on PlatformException catch (e) { + print('FD Monitor error getting count: ${e.message}'); + return null; + } on MissingPluginException { + return null; + } catch (e) { + print('FD Monitor unexpected error: $e'); + return null; + } + } + + Future> logDetailedStatus() async { + try { + final result = + await _channel.invokeMethod>('logDetailedStatus'); + + if (result != null) { + return Map.from(result); + } + + return {'success': false, 'message': 'No response from native code'}; + } on PlatformException catch (e) { + return { + 'success': false, + 'message': 'Platform error: ${e.message}', + 'code': e.code, + }; + } on MissingPluginException { + return { + 'success': false, + 'message': 'FD monitoring not available on this platform', + }; + } catch (e) { + return { + 'success': false, + 'message': 'Unexpected error: $e', + }; + } + } + + Future startIfDebugMode({double intervalSeconds = 60.0}) async { + if (kDebugMode) { + await start(intervalSeconds: intervalSeconds); + } + } +} + +class FdMonitorStats { + final int openCount; + final int tableSize; + final int softLimit; + final int hardLimit; + final double percentUsed; + final String timestamp; + + FdMonitorStats({ + required this.openCount, + required this.tableSize, + required this.softLimit, + required this.hardLimit, + required this.percentUsed, + required this.timestamp, + }); + + factory FdMonitorStats.fromMap(Map map) { + return FdMonitorStats( + openCount: map['openCount'] as int? ?? 0, + tableSize: map['tableSize'] as int? ?? 0, + softLimit: map['softLimit'] as int? ?? 0, + hardLimit: map['hardLimit'] as int? ?? 0, + percentUsed: (map['percentUsed'] as num?)?.toDouble() ?? 0.0, + timestamp: map['timestamp'] as String? ?? '', + ); + } + + Map toMap() { + return { + 'openCount': openCount, + 'tableSize': tableSize, + 'softLimit': softLimit, + 'hardLimit': hardLimit, + 'percentUsed': percentUsed, + 'timestamp': timestamp, + }; + } + + @override + String toString() { + return 'FdMonitorStats(open: $openCount/$softLimit (${percentUsed.toStringAsFixed(1)}%), ' + 'table: $tableSize, limits: $softLimit/$hardLimit, time: $timestamp)'; + } + + bool get isApproachingLimit => percentUsed > 80.0; + + bool get isCritical => percentUsed > 90.0; +}