From 7749ebb66bdef803741d8935eb2bb803a66a9d89 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:04:45 +0000 Subject: [PATCH] android, ios: pass the chat controller to the network info command The controller was read twice - checked and then read again inside the command - so replacing it between the two failed the command, and on iOS reading it again terminates the app, as getChatCtrl() is a fatalError when it is not set. It is now read once and passed to the command. A failed report is not alerted about, as the network info is reported automatically: the command is sent with sendCmd rather than sendCommandOkResp, which shows an alert. --- apps/ios/Shared/Model/NetworkObserver.swift | 5 +++-- apps/ios/Shared/Model/SimpleXAPI.swift | 4 ++-- apps/ios/SimpleXChat/API.swift | 4 ++++ apps/ios/spec/api.md | 4 ++-- apps/ios/spec/architecture.md | 6 +++--- .../chat/simplex/common/helpers/NetworkObserver.kt | 8 ++++++-- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 10 +++++++--- apps/multiplatform/spec/api.md | 2 +- 8 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/ios/Shared/Model/NetworkObserver.swift b/apps/ios/Shared/Model/NetworkObserver.swift index 84c35afa07..01116fc548 100644 --- a/apps/ios/Shared/Model/NetworkObserver.swift +++ b/apps/ios/Shared/Model/NetworkObserver.swift @@ -61,10 +61,11 @@ class NetworkObserver { DispatchQueue.main.sync { ChatModel.shared.networkInfo = info } - if !hasChatCtrl() { return } self.monitorLock.sync { + guard let ctrl = currentChatCtrl() else { return } do { - try apiSetNetworkInfo(info) + // the controller is passed, so that resetting it while the report is sent is not a crash + try apiSetNetworkInfo(info, ctrl: ctrl) } catch let err { logger.error("setNetworkInfo error: \(responseError(err))") } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index be2cde7cd6..b22019a12a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -896,8 +896,8 @@ func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws { throw r.unexpected } -func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws { - let r: ChatResponse2 = try chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) +func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo, ctrl: chat_ctrl) throws { + let r: ChatResponse2 = try chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo), ctrl: ctrl) if case .cmdOk = r { return } throw r.unexpected } diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 3b860d524b..822e8a33c6 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -21,6 +21,10 @@ public func getChatCtrl() -> chat_ctrl { fatalError("chat controller not initialized") } +public func currentChatCtrl() -> chat_ctrl? { + chatController +} + public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: MigrationConfirmation? = nil, backgroundMode: Bool = false) -> (Bool, DBMigrationResult) { if let res = migrationResult { return res } let dbPath = getAppDatabasePath().path diff --git a/apps/ios/spec/api.md b/apps/ios/spec/api.md index 56c3c34cb6..c6c69a9110 100644 --- a/apps/ios/spec/api.md +++ b/apps/ios/spec/api.md @@ -35,7 +35,7 @@ The iOS app communicates with the Haskell core exclusively through a command/res - [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift) -- `ChatCommand` ([L15](../Shared/Model/AppAPITypes.swift#L15)), `ChatResponse0` ([L657](../Shared/Model/AppAPITypes.swift#L657)), `ChatResponse1` ([L779](../Shared/Model/AppAPITypes.swift#L779)), `ChatResponse2` ([L919](../Shared/Model/AppAPITypes.swift#L919)), `ChatEvent` ([L1069](../Shared/Model/AppAPITypes.swift#L1069)) enums - [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift) -- `APIResult` ([L27](../SimpleXChat/APITypes.swift#L27)), `ChatAPIResult` ([L65](../SimpleXChat/APITypes.swift#L65)), `ChatError` ([L699](../SimpleXChat/APITypes.swift#L699)) - [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) -- FFI bridge functions (`chatSendCmd` [L121](../Shared/Model/SimpleXAPI.swift#L121), `chatRecvMsg` [L237](../Shared/Model/SimpleXAPI.swift#L237)) -- [`SimpleXChat/API.swift`](../SimpleXChat/API.swift) -- Low-level FFI (`sendSimpleXCmd` [L115](../SimpleXChat/API.swift#L115), `recvSimpleXMsg` [L137](../SimpleXChat/API.swift#L137)) +- [`SimpleXChat/API.swift`](../SimpleXChat/API.swift) -- Low-level FFI (`sendSimpleXCmd` [L119](../SimpleXChat/API.swift#L119), `recvSimpleXMsg` [L141](../SimpleXChat/API.swift#L141)) - `SimpleXChat/ChatTypes.swift` -- Data types used in commands/responses (User, Contact, GroupInfo, ChatItem, etc.) - `../../src/Simplex/Chat/Controller.hs` -- Haskell controller (function `chat_send_cmd_retry`, `chat_recv_msg_wait`) @@ -543,7 +543,7 @@ func chatApiSendCmdWithRetry( // SimpleXAPI.swift L127 ```swift // Direct C FFI call -- serializes cmd.cmdString, calls chat_send_cmd_retry, decodes response -public func sendSimpleXCmd( // API.swift L115 +public func sendSimpleXCmd( // API.swift L119 _ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?, retryNum: Int32 = 0 diff --git a/apps/ios/spec/architecture.md b/apps/ios/spec/architecture.md index 9ab3eb1fd2..ec75cdb870 100644 --- a/apps/ios/spec/architecture.md +++ b/apps/ios/spec/architecture.md @@ -112,14 +112,14 @@ func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, // Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?, - retryNum: Int32 = 0) -> APIResult // SimpleXChat/API.swift L114 + retryNum: Int32 = 0) -> APIResult // SimpleXChat/API.swift L119 ``` ### Data Flow 1. Swift constructs a `ChatCommand` enum value (e.g., `.apiSendMessages(type:id:scope:live:ttl:composedMessages:)`) 2. [`ChatCommand.cmdString`](../Shared/Model/AppAPITypes.swift#L15) serializes it to a command string (e.g., `"/_send @1 json {...}"`) -3. [`sendSimpleXCmd`](../SimpleXChat/API.swift#L115) passes the string to `chat_send_cmd_retry` via C FFI +3. [`sendSimpleXCmd`](../SimpleXChat/API.swift#L119) passes the string to `chat_send_cmd_retry` via C FFI 4. Haskell core processes the command, returns JSON response string 5. Swift decodes JSON into [`APIResult`](../SimpleXChat/APITypes.swift#L27) where `R: ChatAPIResult` 6. Result is either `.result(R)`, `.error(ChatError)`, or `.invalid(type, json)` @@ -323,7 +323,7 @@ Chat relays are SMP servers that forward messages to channel subscribers. They a | App delegate | [`Shared/AppDelegate.swift`](../Shared/AppDelegate.swift#L15) | L15 | | Root view | [`Shared/ContentView.swift`](../Shared/ContentView.swift#L24) | L24 | | FFI bridge | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | L93 | -| Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift#L115) | L115 | +| Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift#L119) | L119 | | App state | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | L337 | | API types | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | L15 | | Shared types | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | L27 | diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt index 77e0f49cc8..7e7f888013 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt @@ -78,14 +78,18 @@ class NetworkObserver { noNetworkJob.cancel() if (info.online) { withBGApi { - if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) { + // the controller is read once and passed, so that replacing it between the two does not fail the command + val ctrl = controller.currentCtrl() + if (ctrl != null && controller.apiSetNetworkInfo(info, ctrl)) { chatModel.networkInfo.value = info } } } else { noNetworkJob = withBGApi { delay(3000) - if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) { + // the controller is read once and passed, so that replacing it between the two does not fail the command + val ctrl = controller.currentCtrl() + if (ctrl != null && controller.apiSetNetworkInfo(info, ctrl)) { chatModel.networkInfo.value = info } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e3bea95c03..a853d1265a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -532,7 +532,7 @@ class AppPreferences { private const val MESSAGE_TIMEOUT: Int = 300_000_000 object ChatController { - private var chatCtrl: ChatCtrl? = -1 + @Volatile private var chatCtrl: ChatCtrl? = -1 // Spec: spec/state.md#appPrefs val appPrefs: AppPreferences by lazy { AppPreferences() } @@ -547,6 +547,9 @@ object ChatController { fun getChatCtrl(): ChatCtrl? = chatCtrl + // read once, so that the controller that is reset between the checks is not used + fun currentCtrl(): ChatCtrl? = chatCtrl?.takeIf { it != -1L } + fun setChatCtrl(ctrl: ChatCtrl?) { val wasRunning = receiverJob != null stopReceiver() @@ -1497,8 +1500,9 @@ object ChatController { return false } - suspend fun apiSetNetworkInfo(networkInfo: UserNetworkInfo): Boolean = - sendCommandOkResp(null, CC.APISetNetworkInfo(networkInfo)) + // the network info is reported automatically, and again on the next event, so a failure is not alerted about + suspend fun apiSetNetworkInfo(networkInfo: UserNetworkInfo, ctrl: ChatCtrl): Boolean = + sendCmd(null, CC.APISetNetworkInfo(networkInfo), ctrl).ok suspend fun apiSetMemberSettings(rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings): Boolean = sendCommandOkResp(rh, CC.ApiSetMemberSettings(groupId, groupMemberId, memberSettings)) diff --git a/apps/multiplatform/spec/api.md b/apps/multiplatform/spec/api.md index 4114e9de4f..f9c9ee93b2 100644 --- a/apps/multiplatform/spec/api.md +++ b/apps/multiplatform/spec/api.md @@ -234,7 +234,7 @@ All functions below are `suspend fun` members of `ChatController` ([SimpleXAPI.k | Command | Parameters | Description | Line | |---------|-----------|-------------|------| | `apiSetNetworkConfig` | `cfg: NetCfg, showAlertOnError: Boolean, ctrl: ChatCtrl?` | Apply network configuration (SOCKS proxy, timeouts, etc.) | [L1313](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1313) | -| `apiSetNetworkInfo` | `networkInfo: UserNetworkInfo` | Update network reachability information | [L1340](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1340) | +| `apiSetNetworkInfo` | `networkInfo: UserNetworkInfo, ctrl: ChatCtrl` | Update network reachability information | [L1398](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1398) | | `apiSetSettings` | `rh: Long?, type: ChatType, id: Long, settings: ChatSettings` | Update per-chat settings (notifications, favorites) | [L1333](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1333) | | `apiStorageEncryption` | `currentKey: String, newKey: String` | Change the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | | `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Verify a database encryption key is correct | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) |