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.
This commit is contained in:
Narasimha-sc committed 2026-10-05 09:33:06 +00:00
1 parent 905df659d3
commit 7749ebb66b
8 files changed
+28 -15

No files matched your search

+3 -2
View File
@@ -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))")
}
+2 -2
View File
@@ -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
}
+4
View File
@@ -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
+2 -2
View File
@@ -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<R>` ([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<R: ChatAPIResult>( // SimpleXAPI.swift L127
```swift
// Direct C FFI call -- serializes cmd.cmdString, calls chat_send_cmd_retry, decodes response
public func sendSimpleXCmd<R: ChatAPIResult>( // API.swift L115
public func sendSimpleXCmd<R: ChatAPIResult>( // API.swift L119
_ cmd: ChatCmdProtocol,
_ ctrl: chat_ctrl?,
retryNum: Int32 = 0
+3 -3
View File
@@ -112,14 +112,14 @@ func chatApiSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
// Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON
func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?,
retryNum: Int32 = 0) -> APIResult<R> // SimpleXChat/API.swift L114
retryNum: Int32 = 0) -> APIResult<R> // 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<R>`](../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 |
@@ -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
}
}
@@ -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))
+1 -1
View File
@@ -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) |