Author SHA1 Message Date
evgeny f4bcc6eae9 Log BLE packet sequences and acknowledgements in debug category 2026-10-30 04:31:55 +01:00
evgeny 97b18caa1f Revert "Log the exact reason and lifetime of rejected call invites"
This reverts commit 55637c296f80f016f0a29f54aaa7aaf742b92443.
2026-10-30 04:31:55 +01:00
evgeny 6a2e552194 Log the exact reason and lifetime of rejected call invites 2026-10-30 04:31:55 +01:00
evgeny 93672ae95e Fix desktop BLE mock signature for wire channels 2026-10-29 22:53:35 +01:00
evgeny ae1d149545 Merge branch 'master' of http://git.syqiq.tech/jeka/utun2 2026-10-29 22:43:06 +01:00
evgeny 33a3774938 Require BLE probe publication and document WinRT diagnostics 2026-10-29 22:42:36 +01:00
evgeny dd80f589fa Record successful BLE ACK loads in both connection roles 2026-10-29 22:39:54 +01:00
evgeny 4a7f3253f6 Test WinRT BLE exception isolation and pending shutdown 2026-10-29 22:39:15 +01:00
evgeny dc70174af9 Preserve WinRT BLE event context and diagnose API failures 2026-10-29 22:38:08 +01:00
evgeny 552f7b31b0 Record ACK scheduling regression and successful desktop load 2026-10-29 22:36:14 +01:00
evgeny b1a272fa23 Clarify BLE ACK spacing after local completion 2026-10-29 22:33:17 +01:00
evgeny d5505c054f Reserve the ACK interval after completion for payload traffic 2026-10-29 22:31:17 +01:00
evgeny c52afe4f41 Give DATA a slot between slow cumulative BLE ACKs 2026-10-29 22:29:46 +01:00
evgeny b7e0b1f12c Reproduce DATA starvation behind slow BLE ACK completion 2026-10-29 22:29:32 +01:00
evgeny c83b30aa0b Document BLE v3 ACK framing and bounded remote credit 2026-10-29 22:26:34 +01:00
evgeny 6338a2aae1 Open both fake GATT endpoints before delivering HELLO 2026-10-29 22:22:35 +01:00
evgeny 358c3093dd Refresh WinRT subscriptions before the first incoming value 2026-10-29 22:21:51 +01:00
evgeny fabf0c1663 Reject BLE values before all mandatory subscriptions 2026-10-29 22:21:12 +01:00
evgeny 4016e6b73c Include BLE credit events in the existing wire trace 2026-10-29 22:19:43 +01:00
evgeny c43726c17d Verify BLE ACK timeout independently of local completion 2026-10-29 22:17:57 +01:00
evgeny b31b772e66 Delay remote ACK delivery independently in the BLE fixture 2026-10-29 22:16:46 +01:00
evgeny 46f2137f06 Keep BLE fault injection correct with prefixed fragments 2026-10-29 22:16:08 +01:00
evgeny d33b67d87c Test BLE remote credit, ACK timing, duplex and ID wrap 2026-10-29 22:15:32 +01:00
evgeny 85977d97e0 Verify mandatory ACK subscriptions and addressed BlueZ ACKs 2026-10-29 22:13:46 +01:00
evgeny d4182c22cd Connect the ACK characteristic to Android GATT and JNI 2026-10-29 22:13:46 +01:00
evgeny e7adcbe139 Support the BLE ACK characteristic in WinRT 2026-10-29 22:11:46 +01:00
evgeny dd1769ee4c Adapt BLE identity and ETCP fixtures to the ACK profile 2026-10-29 22:10:40 +01:00
evgeny cecdbbe1d7 Add the mandatory ACK characteristic to BlueZ 2026-10-29 22:10:24 +01:00
evgeny 4e38536a55 Bound BLE DATA flight with cumulative 16-bit ACKs 2026-10-29 22:09:12 +01:00
evgeny be0dff7b2c Define the BLE ACK wire channel and fixed flight budget 2026-10-29 22:08:29 +01:00
evgeny e194535b25 Record RTO validation and remaining BLE throughput work 2026-10-29 21:39:31 +01:00
evgeny 635e1726e6 Document BLE readiness fixes and remaining HCI queue delay 2026-10-29 21:37:40 +01:00
evgeny 55bb052406 Model accepted sends in the ETCP lifecycle fixture 2026-10-29 21:34:02 +01:00
evgeny 0f2794cd51 Format the RTT growth regression cases consistently 2026-10-29 21:25:46 +01:00
evgeny a9bec81328 Let ETCP retransmission timeouts cover measured RTT above one second 2026-10-29 21:18:52 +01:00
evgeny 5b2e11337c Use the native BLE discovery version in Android scan filters 2026-10-29 20:47:48 +01:00
evgeny 285a242d2a Version BLE discovery for unacknowledged DATA compatibility 2026-10-29 20:39:20 +01:00
evgeny b5fb6b0f4d Ignore cached BlueZ advertisements when discovering live BLE peers 2026-10-29 20:29:55 +01:00
evgeny 8081f94ebe Record BLE negotiation evidence and retry pre-transfer radio connections 2026-10-29 20:29:09 +01:00
evgeny 05d78c9960 Add a controlled Android scan pause to the manual BLE probe 2026-10-29 20:14:51 +01:00
evgeny 8ab25c1389 Request high BLE priority for incoming Android links on the shared ACL 2026-10-29 19:58:40 +01:00
evgeny 7dab663b93 Preserve incoming disconnect handling and exercise resolved-service notification 2026-10-29 19:50:44 +01:00
evgeny 51621e6240 Wait for resolved BlueZ services before opening cached GATT characteristics 2026-10-29 19:45:27 +01:00
evgeny 083495986e Document HCI queue delays and explain incompatible GATT properties 2026-10-29 19:10:13 +01:00
evgeny b5b522b732 Log ETCP control submission failures and WinRT error codes 2026-10-29 19:00:53 +01:00
evgeny c5711b5974 Preserve Android BLE failure status and callback diagnostics 2026-10-29 19:00:11 +01:00
evgeny de6e2ada0c Report BLE frame gaps and preserve BlueZ I/O errors 2026-10-29 18:56:50 +01:00
evgeny 8811276ca8 Trace BLE frame delivery and authenticated packet dispatch 2026-10-29 18:46:00 +01:00
evgeny 33b4f0acc8 Record BLE throughput results and harden the hardware probe 2026-10-29 18:38:11 +01:00
evgeny 3def3f5af2 Use unacknowledged BLE DATA and request faster connections 2026-10-29 18:28:05 +01:00
evgeny 9382abb6e6 Fix Windows system library dependencies for uasync 2026-10-29 17:46:18 +01:00
evgeny 5152b17114 Fix BLE link display and remove obsolete tool projects 2026-10-29 17:10:39 +01:00
evgeny f84622966e Add BLE ETCP links and GATT invitations for desktop and Android
Attach packet drivers to initially empty ETCP connections. Share BLE neighbor
tracking and group fallback between desktop and Android in cgathui_common.

Deliver join links through a dedicated Write/Indicate characteristic with
independent fragmentation queues, application acknowledgements, deduplication
and bounded retries. Merge LAN/BLE discovery and add a separate Bluetooth
invitation reception setting to both clients.

Validation: 230 core tests, BLE transport/BlueZ/UI tests, ASAN/UBSAN,
Linux GUI and Android builds, and Windows backend cross-compilation.
Physical Android/Linux invitation exchange passed with either GATT initiator.
The Android-initiated run also verified all 32 ETCP packets of 1500 bytes;
the desktop-initiated load run reached 7/32 within its 45-second time limit.
2026-10-29 16:41:04 +01:00
evgeny ef6a5673bd Preserve member connection state when its refresh fails 2026-10-29 14:02:25 +01:00
evgeny 05ea602a3a Distinguish failed member-link snapshots from confirmed disconnection 2026-10-29 14:02:25 +01:00
evgeny a4c7899ffb Keep native member query failures distinct from valid empty snapshots 2026-10-29 14:02:25 +01:00
evgeny 0878947e13 Describe Android view-model snapshots and application-owned configuration 2026-10-29 14:00:16 +01:00
evgeny 2460e810d6 Document asynchronous BLE detach and report probe cleanup failures 2026-10-29 13:59:14 +01:00
evgeny 0f03aa8b5c Align Android architecture with serialized lifecycle and Bluetooth ownership 2026-10-29 13:56:57 +01:00
evgeny f1f113a5f2 Describe readiness recovery when a rejected restart retains the core 2026-10-29 13:56:57 +01:00
evgeny 0800fd61a7 Retry failed BLE SDK releases and propagate radio command rejection 2026-10-29 13:54:52 +01:00
evgeny 5410c4c6c6 Retain BLE cleanup ownership until each Android release succeeds 2026-10-29 13:53:22 +01:00
evgeny 6c7f5b7032 Apply Bluetooth flag changes atomically without replacing concurrent selections 2026-10-29 13:53:22 +01:00
evgeny 8dc10bb54f Keep Bluetooth settings writes alive and contain Android launch failures 2026-10-29 13:51:59 +01:00
evgeny ae4f524448 Observe confirmed Bluetooth preferences from the application owner 2026-10-29 13:51:58 +01:00
evgeny 1651d1b870 Document native ownership and reject media creation during core cleanup 2026-10-29 13:50:21 +01:00
evgeny 4a5a8cf057 Keep malformed events and deleted database state out of Android UI 2026-10-29 13:49:57 +01:00
evgeny c5879042fb Validate complete channel event frames before publishing state 2026-10-29 13:49:00 +01:00
evgeny d636a03928 Test restart readiness recovery and explicit UTF-8 database cleanup 2026-10-29 13:39:58 +01:00
evgeny 132e222415 Resume retained core owners and observe unexpected service loss 2026-10-29 13:35:42 +01:00
evgeny 791ff42547 Keep database reset atomic and owned beyond the activity lifecycle 2026-10-29 13:35:42 +01:00
evgeny 665137314a Serialize native lifecycle mutations and renew readiness across restart 2026-10-29 13:35:42 +01:00
evgeny 0568d263e0 Reject fresh radio and publisher owners during core cleanup 2026-10-29 13:35:42 +01:00
evgeny 044ff034a6 Await radio and shared audio release before native core teardown 2026-10-29 13:35:42 +01:00
evgeny ea67b491f9 Show private conversation start failures and test stale confirmation isolation 2026-10-29 13:35:41 +01:00
evgeny 321a2c3ed7 Match private conversation confirmations before opening the requested peer 2026-10-29 13:35:41 +01:00
evgeny 69ae8b57cb Bind private conversation start requests to the worker conversation ID 2026-10-29 13:35:41 +01:00
evgeny c1ad35e152 Persist channel mute consistently and own panel preference changes 2026-10-29 13:35:41 +01:00
evgeny fb696d0d56 Report rejected core actions and log nickname write outcomes 2026-10-29 13:35:41 +01:00
evgeny 3aecc095ff Return queue rejection for invite and private conversation requests 2026-10-29 13:35:41 +01:00
evgeny da540627fc Keep accepted commands bound to their instance while restart readiness closes 2026-10-29 13:35:41 +01:00
evgeny f15ca995b7 Close frontend readiness while a restart is pending and retain failed core state 2026-10-29 13:35:41 +01:00
evgeny f50310afed Verify config callback thread attachment and reject truncated database paths 2026-10-29 13:35:41 +01:00
evgeny 20f506fc3c Contain JNI config failures and validate audio result buffers 2026-10-29 13:35:41 +01:00
evgeny af776deed6 Keep member snapshots atomic and verify escaped socket names 2026-10-29 13:35:41 +01:00
evgeny 89a4630477 Build complete network JSON snapshots and verify SQL failure recovery 2026-10-29 13:35:41 +01:00
evgeny 05ab3b8054 Return complete channel snapshots and distinguish database errors from empty history 2026-10-29 13:35:41 +01:00
evgeny 57227b331d Exercise event callback replacement and exceptions on a real JVM 2026-10-29 13:35:41 +01:00
evgeny 2f4cded218 Preserve event callback lifetime and contain JNI delivery exceptions 2026-10-29 13:35:41 +01:00
evgeny 17f79b16cd Run checked JVM string tests with the Android verification tasks 2026-10-29 13:35:41 +01:00
evgeny c0a4beeec4 Verify JNI UTF-8 strings against a real JVM with checked JNI 2026-10-29 13:35:41 +01:00
evgeny 86f0ac7625 Route remaining JNI producers through the core task owner 2026-10-29 13:35:41 +01:00
evgeny 95f0e89567 Read JNI database and live network snapshots in the core worker 2026-10-29 13:35:41 +01:00
evgeny 8614ba5428 Use ordinary UTF-8 across JNI strings and test cancelled media ownership 2026-10-29 13:35:41 +01:00
evgeny 72ee8f2586 Release detached voice and PM file payloads when queued work is cancelled 2026-10-29 13:35:41 +01:00
evgeny 57bfadb04c Retain media request ownership until execution or worker cancellation 2026-10-29 13:35:41 +01:00
evgeny 38ab885609 Expose attachment cancellation as a typed task discard callback 2026-10-29 13:35:41 +01:00
evgeny f731ac340d Serialize bridge command publication and report rejected lifecycle requests 2026-10-29 13:35:41 +01:00
evgeny 8374c60b0e Publish message requests through the worker lifetime owner 2026-10-29 13:35:41 +01:00
evgeny 5a4ddca190 Protect lifecycle producers and leave fatal signals to the platform 2026-10-29 13:35:41 +01:00
evgeny 929df3b6cb Exercise worker request cancellation against real lifecycle teardown 2026-10-29 13:35:41 +01:00
evgeny 8c0c17270e Own frontend tasks through worker teardown and marshal synchronous reads 2026-10-29 13:35:41 +01:00
evgeny d20e112933 Isolate activity cleanup and document startup event ownership 2026-10-29 13:35:41 +01:00
evgeny b8ccd515dd Load startup preferences asynchronously and contain service launch errors 2026-10-29 13:35:41 +01:00
evgeny 013ff93406 Load audio and network settings on IO and document persistent change ownership 2026-10-29 13:35:41 +01:00
evgeny 30648086b4 Contain remaining settings loader failures and remove retired UI scopes 2026-10-29 13:35:41 +01:00
evgeny 828b36f290 Keep failed settings teardown retryable and preserve UI until database stop succeeds 2026-10-29 13:35:41 +01:00
evgeny c5c4d0e588 Load confirmed settings after pending changes and guard invalid audio preferences 2026-10-29 13:35:41 +01:00
evgeny 48564e8ab3 Respect microphone permission callback ownership and test settings disposal 2026-10-29 13:35:19 +01:00
evgeny b086750cf3 Persist audio settings before runtime updates and release hidden microphone tests 2026-10-29 13:35:19 +01:00
evgeny eb837206c3 Own radio settings changes and preserve observed state after IO failures 2026-10-29 13:35:19 +01:00
evgeny 61bdf4a6df Retain observed settings through read failures and retry without default resets 2026-10-29 13:35:19 +01:00
evgeny e8aeaaf365 Apply developer settings only after accepted persistence completes 2026-10-29 13:35:19 +01:00
evgeny a9495de90f Keep settings persistence alive after navigation and guard initial preference loading 2026-10-29 13:35:19 +01:00
evgeny 87166db832 Test settings persistence ownership, ordering and failure isolation 2026-10-29 13:35:19 +01:00
evgeny a95245e272 Own ordered settings changes independently of the Compose screen 2026-10-29 13:35:19 +01:00
evgeny 299518e866 Preserve confirmed settings and manual socket choices after failures 2026-10-29 13:35:19 +01:00
evgeny de907c697b Match temporary IPv6 flags using canonical address bytes 2026-10-29 13:35:19 +01:00
evgeny 268e8794ab Document process lifetime voice ownership and core shutdown prerequisites 2026-10-29 13:35:19 +01:00
evgeny d0d63c1d4b Keep voice cleanup independent of caller cancellation and stop playback promptly 2026-10-29 13:35:19 +01:00
evgeny 48a20cda8c Reject new capture while an IO teardown holds its lifecycle lock 2026-10-29 13:35:19 +01:00
evgeny c7356d2a5b Wait for asynchronous voice cleanup acknowledgement in lifecycle regression test 2026-10-29 13:35:19 +01:00
evgeny 5ee87c535b Avoid capture-lock acquisition while rolling back a playback worker launch 2026-10-29 13:35:19 +01:00
evgeny 3f2cc2abb3 Verify call preparation waits for voice cleanup and document process barriers 2026-10-29 13:35:19 +01:00
evgeny 41ea2afaaa Verify explicit native cancellation failure and document independent audio ownership 2026-10-29 13:35:19 +01:00
evgeny a9099a5ea7 Separate playback controls from blocked capture cleanup and gate stopping cores 2026-10-29 13:35:19 +01:00
evgeny b25766ae5a Test retained voice resources, decoder failures and nonblocking disposal 2026-10-29 13:35:19 +01:00
evgeny bb08b20e58 Wait for voice recording and playback release before call signalling 2026-10-29 13:35:19 +01:00
evgeny a85d2cc8c7 Retain voice resources after ViewModel disposal and block premature core teardown 2026-10-29 13:35:19 +01:00
evgeny 85af6f5b87 Deliver app events once and isolate failures in independent consumers 2026-10-29 13:35:19 +01:00
evgeny 6916575380 Contain headless foreground, notification and teardown failures 2026-10-29 13:34:26 +01:00
evgeny d7fb8568b2 Retry core shutdown until pending frontend resources are released 2026-10-29 13:34:26 +01:00
evgeny 1529dfd04b Document camera finalization ownership and isolate late SDK diagnostics 2026-10-29 13:34:26 +01:00
evgeny 8dd8dc96d7 Verify recorded tracks and handle camera permission and SDK cleanup failures 2026-10-29 13:34:26 +01:00
evgeny 9ac6e4a2f4 Compile track inspection and its SDK boundary in video lifecycle tests 2026-10-29 13:34:26 +01:00
evgeny fca68080bc Own recorded video track inspection until extractor release succeeds 2026-10-29 13:34:26 +01:00
evgeny 8a1c77793d Keep camera caches until finalization and contain recording control failures 2026-10-29 13:34:26 +01:00
evgeny 871acd9383 Test camera writer completion, failed close, binding cleanup and cache transfer 2026-10-29 13:34:26 +01:00
evgeny 09386ee392 Retain camera recording and cache ownership until writer completion 2026-10-29 13:34:26 +01:00
evgeny 3ae8d12434 Keep submission IDs unique across ViewModels and document native cache transfer 2026-10-29 13:34:26 +01:00
evgeny ab6a10026e Verify PM voice receipts and document composer and cache ownership 2026-10-29 13:34:26 +01:00
evgeny 3f0c89a6a3 Verify real PM media commit failures, exact receipts and source cleanup 2026-10-29 13:34:26 +01:00
evgeny 6eb865242b Test video submission validation and retriever cleanup acknowledgements 2026-10-29 13:34:26 +01:00
evgeny 3df64b6744 Use voice commit receipts for both conversation types and document ownership 2026-10-29 13:34:26 +01:00
evgeny dad127d770 Wait for committed PM media and retain native ownership of accepted cache files 2026-10-29 13:34:26 +01:00
evgeny 2dbc45e63d Transfer Android ordinary attachment caches to the shared media worker 2026-10-29 13:34:26 +01:00
evgeny ce80fbcd10 Expose committed PM file results through Android JNI 2026-10-29 13:34:26 +01:00
evgeny d6eed3e9c9 Specify receipt policy in native DM media callers 2026-10-29 13:34:26 +01:00
evgeny 31e461dca2 Carry voice PM receipts and document shared media completion semantics 2026-10-29 13:34:26 +01:00
evgeny e943879c50 Carry Android photo and video request IDs through PM preparation 2026-10-29 13:34:26 +01:00
evgeny 4fd5ebf57a Acknowledge PM media submissions after local commit or preparation failure 2026-10-29 13:34:26 +01:00
evgeny dae19c06d7 Preserve composer commit receipts and finish message batches after rotation 2026-10-29 13:34:26 +01:00
evgeny 47e997df15 Own composed message batches and receipts beyond Activity lifetime 2026-10-29 13:34:26 +01:00
evgeny a0c8d5490c Test atomic JSON snapshots, failure diagnostics and subsequent recovery 2026-10-29 13:34:26 +01:00
evgeny 3eb7f8b0e6 Preserve UI snapshots after parse failure and ignore superseded queries 2026-10-29 13:34:26 +01:00
evgeny 7d67213847 Handle unavailable channel snapshots in Android media consumers 2026-10-29 13:34:26 +01:00
evgeny 4fbb825b9b Reject partial Android repository snapshots and distinguish query failures 2026-10-29 13:34:26 +01:00
evgeny e942190b9f Allow call teardown to defer optional pause when reporter owns the output 2026-10-29 13:34:26 +01:00
evgeny 1097c24d91 Document call signalling barriers and isolate routing diagnostic failures 2026-10-29 13:34:26 +01:00
evgeny 5dffdee8ff Document nonblocking audio cleanup and keep lifecycle tests on main 2026-10-29 13:34:25 +01:00
evgeny ff1f7a4bb8 Keep call audio main thread responsive while device workers are blocked 2026-10-29 13:34:25 +01:00
evgeny 314b77242f Test call route persistence and busy outcome failure isolation 2026-10-29 13:34:25 +01:00
evgeny 236dbd3e84 Contain call notification and route persistence failures 2026-10-29 13:34:25 +01:00
evgeny 7e328b3908 Test rejected call signalling and deferred hangup recovery 2026-10-29 13:34:25 +01:00
evgeny 38e2cc4336 Retain failed call end commands until the worker accepts them 2026-10-29 13:34:25 +01:00
evgeny 1600f5710c Return explicit queue results for call signalling commands 2026-10-29 13:34:25 +01:00
evgeny eccf5f451c Use explicit DM test headers and clarify proximity lifecycle text 2026-10-29 13:34:25 +01:00
evgeny f5405cdb3d Verify Android DM commit results and document sensor cleanup retry 2026-10-29 13:34:25 +01:00
evgeny 96166cd3a7 Keep PM drafts until the core confirms persistence 2026-10-29 13:34:25 +01:00
evgeny d5a1e4b962 Request DM persistence acknowledgement and send canonical UTF-8 2026-10-29 13:34:25 +01:00
evgeny c5d8307697 Retain proximity registrations after partial SDK failures 2026-10-29 13:34:25 +01:00
evgeny aa9ca53468 Clarify member update acknowledgement and suppress duplicate downloads 2026-10-29 13:34:25 +01:00
evgeny e0e97f1c7a Guard media previews and cancel interrupted voice recording gestures 2026-10-29 13:34:25 +01:00
evgeny 236aeae828 Bound video thumbnails and share retriever cleanup ownership 2026-10-29 13:34:25 +01:00
evgeny d3e83c3c1a Guard member editor actions and display unsigned node identifiers 2026-10-29 13:34:25 +01:00
evgeny 2c2d4f3d9d Validate restored attachment recipients and document conversation UI 2026-10-29 13:34:25 +01:00
evgeny 98d4f903a0 Report channel file export failures and preserve attachment choices 2026-10-29 13:34:25 +01:00
evgeny 666210c3fb Preserve selected attachment targets and contain composer errors 2026-10-29 13:34:25 +01:00
evgeny 89cf180984 Document local video playback and metadata release contracts 2026-10-29 13:34:25 +01:00
evgeny e18ecda41e Fix video fixture ownership and identify playback resource diagnostics 2026-10-29 13:34:25 +01:00
evgeny 8678ace89b Test local video SDK failures, stale callbacks and release barriers 2026-10-29 13:34:25 +01:00
evgeny 5694f97f12 Add controllable SDK boundaries for local video lifecycle tests 2026-10-29 13:34:25 +01:00
evgeny 7ec46278af Keep video gesture callbacks current and forbid reads during cleanup 2026-10-29 13:34:25 +01:00
evgeny fdecfe1b78 Drive video controls from SDK outcomes and preserve paused playback 2026-10-29 13:34:25 +01:00
evgeny d9661d0f59 Own local video playback resources through confirmed SDK release 2026-10-29 13:34:25 +01:00
evgeny 401edd37b8 Document call view ownership and preserve probe interrupts 2026-10-29 13:34:25 +01:00
evgeny 623344369f Guard call permission results and release video targets idempotently 2026-10-29 13:34:25 +01:00
evgeny c8ecf89424 Handle PM document export failures and stale channel dialogs 2026-10-29 13:34:25 +01:00
evgeny 096c1ee2a8 Report probe worker failures and attempt independent SDK cleanup 2026-10-29 13:34:25 +01:00
evgeny 6f5850b9ca Reject empty codec probe runs and preserve decoder cleanup 2026-10-29 13:34:25 +01:00
evgeny 9383a68f17 Release failed member snapshot requests and validate deletion frames 2026-10-29 13:34:25 +01:00
evgeny 36af8d077f Handle stream capture permission failures and document preview release 2026-10-29 13:34:25 +01:00
evgeny 45d1b50cb4 Release preview UI leases even without texture destroy callbacks 2026-10-29 13:34:25 +01:00
evgeny b3f5c49c4d Keep stream catalogue labels current and preserve viewer errors 2026-10-29 13:34:25 +01:00
evgeny 6f0bc4307f Document QR task ownership and application license assets 2026-10-29 13:34:25 +01:00
evgeny f7a80d0a3a Exercise QR cleanup during pending tasks and SDK failures 2026-10-29 13:34:25 +01:00
evgeny 36328814e6 Compile real QR scanner ownership in video host tests 2026-10-29 13:34:25 +01:00
evgeny 41208833b6 Model asynchronous ML Kit tasks and scanner failures in host tests 2026-10-29 13:34:25 +01:00
evgeny 68997853b2 Add CameraX boundaries for QR resource lifecycle tests 2026-10-29 13:34:25 +01:00
evgeny 857338a386 Retain QR scanner resources through asynchronous task completion 2026-10-29 13:34:25 +01:00
evgeny dc09332b2e Share immersive window restoration across stream viewers 2026-10-29 13:34:25 +01:00
evgeny efe421b162 Restore viewer window state and handle photo decoding errors 2026-10-29 13:34:25 +01:00
evgeny bd4bd47e00 Recover join dialog after clipboard and connection errors 2026-10-29 13:34:25 +01:00
evgeny 87b7c6f36c Clarify setup and invitation dialog contracts 2026-10-29 13:34:25 +01:00
evgeny a4a4707c4a Describe NAT snapshots and stream buffer indicator limits 2026-10-29 13:34:25 +01:00
evgeny a442cea1b2 Handle emoji playback failures and clarify image gesture contracts 2026-10-29 13:34:25 +01:00
evgeny 104d70d465 Handle audio device menu launch errors and document UI ownership 2026-10-29 13:34:25 +01:00
evgeny 257df6c520 Describe publisher navigation and video viewport geometry 2026-10-29 13:34:25 +01:00
evgeny 83ba4a03a6 Clarify call banner and status icon presentation contracts 2026-10-29 13:34:25 +01:00
evgeny a7b3d0fc51 Document Telecom cleanup and confirmed route ownership 2026-10-29 13:34:25 +01:00
evgeny eaed746934 Verify radio restoration waits for both service and Telecom release 2026-10-29 13:34:25 +01:00
evgeny e81138e3d1 Await Telecom cleanup before restoring the previous radio session 2026-10-29 13:34:25 +01:00
evgeny 870f1b5914 Clarify Android build modules and handle rejected boot service requests 2026-10-29 13:34:25 +01:00
evgeny 1954c36367 Clarify Telecom ownership and distinguish requested from confirmed cleanup 2026-10-29 13:34:25 +01:00
evgeny 37c72cc2fb Report the retained service ID after the combined teardown check 2026-10-29 13:34:25 +01:00
evgeny 9f1b09aefe Verify Telecom cleanup blocks media and new foreground ownership 2026-10-29 13:34:25 +01:00
evgeny b71f992a45 Retain the media teardown barrier while Telecom cleanup is pending 2026-10-29 13:34:25 +01:00
evgeny e7a03f33fb Cover endpoint callback order and malformed Telecom service requests 2026-10-29 13:34:25 +01:00
evgeny 90e33f568b Validate Telecom request identity and contain extras decoding errors 2026-10-29 13:34:25 +01:00
evgeny 482e1658b0 Preserve the latest route choice while endpoint confirmation is pending 2026-10-29 13:34:25 +01:00
evgeny 8d3cee4a56 Exercise real Telecom ownership, callbacks and endpoint routing 2026-10-29 13:34:25 +01:00
evgeny 8a9e421ea7 Ignore ended Telecom callbacks and contain route listener failures 2026-10-29 13:34:25 +01:00
evgeny f031e510b8 Model Telecom state, endpoint outcomes and ConnectionService requests 2026-10-29 13:34:25 +01:00
evgeny 3babc636b4 Compile real Telecom integration against explicit host boundaries 2026-10-29 13:34:25 +01:00
evgeny 06058fad8e Add URI and main executor boundaries for Telecom host tests 2026-10-29 13:34:25 +01:00
evgeny 7d4a0e05fd Allow explicit Telecom cleanup retry after a rejected timer 2026-10-29 13:34:25 +01:00
evgeny 01a84c6851 Retain retiring Telecom connections and reject duplicate registrations 2026-10-29 13:34:25 +01:00
evgeny c136ff60c2 Document acknowledged publisher shutdown and lifecycle fault coverage 2026-10-29 13:34:25 +01:00
evgeny e5d3a1d584 Cover partial capture startup and callbacks from retired camera owners 2026-10-29 13:34:25 +01:00
evgeny 4af2402e20 Exercise capture ownership across Camera2, Surface, microphone and Opus failures 2026-10-29 13:34:25 +01:00
evgeny 6194538963 Preserve the earliest publisher cleanup retry across repeated shutdowns 2026-10-29 13:34:25 +01:00
evgeny 41a4a2e37e Compile real publisher capture with controlled microphone and native boundaries 2026-10-29 13:34:25 +01:00
evgeny 7a2fc0112c Model capture codec callbacks and Camera2 sessions for host fault tests 2026-10-29 13:34:24 +01:00
evgeny 59d1839d8d Exercise publisher lifecycle failures, retained capture and stale notifications 2026-10-29 13:34:24 +01:00
evgeny f1ffd21cda Report missing publisher system services and use current app language 2026-10-29 13:34:24 +01:00
evgeny 9166268102 Compile publisher service and controller in the lifecycle test harness 2026-10-29 13:34:24 +01:00
evgeny 1beb19a7f8 Expose publisher service boundaries to the existing lifecycle harness 2026-10-29 13:34:24 +01:00
evgeny e49ccfeb64 Guard publisher handshake, error callbacks and native teardown 2026-10-29 13:34:24 +01:00
evgeny 2abdb229fd Keep publisher service owners until capture and foreground cleanup are acknowledged 2026-10-29 13:34:24 +01:00
evgeny 68d21fbffa Acknowledge stream capture shutdown only after every owner is released 2026-10-29 13:34:24 +01:00
evgeny 5e9f7e01e0 Preserve capture camera and session ownership through late callbacks 2026-10-29 13:34:24 +01:00
evgeny fd1e3a7f77 Adopt passive stream capture ownership before starting SDK resources 2026-10-29 13:34:24 +01:00
evgeny 100d208a9a Document stream resource barriers and retained routing registrations 2026-10-29 13:34:24 +01:00
evgeny a1c624a819 Preserve the next KEY after a GAP within one native stream batch 2026-10-29 13:34:24 +01:00
evgeny 0330f30c53 Verify orphan routing listener cleanup and independent route release 2026-10-29 13:34:24 +01:00
evgeny 2516545f9d Retain failed media routing listener removals for acknowledged retry 2026-10-29 13:34:24 +01:00
evgeny ee20c8b9b7 Guard viewer startup and cover the native teardown barrier 2026-10-29 13:34:24 +01:00
evgeny 495d399691 Block native teardown until viewer and shared mixer cleanup is confirmed 2026-10-29 13:34:24 +01:00
evgeny 16c7d817cc Keep mixer shutdown inside the guarded cleanup path 2026-10-29 13:34:24 +01:00
evgeny 304b48a289 Verify stream viewer cleanup barriers with native and SDK faults 2026-10-29 13:34:24 +01:00
evgeny e072624c8d Keep the host audio threshold API compatible with Android signatures 2026-10-29 13:34:24 +01:00
evgeny 68651795bf Compile real stream viewer and mixer for host lifecycle tests 2026-10-29 13:34:24 +01:00
evgeny 5dffa9917f Add controlled native stream and Android audio test boundaries 2026-10-29 13:34:24 +01:00
evgeny 43a749d8ab Use retained Surface leases for stream viewer texture cleanup 2026-10-29 13:34:24 +01:00
evgeny 35fe0ecda1 Exercise stream decoder and audio device failure ownership 2026-10-29 13:34:24 +01:00
evgeny 5b80d5769d Compile the real stream video worker against controlled SDK boundaries 2026-10-29 13:34:24 +01:00
evgeny 26348b35e8 Retain stream audio owners through partial startup and failed release 2026-10-29 13:34:24 +01:00
evgeny f28f95a501 Keep viewer resources and Surface callbacks until cleanup succeeds 2026-10-29 13:34:24 +01:00
evgeny 2f353bf35e Retain stream decoder ownership until worker and SDK cleanup finish 2026-10-29 13:34:24 +01:00
evgeny aa27892c12 Reject regressed stream PTS before submitting it to the decoder 2026-10-29 13:34:24 +01:00
evgeny 5e6313377b Publish verified encoder modes only after successful probe cleanup 2026-10-29 13:34:24 +01:00
evgeny 96f8e6db5b Document acknowledged video shutdown and passive resource adoption 2026-10-29 13:34:24 +01:00
evgeny e9fa7ebb45 Exercise camera teardown despite RX or captured-image cleanup errors 2026-10-29 13:34:24 +01:00
evgeny ea121ac4e7 Model failed-session acknowledgement and verify live preview fallback 2026-10-29 13:34:24 +01:00
evgeny 2068774d58 Clean camera resources independently of RX and image release failures 2026-10-29 13:34:24 +01:00
evgeny e6e3006128 Test Camera2 configuration failure and preview output retention 2026-10-29 13:34:24 +01:00
evgeny 4d559dc08b Handle failed Camera2 sessions without waiting for an absent callback 2026-10-29 13:34:24 +01:00
evgeny 53340db0e3 Verify independent image and codec cleanup under SDK failures 2026-10-29 13:34:24 +01:00
evgeny 43992bb6b3 Expose codec resource ownership to host fault tests 2026-10-29 13:34:24 +01:00
evgeny d9959e5176 Keep codec cleanup diagnostics independent of released SDK handles 2026-10-29 13:34:24 +01:00
evgeny b519536237 Retain codec and input image owners across failed cleanup 2026-10-29 13:34:24 +01:00
evgeny bf3d4f14e8 Keep pending video cleanup distinct from service shutdown failure 2026-10-29 13:34:24 +01:00
evgeny 87e90b9462 Cover late camera callbacks, rejected cleanup and video port recovery 2026-10-29 13:34:24 +01:00
evgeny 776cec590e Retry each pending encoder once per cleanup pass with the shared backoff 2026-10-29 13:34:24 +01:00
evgeny 682ed68d2f Exercise explicit video startup after host tests adopt resource ownership 2026-10-29 13:34:24 +01:00
evgeny 2320aa4f71 Retain the video engine in the call service before starting its workers 2026-10-29 13:34:24 +01:00
evgeny fba07e893f Separate passive video ownership from explicit worker startup 2026-10-29 13:34:24 +01:00
evgeny 5e9dad2e9e Handle camera error callbacks promised alongside synchronous open failures 2026-10-29 13:34:24 +01:00
evgeny a6f41c0256 Confirm camera codec and native video cleanup before acknowledging shutdown 2026-10-29 13:34:24 +01:00
evgeny 53383ec754 Reproduce premature video shutdown acknowledgments and lost camera cleanup owners 2026-10-29 13:34:24 +01:00
evgeny 43f61f6f0b Model pending video shutdown separately from an exception in service tests 2026-10-29 13:34:24 +01:00
evgeny 1ddd2a4a0b Let call service retain video ownership while asynchronous shutdown is pending 2026-10-29 13:34:24 +01:00
evgeny 12db99531f Compile the real call video engine alongside its receive worker in host tests 2026-10-29 13:34:24 +01:00
evgeny 63f393c615 Provide video engine capability, encoder and native port test boundaries 2026-10-29 13:34:24 +01:00
evgeny 3a4e1c6a7c Model Camera2 device session and image reader ownership for lifecycle faults 2026-10-29 13:34:24 +01:00
evgeny 5ad36ba57b Support direct preview buffer sizing and YUV capture format in host fixtures 2026-10-29 13:34:24 +01:00
evgeny 92a301ae54 Provide camera context and fault-controlled orientation sensor boundaries 2026-10-29 13:34:24 +01:00
evgeny 278efd574e Run camera handler tasks on real host worker threads with controlled main timers 2026-10-29 13:34:24 +01:00
evgeny d88d77c14d Specify call snapshot units and the limited config getter interface 2026-10-29 13:34:24 +01:00
evgeny 1596088ca0 Describe camera capability candidates and canonical UDP logger targets 2026-10-29 13:34:24 +01:00
evgeny 8e4d28c1fc Document camera preset restoration, orientation and manual bitrate bounds 2026-10-29 13:34:24 +01:00
evgeny 97f1eaa048 Clarify stream grid and PCM timeline contracts without per-record allocations 2026-10-29 13:34:24 +01:00
evgeny ffa5dece4f Validate stream snapshot counts and verify native metadata mappings 2026-10-29 13:34:24 +01:00
evgeny a1dcf28bf9 Reject malformed stream batches before mutating the playback queue 2026-10-29 13:34:24 +01:00
evgeny 5fb6e72c43 Describe stream source snapshots, codec framing and playback telemetry contracts 2026-10-29 13:34:24 +01:00
evgeny cc7648bb33 Validate length-prefixed NAL headers consistently with Annex B input 2026-10-29 13:34:24 +01:00
evgeny 92ce2d84e9 Describe preview lease transfer, callback cancellation and physical cleanup retries 2026-10-29 13:34:24 +01:00
evgeny 0a777baabb Adopt a fresh preview lease when a previous command lease was cancelled 2026-10-29 13:34:24 +01:00
evgeny 44a137e23b Verify preview ownership, callback rollback and independent physical release retries 2026-10-29 13:34:24 +01:00
evgeny 4d15c6d42a Keep preview cleanup retryable after scheduling rejection and compile real leases in host tests 2026-10-29 13:34:24 +01:00
evgeny 83ea839d0c Recognize shared preview sources and model texture cleanup failures 2026-10-29 13:34:24 +01:00
evgeny 4a40fb9d52 Give preview owners independent leases and retain failed physical cleanup 2026-10-29 13:34:24 +01:00
evgeny 4ba21b3082 Remove the trailing blank line from the audited video JNI declarations 2026-10-29 13:34:24 +01:00
evgeny b2b34df2ec Document video receive cleanup acknowledgments and host fault coverage 2026-10-29 13:34:24 +01:00
evgeny 8040431a4a Specify call video bitrate, cadence and orientation helper contracts 2026-10-29 13:34:24 +01:00
evgeny 98cd5bf81c Describe call video JNI buffers, return values and playout handle ownership 2026-10-29 13:34:24 +01:00
evgeny 000273d451 Keep the earliest pending video cleanup retry instead of postponing it twice 2026-10-29 13:34:24 +01:00
evgeny d8fd73d917 Exercise video RX cleanup failures, surface leases and generation draining 2026-10-29 13:34:24 +01:00
evgeny 71e1e5c747 Run real call video receive tests with controlled native and codec boundaries 2026-10-29 13:34:24 +01:00
evgeny 7f89b7c734 Provide controlled MediaCodec input output and cleanup failures 2026-10-29 13:34:24 +01:00
evgeny 8951af6b63 Model video surfaces and deterministic cleanup retry timers on the JVM 2026-10-29 13:34:24 +01:00
evgeny af2209ec96 Prepare host video tests and initialize receive ownership before starting its worker 2026-10-29 13:34:24 +01:00
evgeny eff6f8965a Retain video receive resources through failed decoder and surface cleanup 2026-10-29 13:34:24 +01:00
evgeny 448fa39a0c Separate call video format values from the native runtime boundary 2026-10-29 13:34:24 +01:00
evgeny 99ab91d2f5 Clarify router cleanup as a native audio teardown prerequisite 2026-10-29 13:34:24 +01:00
evgeny bc6ca78ce8 Document retained router resources and shared session close acknowledgments 2026-10-29 13:34:23 +01:00
evgeny 774dc10d84 Preserve configured Telecom ownership when startup retries router cleanup 2026-10-29 13:34:23 +01:00
evgeny e660f8f762 Cover partial communication selection and SCO cleanup effects 2026-10-29 13:34:23 +01:00
evgeny 8cdb4190b6 Log rejected media operations while session cleanup remains pending 2026-10-29 13:34:23 +01:00
evgeny a6f8156564 Track possible SCO effects before starting the Android request 2026-10-29 13:34:23 +01:00
evgeny 48ab63a9d3 Test router cleanup barriers in call and shared media owners 2026-10-29 13:34:23 +01:00
evgeny b3e1356d16 Exercise retained router cleanup, stale proxies and SCO failure recovery 2026-10-29 13:34:23 +01:00
evgeny 3ef245fc0a Verify pending router cleanup through repeatable stop calls 2026-10-29 13:34:23 +01:00
evgeny f7ebf81061 Ignore repeated Bluetooth callbacks for proxies already released 2026-10-29 13:34:23 +01:00
evgeny 3d2e98b02e Keep media audio session close retryable until router cleanup succeeds 2026-10-29 13:34:23 +01:00
evgeny 1970f895da Retain router resources and retry incomplete audio route cleanup 2026-10-29 13:34:23 +01:00
evgeny 1da06164e2 Document retryable call audio shutdown and device ownership 2026-10-29 13:34:23 +01:00
evgeny 94b5d44fe7 Check call audio main ownership and the late Telecom handoff 2026-10-29 13:34:23 +01:00
evgeny 326bfd124e Guard call audio ownership on main and avoid touching released devices 2026-10-29 13:34:23 +01:00
evgeny 0340a59b15 Dispatch host mainExecutor callbacks through the main looper 2026-10-29 13:34:23 +01:00
evgeny f385d3dc5e Cover call audio teardown and partial device startup failures 2026-10-29 13:34:23 +01:00
evgeny 83a671b72f Compile the real call audio engine in host regression tests 2026-10-29 13:34:23 +01:00
evgeny ecd4e12274 Provide call DSP and Telecom boundaries for real audio worker tests 2026-10-29 13:34:23 +01:00
evgeny 7304647baa Model call audio hardware failures and blocking capture on the JVM 2026-10-29 13:34:23 +01:00
evgeny e857b9de75 Retain call audio owners until worker and device cleanup succeeds 2026-10-29 13:34:23 +01:00
evgeny a1c77268b8 Document retained call service cleanup, retry policy and native core teardown barriers 2026-10-29 13:34:23 +01:00
evgeny 6ac6dd1f6f Describe call teardown acknowledgments and the boundary of asynchronous video cleanup 2026-10-29 13:34:23 +01:00
evgeny 02763bd4a1 Reject missing service components instead of waiting forever for Android startup 2026-10-29 13:34:23 +01:00
evgeny 4dd84c601b Cover null foreground service results in call and overlay startup 2026-10-29 13:34:23 +01:00
evgeny d2c6734bfb Model Android service requests that return no matching component 2026-10-29 13:34:23 +01:00
evgeny 17dc0b26b4 Preserve the service call identity until its final lifecycle notification 2026-10-29 13:34:23 +01:00
evgeny 42c6e2a62b Keep service loss associated with the call after local resources are released 2026-10-29 13:34:23 +01:00
evgeny 9b0ca7ca2b Document call service resource acknowledgments and contain conflict callback errors 2026-10-29 13:34:23 +01:00
evgeny aefca81444 Keep cancelled media preparation inside the native teardown barrier 2026-10-29 13:34:23 +01:00
evgeny ca7b23909b Use retained resource ownership instead of Deferred cancellation as the cleanup barrier 2026-10-29 13:34:23 +01:00
evgeny a5d8131d8d Verify call cleanup completion, core barriers, retry lifetime and stale callbacks 2026-10-29 13:34:23 +01:00
evgeny 3e7ac47d18 Close the independent call service cleanup block at the correct scope 2026-10-29 13:34:23 +01:00
evgeny 826055d08b Keep failed video shutdown joins and rejected cleanup requests retryable 2026-10-29 13:34:23 +01:00
evgeny 878f71e211 Wait for call service cleanup before restoring media and stopping the native core 2026-10-29 13:34:23 +01:00
evgeny cf793ce6bb Confirm call cleanup before releasing ownership and retry every retained resource independently 2026-10-29 13:34:23 +01:00
evgeny 8eb2363177 Retain call service startup owners before IPC effects and isolate callback failures 2026-10-29 13:34:23 +01:00
evgeny 4f9bfbbcf3 Reference the host service lifecycle result through its Android boundary 2026-10-29 13:34:23 +01:00
evgeny ff05e34d22 Compile call integration models and contain missing notification service errors 2026-10-29 13:34:23 +01:00
evgeny 9c70858828 Exercise call service startup, partial IPC effects and independent cleanup failures 2026-10-29 13:34:23 +01:00
evgeny dd9aac79a2 Handle absent notification managers and nullable call channels explicitly 2026-10-29 13:34:23 +01:00
evgeny 15a2a7bb70 Control audio and video cleanup effects around the real call service controller 2026-10-29 13:34:23 +01:00
evgeny be3a2d2325 Add isolated host checks for the real call service and controller 2026-10-29 13:34:23 +01:00
evgeny bc7063966d Expose call signaling and saved route boundaries to service integration tests 2026-10-29 13:34:23 +01:00
evgeny 1600452147 Model call power events and supported foreground API levels 2026-10-29 13:34:23 +01:00
evgeny 68b922bf80 Expose call foreground intents, receivers and service type contracts 2026-10-29 13:34:23 +01:00
evgeny 0a48cccd9a Model ongoing call notifications and independent foreground stop failures 2026-10-29 13:34:23 +01:00
evgeny a65385bbb1 Keep radio outcomes separate from the live stream foreground notification 2026-10-29 13:34:23 +01:00
evgeny a90416f50c Preserve stream notifications and synchronize host checks with audio cleanup 2026-10-29 13:34:23 +01:00
evgeny 0d9dabcd23 Cover touch permission checks with the overlay error boundary 2026-10-29 13:34:23 +01:00
evgeny 4de7d7ec9d Document overlay service handshake, window cleanup and gesture error ownership 2026-10-29 13:34:23 +01:00
evgeny 1559e0681b Verify overlay latch release, drag lock and position ownership contracts 2026-10-29 13:34:23 +01:00
evgeny 5553d968b6 Deliver cancelled service creation and account for self-context stop requests 2026-10-29 13:34:23 +01:00
evgeny abef146325 Stop unused overlay foreground capture after touch and permission failures 2026-10-29 13:34:23 +01:00
evgeny 36368640f1 Exercise overlay PTT through native capture and permission-loss stop races 2026-10-29 13:34:23 +01:00
evgeny 982315bacc Cancel overlay PTT on gesture faults and ignore callbacks from retired windows 2026-10-29 13:34:23 +01:00
evgeny b3d70ea92e Retain overlay windows through partial IPC failures and guarded service startup 2026-10-29 13:34:23 +01:00
evgeny fa14305349 Avoid a host settings getter collision with the application API 2026-10-29 13:34:23 +01:00
evgeny 08f3b892a5 Separate overlay notification ownership and recover rejected service actions 2026-10-29 13:34:23 +01:00
evgeny d6c178a39b Keep host window lookup separate from the service-owned window field 2026-10-29 13:34:23 +01:00
evgeny 18cb661c91 Reproduce overlay partial IPC effects, stale gestures and foreground lifecycle failures 2026-10-29 13:34:23 +01:00
evgeny 2f43407ae4 Require an available notification manager at overlay IPC boundaries 2026-10-29 13:34:23 +01:00
evgeny 19050cd4f2 Use the real overlay service with radio lifecycle components in host tests 2026-10-29 13:34:23 +01:00
evgeny ff16eec78d Expose overlay notification and state resource identifiers to JVM tests 2026-10-29 13:34:23 +01:00
evgeny 263d1348ec Model overlay permissions and persistent settings for service tests 2026-10-29 13:34:23 +01:00
evgeny ce756ed6a9 Expose foreground type and deferred layout contracts in overlay fixtures 2026-10-29 13:34:23 +01:00
evgeny 19fd2d3042 Provide overlay drawing boundaries without Android rendering 2026-10-29 13:34:23 +01:00
evgeny d1dc3bb8f7 Model overlay window IPC and foreground service lifetime for Android host tests 2026-10-29 13:34:23 +01:00
evgeny 6d5b2bd176 Record distinct radio core availability and busy outcomes 2026-10-29 13:34:23 +01:00
evgeny 6ee4aa99eb Report unavailable core accurately and include malformed radio event fields 2026-10-29 13:34:23 +01:00
evgeny 16a9bee8f3 Distinguish unavailable radio signaling from an occupied call slot 2026-10-29 13:34:23 +01:00
evgeny 8100ea728e Document Android radio invitation ownership, UI phases and fault checks 2026-10-29 13:34:23 +01:00
evgeny 6ec8950a85 Clarify asynchronous radio signaling and wire identity contracts 2026-10-29 13:34:23 +01:00
evgeny cda411a57e Document radio invitation state and cover continuation and stale pending events 2026-10-29 13:34:23 +01:00
evgeny 45992a3dd3 Allow the missing-notification-service fault in radio cleanup assertions 2026-10-29 13:34:23 +01:00
evgeny 8a7f732428 Validate radio call identities and preserve terminal activation states 2026-10-29 13:34:23 +01:00
evgeny b24bee7a89 Own pending radio starts and diagnose every signaling queue refusal 2026-10-29 13:34:23 +01:00
evgeny 16f52c0136 Exercise radio call signaling refusals, identity, activation and notification lifetime 2026-10-29 13:34:23 +01:00
evgeny 93b174ecde Keep radio outcome presentation failures inside the controller lifecycle 2026-10-29 13:34:23 +01:00
evgeny 8b982efa05 Compile the real radio call controller in host lifecycle tests 2026-10-29 13:34:23 +01:00
evgeny a5718c4ca9 Model native radio call requests and controller dependencies in host fixtures 2026-10-29 13:34:23 +01:00
evgeny 5823d6f907 Bind capture startup to the generation of its update request 2026-10-29 13:34:23 +01:00
evgeny 7810137eb0 Handle playback cancellation and document complete radio failure recovery 2026-10-29 13:34:23 +01:00
evgeny b1a888ffb6 Distinguish stop-unblocked writes from active playback failures 2026-10-29 13:34:23 +01:00
evgeny a5fea0c503 Preserve replacement PTT requests during obsolete capture rollback 2026-10-29 13:34:23 +01:00
evgeny 17caeb0a9c Cover capture startup failures that overlap a replacement group 2026-10-29 13:34:23 +01:00
evgeny 6677b3abfe Document radio worker ownership and format failure boundaries 2026-10-29 13:34:23 +01:00
evgeny 613ea315b0 Synchronize radio discontinuity regression with the first capture frame 2026-10-29 13:34:23 +01:00
evgeny ed8d208454 Recover radio worker failures on the owning I/O executor 2026-10-29 13:34:23 +01:00
evgeny a04cfe4441 Keep radio capture owned through startup faults and deferred native cleanup 2026-10-29 13:34:23 +01:00
evgeny 1ddd75e134 Isolate complete short-write frames from playback already in flight 2026-10-29 13:34:23 +01:00
evgeny 9baa72759f Reproduce radio worker failures and stale session cleanup races 2026-10-29 13:34:23 +01:00
evgeny 5d74bcd865 Model radio worker I/O faults and partial native capture effects 2026-10-29 13:34:23 +01:00
evgeny 128af68a89 Invalidate radio settings before reconfiguration and log failed attempts 2026-10-29 13:34:23 +01:00
evgeny 67615cc330 Reproduce stale settings acknowledgements after failed radio reconfiguration 2026-10-29 13:34:23 +01:00
evgeny bb8518ab67 Document radio startup completion and independent cleanup contracts 2026-10-29 13:34:23 +01:00
evgeny 2fd8a35230 Verify failed radio restore and trace resource join completion 2026-10-29 13:34:23 +01:00
evgeny b6ec030128 Bind radio screen callbacks to the originating startup generation 2026-10-29 13:34:23 +01:00
evgeny 7084454e84 Verify radio output lease dependencies and interrupted cleanup 2026-10-29 13:34:23 +01:00
evgeny 7699ebfcf6 Own radio acknowledgement timers and close unschedulable background capture 2026-10-29 13:34:23 +01:00
evgeny 0ba189863e Complete radio startup callbacks after failures and reject stale readiness 2026-10-29 13:34:23 +01:00
evgeny e197f72722 Release radio resources independently and retain failed cleanup owners 2026-10-29 13:34:23 +01:00
evgeny 530dd81a71 Isolate old radio cleanup failures between negative control scenarios 2026-10-29 13:34:23 +01:00
evgeny c1fe2a3cd0 Reproduce radio lifecycle cleanup and startup callback failures 2026-10-29 13:34:23 +01:00
evgeny 45bc97c25b Model radio startup and main queue delivery failures 2026-10-29 13:34:23 +01:00
evgeny 982ed0ca7d Model recoverable receiver and radio output lifecycle failures 2026-10-29 13:34:23 +01:00
evgeny 7780922353 Document radio PTT release fallback and native command coverage 2026-10-29 13:34:22 +01:00
evgeny 46edb1beb5 End radio PTT immediately when its release timer is rejected 2026-10-29 13:34:22 +01:00
evgeny 722ca3e6f7 Drain radio callbacks before terminating the host executor 2026-10-29 13:34:22 +01:00
evgeny ef95ff54b9 Reproduce stuck radio PTT and verify native release ordering 2026-10-29 13:34:22 +01:00
evgeny a162cc6eca Compile the real radio engine in Android host tests 2026-10-29 13:34:22 +01:00
evgeny f494c20430 Keep radio overlay and toast boundaries outside host audio logic 2026-10-29 13:34:22 +01:00
evgeny 6df258cf13 Provide microphone permission resources for radio host tests 2026-10-29 13:34:22 +01:00
evgeny 81b9469302 Observe native radio capture and PTT commands in host tests 2026-10-29 13:34:22 +01:00
evgeny 4ff2f56aa8 Provide radio screen state and monotonic nanoseconds to host tests 2026-10-29 13:34:22 +01:00
evgeny 841d4ecdde Model radio screen receiver registration in host tests 2026-10-29 13:34:22 +01:00
evgeny 51611cbf5a Model radio playback without Android audio hardware 2026-10-29 13:34:22 +01:00
evgeny 9d6018cdbb Describe radio manual holds and independent speaker indication 2026-10-29 13:34:22 +01:00
evgeny 95894273f1 Document radio PTT ownership and talk event state contracts 2026-10-29 13:34:22 +01:00
evgeny 43b14ece78 Unify UDP and STCP packet link API with bounded backpressure 2026-10-29 12:53:57 +01:00
evgeny 3203947698 Add desktop log rotation on startup and size limit 2026-10-29 11:23:36 +01:00
evgeny ba1c4cf160 Add cross-platform BLE transport and Bluetooth settings
Integrate shared C transport with desktop and Android frontends, add radio probes and detailed Windows diagnostics, and fix GUI build dependencies and early configuration startup errors.
2026-10-29 10:51:19 +01:00
evgeny 221f58c9d2 Draw storage avatars as clear cylinders with curved bands 2026-10-28 17:58:40 +01:00
evgeny 54fdd167c2 Use server and storage role icons instead of member avatars 2026-10-28 17:49:24 +01:00
evgeny c96a63091c Replace supernode lightning with a server icon renderer 2026-10-28 17:48:54 +01:00
evgeny 2b05140e74 Verify actual route labels and local to peer NAT pairs in member details 2026-10-28 17:21:37 +01:00
evgeny faa75a7e1d Show actual member routes and both endpoints' NAT types in desktop details 2026-10-28 17:20:46 +01:00
evgeny 26edd84bb9 Enable MJPEG thumbnail encoding and refresh bundled FFmpeg during Windows builds 2026-10-28 15:56:18 +01:00
evgeny 19cece789e Remove local test databases from version control and ignore their directories 2026-10-28 15:38:52 +01:00
evgeny 9cabb698e2 Exercise cached media preparation in real history widgets across scrolling and reopening 2026-10-28 15:09:35 +01:00
evgeny ba44c31f42 Add focused cache and prepared media rendering checks to desktop tests 2026-10-28 15:06:43 +01:00
evgeny ed19adc1ba Cover persistent preview reuse, PNG alpha, first video frame, rotation and creation based expiry 2026-10-28 15:06:06 +01:00
evgeny 806632b67d Link the persistent preview cache and keep image preparation free of temporary files 2026-10-28 15:04:03 +01:00
evgeny 73b1fc7080 Render cached image and video previews without filesystem access or scaled pixmap allocation 2026-10-28 15:02:28 +01:00
evgeny 60d2ba0fac Prepare visible image and video previews through the shared background cache 2026-10-28 15:01:29 +01:00
evgeny 468af7abbe Add persistent media preview cache with fixed creation timestamps and seven day expiry 2026-10-28 14:57:54 +01:00
evgeny fc5ad51ec3 Clarify ringing phase precedes accepting the call 2026-10-28 14:43:27 +01:00
evgeny 6a82192b02 Document ringback and notification playback ownership contracts 2026-10-28 14:43:27 +01:00
evgeny 776345997f Keep notification players strongly owned throughout playback 2026-10-28 14:43:27 +01:00
evgeny b318627022 Require confirmed media release in existing alert lifecycle tests 2026-10-28 14:43:27 +01:00
evgeny 0b10a38f41 Retain failed media releases and retry without replacing ringback owners 2026-10-28 14:43:27 +01:00
evgeny 383da15d95 Allow absent callbacks after partial ringback listener setup 2026-10-28 14:43:27 +01:00
evgeny 2a4ccd8ca8 Reproduce lost ringback and notification players after release failures 2026-10-28 14:43:27 +01:00
evgeny 512a87502b Document ringtone resource ownership and cleanup retry contracts 2026-10-28 14:43:27 +01:00
evgeny 27b99435c0 Clarify alert stop diagnostics when sound cleanup remains pending 2026-10-28 14:43:27 +01:00
evgeny e45ddd2a7f Require confirmed cleanup in existing ringtone lifecycle tests 2026-10-28 14:43:27 +01:00
evgeny 94b9786d91 Retain ringtone resources and retry cleanup before replacement 2026-10-28 14:43:27 +01:00
evgeny 6abedd8177 Reproduce lost ringtone and focus owners after cleanup failures 2026-10-28 14:43:27 +01:00
evgeny cc9e4f4d85 Model rejected vibration scheduling and partial cancellation 2026-10-28 14:43:27 +01:00
evgeny 7ed1a63b36 Model recoverable alert playback and focus cleanup failures 2026-10-28 14:43:27 +01:00
evgeny edc6b32fa7 Cover orphan notification cleanup through language refresh 2026-10-28 14:43:27 +01:00
evgeny d67991d08a Avoid repeated alert cancellation after successful lifecycle cleanup 2026-10-28 14:43:27 +01:00
evgeny 61e013e762 Check lifecycle refresh does not repeat completed alert cleanup 2026-10-28 14:43:27 +01:00
evgeny 774284b699 Verify repeated cleanup across incoming alert lifecycle failures 2026-10-28 14:43:27 +01:00
evgeny 66e99066f9 Retain incoming alert resources until cleanup succeeds 2026-10-28 14:43:27 +01:00
evgeny 793c2b251b Reproduce lost incoming alert resources after cleanup exceptions 2026-10-28 14:43:27 +01:00
evgeny f4cfe5ba30 Model partial wake lock and notification cleanup failures 2026-10-28 14:43:27 +01:00
evgeny 3a6ef25ca6 Document requested locales and notification language refresh contracts 2026-10-28 14:43:27 +01:00
evgeny 7b8e0a95bb Document language host tests and their runtime boundaries 2026-10-28 14:43:27 +01:00
evgeny 3dfedc8822 Read back locale requests after partial framework failures 2026-10-28 14:43:27 +01:00
evgeny a541484df1 Expose language read and selection failures in the settings UI 2026-10-28 14:43:27 +01:00
evgeny caccb4e56f Add localized language selection and catalog error messages 2026-10-28 14:43:27 +01:00
evgeny 8537ac1e6d Include language lifecycle coverage in Android unit test runs 2026-10-28 14:43:27 +01:00
evgeny 4bffffb0ad Keep language tests compatible with an expanded XML catalog 2026-10-28 14:43:27 +01:00
evgeny 6133257c24 Validate language catalogs and framework failure contracts on the host 2026-10-28 14:43:27 +01:00
evgeny 8720cee7af Keep the headless service fixture separate from Android sources 2026-10-28 14:43:27 +01:00
evgeny fd183a43fe Retry notification language refreshes on every configuration signal 2026-10-28 14:43:27 +01:00
evgeny 5360ee9ff1 Reproduce skipped notification language retries and callback failures 2026-10-28 14:43:27 +01:00
evgeny 9369429600 Observe independent notification refresh attempts in language tests 2026-10-28 14:43:27 +01:00
evgeny 909761b0c5 Provide application and main thread boundaries for language tests 2026-10-28 14:43:27 +01:00
evgeny 638a2a2d6b Model locale resources and AppCompat failures for language tests 2026-10-28 14:43:27 +01:00
evgeny aae145b82a Prepare isolated host tests for application language behavior 2026-10-28 14:43:27 +01:00
evgeny 5e0ed8b077 Document battery exemption query and settings request contracts 2026-10-28 14:43:27 +01:00
evgeny 8551234cfb Show unknown battery exemption state with an explicit retry action 2026-10-28 14:43:27 +01:00
evgeny 0b31f27177 Preserve unknown battery exemption status on Android service errors 2026-10-28 14:43:27 +01:00
evgeny 04b76be1c8 Compile the battery permission adapter in Android host tests 2026-10-28 14:43:27 +01:00
evgeny ff08f05e9a Model power exemption queries and system activity launch failures 2026-10-28 14:43:27 +01:00
evgeny 4b221ef0d1 Verify growing call preview and emphasized metric values 2026-10-28 14:35:46 +01:00
evgeny 859f33b5e9 Expand call preview to the available window area and emphasize diagnostic values 2026-10-28 14:34:12 +01:00
evgeny b03d101c87 Check call diagnostics, avatar visibility and bottom action alignment 2026-10-28 14:20:36 +01:00
evgeny 309754244a Localize call diagnostics and keep video parameters in logs 2026-10-28 14:20:36 +01:00
evgeny b82e9cc98e Pin call actions to the bottom and add a separate diagnostics toggle 2026-10-28 14:19:14 +01:00
evgeny 264a13705f Finalize shared frame rate selection and deterministic call control verification 2026-10-28 14:10:53 +01:00
evgeny bb0fa13c8b Show audio output captions and validate the desktop call settings menus 2026-10-28 14:08:21 +01:00
evgeny 2540909fda Replace desktop call settings with compact capture control menus 2026-10-28 14:06:40 +01:00
evgeny f76aacfba1 Share resolution dependent frame rate menus for capture settings 2026-10-28 14:04:31 +01:00
evgeny f9c150b7d4 Share capture control menus and Android style glyphs between desktop windows 2026-10-28 14:02:39 +01:00
evgeny 7be5c8bd0b Verify rendered control sizes and muted stream startup 2026-10-28 13:57:05 +01:00
evgeny 287b54b283 Allow and remember stream audio toggling before publication starts 2026-10-28 13:55:45 +01:00
evgeny c932528217 Apply initial stream mute before opening capture devices 2026-10-28 13:55:15 +01:00
evgeny 26953fc1a4 Render broadcast controls at their actual icon size with Android style values and rotation 2026-10-28 13:55:14 +01:00
evgeny 01dd40fdd1 Double broadcast control icons and tighten button spacing 2026-10-28 13:47:09 +01:00
evgeny 07b8c2b338 Keep broadcast menu style alive through menu teardown and add English labels 2026-10-28 13:44:02 +01:00
evgeny 21ab92d2b4 Update broadcast control tests and Russian menu labels 2026-10-28 13:42:58 +01:00
evgeny 35875a2c43 Move desktop broadcast settings into compact top menus with separate resolution and FPS 2026-10-28 13:41:09 +01:00
evgeny 7d3f0852f7 Place video settings in the centered desktop call control row 2026-10-28 13:25:24 +01:00
evgeny f1da7e1837 Center desktop call actions and use a gear button for video settings 2026-10-28 13:17:19 +01:00
evgeny 75336387c5 Translate desktop video settings and camera toggles 2026-10-28 13:09:42 +01:00
evgeny 858e27ec8c Replace desktop call video checkbox with a round camera button 2026-10-28 13:09:42 +01:00
evgeny 270eb0c3b0 android: add photo and video capture orientation controls 2026-10-28 12:58:21 +01:00
evgeny 19a3cc8934 Merge remote-tracking branch 'origin/master' 2026-10-28 12:43:38 +01:00
evgeny f9ccca85bb Insert generated config fields before blank section separators 2026-10-28 12:41:10 +01:00
evgeny 41930a35c4 Audit configuration sample against current parser and service behavior 2026-10-28 12:36:21 +01:00
evgeny 2c82ceaad7 Handle nullable multicast lock creation before acquiring ownership 2026-10-28 12:28:28 +01:00
evgeny 3c5a9410ec Clarify retained LAN delivery state after core shutdown 2026-10-28 12:28:28 +01:00
evgeny 25dda68ecf Document LAN invitation framing and server link preview contracts 2026-10-28 12:28:28 +01:00
evgeny 237ba93e1a Report invitation presentation errors without exposing link secrets 2026-10-28 12:28:28 +01:00
evgeny 6f9ddc88e2 Add localized invitation copy errors and delivery recipient labels 2026-10-28 12:28:28 +01:00
evgeny f910d235fd Validate LAN events and retain multicast lock ownership on errors 2026-10-28 12:28:28 +01:00
evgeny 0be56a133b Reproduce LAN invitation state and multicast ownership failures 2026-10-28 12:28:28 +01:00
evgeny 0b8caaa810 Model LAN invitation JNI calls and multicast lock failures 2026-10-28 12:28:28 +01:00
evgeny e32e4be28b Document JNI log encoding and failure containment contracts 2026-10-28 12:28:28 +01:00
evgeny c137ad89d1 Clarify JNI log scenario coverage and callback descriptor checks 2026-10-28 12:28:28 +01:00
evgeny 1c705e64ca Verify repeated JNI delivery and log subscription changes 2026-10-28 12:28:28 +01:00
evgeny 269d09815b Document JNI host checks and their platform requirements 2026-10-28 12:28:28 +01:00
evgeny 0931eeedad Run JNI log fault scenarios with Android unit tests 2026-10-28 12:28:28 +01:00
evgeny a77fc553e7 Contain JNI logging failures and preserve UTF-8 payloads 2026-10-28 12:28:28 +01:00
evgeny c656e3a96b Reproduce Android JNI log callback failures on the host 2026-10-28 12:28:28 +01:00
evgeny 8268c5112c Describe Android log retention and diagnostic output contracts 2026-10-28 12:28:28 +01:00
evgeny 401e65c132 Read Android log snapshots without losing queued updates 2026-10-28 12:28:28 +01:00
evgeny 69cb7664f3 Update chatserver sample for generated addition key 2026-10-28 12:26:35 +01:00
evgeny 38d24e7bbf Document opt-in Android network logging 2026-10-28 12:08:07 +01:00
evgeny 0c2b96ee95 Add a logreceiver checkbox to Android debug settings 2026-10-28 12:08:07 +01:00
evgeny 0a36dc5ac6 Make Android UDP log forwarding opt-in with a persisted target 2026-10-28 12:08:07 +01:00
evgeny 917a2a2b28 Serialize stream capture preferences with other saved media profiles 2026-10-28 12:08:07 +01:00
evgeny 7ed8f90068 Keep call controls visible while a video menu is open 2026-10-28 12:08:07 +01:00
evgeny c0acb6b268 Remember separate photo and video recording preferences 2026-10-28 12:08:07 +01:00
evgeny c9f8f17858 Restore video call modes and remember the selected bitrate range 2026-10-28 12:08:07 +01:00
evgeny 76f81bc14e Store independent capture profiles in the application settings queue 2026-10-28 12:08:07 +01:00
evgeny 9f1c1cf9e4 Add titled one-tap menus to stream capture and audio controls 2026-10-28 12:08:07 +01:00
evgeny 211f8f4c5d Keep the video bitrate slider aligned with its saved range 2026-10-28 12:08:07 +01:00
evgeny c368b77a00 Unify video call controls and support one-tap option menus 2026-10-28 12:08:07 +01:00
evgeny ead884e6d3 Prefer the main rear camera in photo mode 2026-10-28 12:08:07 +01:00
evgeny 067a6bd801 Switch camera menus in one tap and toggle the current menu 2026-10-28 12:08:07 +01:00
evgeny 217a4bb27b Add descriptive headers to camera option menus 2026-10-28 12:08:07 +01:00
evgeny 93da173987 Default camera JPEG quality to 85 and remove lowest presets 2026-10-28 12:08:07 +01:00
evgeny a53668ade8 Show captured JPEG file size and enlarge camera metadata text 2026-10-28 12:08:07 +01:00
evgeny 07c9cf223c Hide camera discard and send actions until there is captured media 2026-10-28 12:08:07 +01:00
evgeny f351f289af Center camera actions and replace inactive shutter with a half-width gap 2026-10-28 12:08:07 +01:00
evgeny 22c8f9a69c Place compact photo and video details beside camera controls 2026-10-28 12:08:07 +01:00
evgeny 1359247e3f Describe recording codec checks and JPEG quality preview 2026-10-28 12:08:07 +01:00
evgeny c7b301b3a1 Document updated Android build stack and keep capture diagnostics compact 2026-10-28 12:08:07 +01:00
evgeny 03059e3001 Choose recording codec, resolution and bitrate independently and set JPEG quality 2026-10-28 12:08:07 +01:00
evgeny afa6af5012 Share compact bitrate labels between streaming and recording controls 2026-10-28 12:08:06 +01:00
evgeny aaf0617a1f Upgrade Android build tools and CameraX while retaining API 26 minimum 2026-10-28 12:08:06 +01:00
evgeny b84561dba5 Use shared capture icons and menus for camera and resolution selection 2026-10-28 12:08:06 +01:00
evgeny 379e7731df Align Android camera timer to the right of the control panel 2026-10-28 12:08:06 +01:00
evgeny 4aa5513f25 Keep Android camera settings and controls outside the preview 2026-10-28 12:08:06 +01:00
evgeny 10a4554c81 Localize desktop image validation errors 2026-10-28 12:08:06 +01:00
evgeny c84c328c90 Align camera navigation and developer references with shared capture screen 2026-10-28 12:08:06 +01:00
evgeny 4f5e46e9a9 Keep cache sources alive while accepted legacy image copies are pending 2026-10-28 12:08:06 +01:00
evgeny 2ac483f10f Document image attachment metadata and shared Android camera ownership 2026-10-28 12:08:06 +01:00
evgeny 199eaa0429 Verify desktop image previews decode asynchronously and paint without file I/O 2026-10-28 12:08:06 +01:00
evgeny 154c76068c Check encrypted image dimensions across the Android bridge 2026-10-28 12:08:06 +01:00
evgeny b3e7db7b26 Cover encrypted group images and fixed private generation in desktop submission tests 2026-10-28 12:08:06 +01:00
evgeny 206abcae10 Test typed desktop images in channels and PM and reject corrupted attachments 2026-10-28 12:08:06 +01:00
evgeny 3c74fc89fe Handle camera cache failures and remove unused photo submission wrapper 2026-10-28 12:08:06 +01:00
evgeny 684062c115 Test image metadata, private attachment publication and Android cache ownership 2026-10-28 12:08:06 +01:00
evgeny c6fdf52582 Decode desktop image previews in bounded background jobs 2026-10-28 12:08:06 +01:00
evgeny 08e9b62e16 Render image previews from prepared model data and preserve aspect ratios 2026-10-28 12:08:06 +01:00
evgeny 04d9d1f2b9 Connect shared camera and photo viewer to Android channel and PM navigation 2026-10-28 12:08:06 +01:00
evgeny 6e30fd474e Unify Android photo and video capture with explicit cache ownership 2026-10-28 12:08:06 +01:00
evgeny 8ff6694c5a Show bounded image previews and open zoomable photos in Android private chats 2026-10-28 12:08:06 +01:00
evgeny d1bb143928 Add localized image and camera error states 2026-10-28 12:08:06 +01:00
evgeny 4228f37cf5 Recognize image contents and preserve image dimensions in Android conversations 2026-10-28 12:08:06 +01:00
evgeny 233a3f78d1 Pass image MIME and private conversation generation through Android JNI 2026-10-28 12:08:06 +01:00
evgeny e4af3907c2 Prepare Android JPEG and PNG attachments for fixed channel or PM targets 2026-10-28 12:08:06 +01:00
evgeny 23fecc2c11 Send and display JPEG and PNG in desktop private conversations 2026-10-28 12:08:06 +01:00
evgeny e3b09bd0b7 Support JPEG and PNG attachment metadata and preparation 2026-10-28 12:08:06 +01:00
evgeny 78ea57d183 Clarify conversation rendering and preserve quote Unicode 2026-10-28 11:36:13 +01:00
evgeny 0b285ec19b Move the logs shortcut beside the CPU indicator 2026-10-28 11:36:13 +01:00
evgeny 8b3b6fb5af Reject malformed feedback events before updating UI state 2026-10-28 11:36:13 +01:00
evgeny 317f362a7a Reproduce malformed feedback event handling in the UI 2026-10-28 11:36:13 +01:00
evgeny a1ce486e89 Document feedback snapshots and poll UI submission contracts 2026-10-28 11:36:13 +01:00
evgeny 37515df8c1 Validate poll drafts and reject malformed feedback snapshots 2026-10-28 11:36:13 +01:00
evgeny 89efd10450 Reproduce invalid poll draft and inactive custom answer handling 2026-10-28 11:36:13 +01:00
evgeny f6b5e5b42c Route group avatar snapshots separately from feedback changes 2026-10-28 11:36:13 +01:00
evgeny d969b277e1 Assign a registered event ID to Android group avatar snapshots 2026-10-28 11:36:13 +01:00
evgeny e91987d241 Reproduce Android avatar and feedback event ID collision 2026-10-28 11:36:13 +01:00
evgeny 69bc182ee9 Clarify capture timeline and microphone permission contracts 2026-10-28 11:36:13 +01:00
evgeny c0c282ca3a Make voice recording ownership and playback failures explicit 2026-10-28 11:36:13 +01:00
evgeny 0219624c7d Reproduce voice capture rollback and playback cleanup failures 2026-10-28 11:36:13 +01:00
evgeny 537814e7fd Model microphone permission for voice recorder host tests 2026-10-28 11:36:13 +01:00
evgeny c31bdf74d8 Add host audio output controls for voice recorder failures 2026-10-28 11:36:13 +01:00
evgeny 5db51e64de Retry file resource cleanup when the core stops 2026-10-28 11:36:13 +01:00
evgeny a8c9bc6049 Document file browser lifecycle and export error handling 2026-10-28 11:36:13 +01:00
evgeny 0aa63c98ee Document Android file framing and error status contracts 2026-10-28 11:36:13 +01:00
evgeny 5290a2868d Preserve file wake lock ownership and isolate cleanup errors 2026-10-28 11:36:13 +01:00
evgeny 4f06b4bca9 Reproduce file cleanup failures and verify native cancellation 2026-10-28 11:36:13 +01:00
evgeny f60390c99c Model native file failures and wake lock ownership on host 2026-10-28 11:36:13 +01:00
evgeny 529237dfc8 Add host document providers for file failure tests 2026-10-28 11:36:13 +01:00
evgeny 84e4667f5c Describe avatar gallery and EXIF decoding behavior 2026-10-28 11:36:13 +01:00
evgeny 81f36d15c8 Clarify avatar editor ownership and UI publication state 2026-10-28 11:36:13 +01:00
evgeny 977992e834 Document Android avatar storage and publication contracts 2026-10-28 11:36:13 +01:00
evgeny 6f8baf82c7 Clarify Android playback recovery and diagnostics contracts 2026-10-28 11:36:13 +01:00
evgeny 00f8d8aeb0 Document Android call audio I/O and clock contracts 2026-10-28 11:36:13 +01:00
evgeny 7e502f9dd3 Verify headless server link commands and self-add rejection 2026-10-28 11:17:28 +01:00
evgeny 52dadf8b23 Document server link generation and channel addition commands 2026-10-28 11:13:34 +01:00
evgeny f1ff394f83 Document server authorization contract and cover cancellation lifecycle 2026-10-28 11:10:27 +01:00
evgeny bc296af99f Test server link presentation and authorization errors in clients 2026-10-28 11:07:58 +01:00
evgeny 3daacc07b7 Select any joined channel when opening an Android server link 2026-10-28 11:06:24 +01:00
evgeny f2d6561509 Route Android server links through channel selection and ordinary JOIN request 2026-10-28 11:05:23 +01:00
evgeny a9cc6785a9 Expose authorized server addition to Android JNI 2026-10-28 11:04:56 +01:00
evgeny f606c65858 Choose a local channel when opening a server link 2026-10-28 11:03:18 +01:00
evgeny 031b8b086b Accept server links in channel invitation dialog 2026-10-28 11:03:00 +01:00
evgeny 667cd1b302 Expose server link kind and authorization failures in Qt 2026-10-28 11:02:59 +01:00
evgeny 3c2c8978df Test authorized server JOIN over UDP and persistent addition keys 2026-10-28 11:01:35 +01:00
evgeny 3bc3e86529 Expose server links and channel addition through headless control 2026-10-28 10:59:16 +01:00
evgeny 7374b59c03 Authorize server join requests and run ordinary channel JOIN 2026-10-28 10:58:25 +01:00
evgeny 759a576a8d Build server links from shared codec and published endpoints 2026-10-28 10:56:09 +01:00
evgeny 0b6a882480 Persist generated server addition credentials in configuration 2026-10-28 10:55:50 +01:00
evgeny bd50140d26 Add explicit server and channel link kinds to shared codec 2026-10-28 10:55:25 +01:00
evgeny b773656dad Fix Windows entry point definitions in desktop GUI tests 2026-10-28 01:51:09 +01:00
evgeny 1ad00c12ad Fix attachment sending, media manifests and storage defaults 2026-10-28 01:47:17 +01:00
evgeny b86dcdd926 Fix member detail test bridge stubs after member editing API changes 2026-10-28 01:20:18 +01:00
evgeny 0ce248eb4a Configure recording compressor and provide a valid shutdown test identity 2026-10-28 00:59:00 +01:00
evgeny 65f39b6bae Test compressed recording rejection and ordered MainWindow shutdown 2026-10-28 00:59:00 +01:00
evgeny fee528194b Handle the tracked voice view during destruction and isolate test startup work 2026-10-28 00:59:00 +01:00
evgeny f624fd6a75 Clarify SQLite snapshot lifetime during desktop teardown 2026-10-28 00:59:00 +01:00
evgeny 15ca1047ce Cover desktop queue, attachment, DM, SQLite and media regressions 2026-10-28 00:59:00 +01:00
evgeny 9ee64d9a9f Extend desktop command regression suite and verify SQLite stopping handoff 2026-10-28 00:59:00 +01:00
evgeny 72f4258410 Replace stale desktop architecture documentation and honor the temporary directory 2026-10-28 00:59:00 +01:00
evgeny b4553db769 Reset selected GUI state during teardown and keep CHAT read watermarks separate from PM sequences 2026-10-28 00:59:00 +01:00
evgeny d5f5cf407a Finish desktop header ownership, state and model role contracts 2026-10-28 00:59:00 +01:00
evgeny 17772163cd Complete MainWindow and MessageList contracts for commands and snapshots 2026-10-28 00:59:00 +01:00
evgeny 7556695cc5 Guard desktop modal owners and remove obsolete message helpers 2026-10-28 00:59:00 +01:00
evgeny 8b2b68d383 Own feedback requests and fence poll dialogs to their channel and lifetime 2026-10-28 00:59:00 +01:00
evgeny d0bf402824 Document checked desktop snapshots and require matching media click gestures 2026-10-28 00:59:00 +01:00
evgeny 0942bf3d24 Use the running profile database directory for desktop media paths 2026-10-28 00:59:00 +01:00
evgeny b87050900c Deduplicate channel voice playback and resolve encrypted media snapshots 2026-10-28 00:59:00 +01:00
evgeny 6adfe5cf13 Centralize owned attachment downloads and key auto-play by channel and row 2026-10-28 00:59:00 +01:00
evgeny 0d6fd63e8f Return attachment metadata as checked SQLite snapshots 2026-10-28 00:59:00 +01:00
evgeny 034df2939c Reject incomplete DB reads and clock voice progress from PCM outside paint 2026-10-28 00:59:00 +01:00
evgeny 1235f770eb Fence deferred message actions, release voice views and flush state before shutdown 2026-10-28 00:59:00 +01:00
evgeny adefef675c Detach GUI SQLite readers before worker teardown 2026-10-28 00:59:00 +01:00
evgeny ce133a6acf Remove obsolete desktop DB sync and GUI writes and enforce read-only queries 2026-10-28 00:59:00 +01:00
evgeny f87ee63a1d Preserve failed composer drafts and cancel PM submissions with a result 2026-10-28 00:59:00 +01:00
evgeny 9d48821af2 Own message list requests and acknowledge PM text persistence 2026-10-28 00:59:00 +01:00
evgeny 4c1512cb78 Release unsubmitted media resources and own desktop channel and PM requests 2026-10-28 00:59:00 +01:00
evgeny 707938d503 Bind remaining desktop commands atomically and cancel owned attachments 2026-10-28 00:59:00 +01:00
evgeny d9abb1f025 Exercise both native owners throughout concurrent radio rebinding 2026-10-28 00:59:00 +01:00
evgeny 6063d3eb2c Include the chat service API in the radio command fixture 2026-10-28 00:59:00 +01:00
evgeny 27388c1137 Link the radio command GUI test to Windows COM support 2026-10-28 00:59:00 +01:00
evgeny bc78e0889a Route MainWindow radio invitations through owned commands and test them 2026-10-28 00:59:00 +01:00
evgeny 0f36248d1e Own and bind desktop radio call commands inside the bridge 2026-10-28 00:59:00 +01:00
evgeny 87b971569e Check command ownership independently of the uasync timer pool 2026-10-28 00:59:00 +01:00
evgeny 85172f3ac0 Include miniaudio types for the settings test audio backend 2026-10-28 00:59:00 +01:00
evgeny 096c613666 Own MainWindow UI state requests through the settings bridge 2026-10-28 00:59:00 +01:00
evgeny 95e59df5c7 Complete call window header contracts and align test data rows 2026-10-28 00:59:00 +01:00
evgeny fd4f092c9a Decline incoming calls when closing their window and verify teardown 2026-10-28 00:59:00 +01:00
evgeny 9ef188b0d3 Link call command tests against the camera and codec modules 2026-10-28 00:59:00 +01:00
evgeny de60f8fb15 Translate call command queue errors 2026-10-28 00:59:00 +01:00
evgeny e23a7e27eb Test call command ownership, allocation failures and core rebinding 2026-10-28 00:58:59 +01:00
evgeny f3d18a57b5 Use owned call start and audio failure hangup from the main window 2026-10-28 00:58:59 +01:00
evgeny 299c1b86f8 Send call window actions through the owned bridge API 2026-10-28 00:58:59 +01:00
evgeny 4b4f48963e Own desktop call commands and bind them atomically to the core 2026-10-28 00:58:59 +01:00
evgeny 138f6071b0 Guard capture-only image queue and test publication without Multimedia 2026-10-28 00:58:59 +01:00
evgeny fe7cd73646 Document asynchronous desktop stream publication and settings 2026-10-28 00:58:59 +01:00
evgeny cb95cf7d27 Document desktop file and live player window contracts 2026-10-28 00:58:59 +01:00
evgeny cfa53af3a3 Translate video playback seek and read errors 2026-10-28 00:58:59 +01:00
evgeny 71f5cc9c94 Preserve early seek position and document playback lifecycle 2026-10-28 00:58:59 +01:00
evgeny e371cc4dd9 Cover audio tail pause, replay, seeking and queued EOF callbacks 2026-10-28 00:58:59 +01:00
evgeny ab37a06dad Follow PCM node completion and keep seeking and replay consistent 2026-10-28 00:58:59 +01:00
evgeny 7fe90be7b2 Expose PCM drain state and cursor while file playback waits at EOF 2026-10-28 00:58:59 +01:00
evgeny d8109770a0 Finish file playback only after video presentation and PCM drain 2026-10-28 00:58:59 +01:00
evgeny 4118149dbc Reproduce file playback completion before queued media is presented 2026-10-28 00:58:59 +01:00
evgeny 7b3fe48657 Verify caption updates preserve continuous H264 and Opus playback 2026-10-28 00:58:59 +01:00
evgeny 5286388ada Refresh multiview names without recreating stream tiles 2026-10-28 00:58:59 +01:00
evgeny 3ff5ca7009 Update live player captions independently of playback state 2026-10-28 00:58:59 +01:00
evgeny 9d2ddcc496 Reproduce stale stream captions without restarting playback 2026-10-28 00:58:59 +01:00
evgeny debd3b45ef Clarify preview validity and publisher audio state responsibilities 2026-10-28 00:58:59 +01:00
evgeny a03abcaa02 Describe multiview tile lifetime, navigation and retry contracts 2026-10-28 00:58:59 +01:00
evgeny 1f0c7483e0 Describe stream bridge ownership, asynchronous requests and snapshots 2026-10-28 00:58:59 +01:00
evgeny 3960e28638 Document portal capture setup, frame lifetime and deferred shutdown 2026-10-28 00:58:59 +01:00
evgeny d9eab97dee Describe cursor overlay coordinates, thread ownership and outcomes 2026-10-28 00:58:59 +01:00
evgeny 615bdda36e Document QP sampling and atomic stream source snapshot parsing 2026-10-28 00:58:59 +01:00
evgeny ce4ff084cc Target transfer allocation faults after the file dialog closes 2026-10-28 00:58:59 +01:00
evgeny e05df14371 Keep file lifecycle test data isolated and suppress expected poll stalls 2026-10-28 00:58:59 +01:00
evgeny 07d9c17dec Check empty file cancellation and keep test capture semantics aligned 2026-10-28 00:58:59 +01:00
evgeny 0419c83ab0 Use file bridge cancellation without browser forwarding helpers 2026-10-28 00:58:59 +01:00
evgeny b04b262174 Cancel file batch commands directly through the owned bridge 2026-10-28 00:58:59 +01:00
evgeny 59cd456386 Allow cancellation of an empty file operation as a no-op 2026-10-28 00:58:59 +01:00
evgeny ba44e243a9 Verify file command success against the real storage service 2026-10-28 00:58:59 +01:00
evgeny 83f8b0cbe2 Cover file snapshots, cancellation ownership and browser retries 2026-10-28 00:58:59 +01:00
evgeny 656d64ae74 Adapt member file browser capture tests to snapshot APIs 2026-10-28 00:58:59 +01:00
evgeny bc90e88e72 Complete file browser operations when command posting fails 2026-10-28 00:58:59 +01:00
evgeny 7d789f68d4 Own desktop file commands and report cancellation through Qt 2026-10-28 00:58:59 +01:00
evgeny 142278d651 Reproduce cancelled and rejected desktop file commands 2026-10-28 00:58:59 +01:00
evgeny 7a4ce5d476 Clarify member file snapshots and browser helper contracts 2026-10-28 00:58:59 +01:00
evgeny 897da82b35 Document desktop member file batch and browser contracts 2026-10-28 00:58:59 +01:00
evgeny 8a831a0ed5 Remove deleted media blocks from the desktop module inventory 2026-10-28 00:58:59 +01:00
evgeny 5b07edcd98 Remove unused legacy media block reader and splitter 2026-10-28 00:58:59 +01:00
evgeny 4d2b59cb20 Disconnect unused legacy media blocks from desktop builds 2026-10-28 00:58:59 +01:00
evgeny b7ca74d61d Clarify link callback captures and their lifetime requirements 2026-10-28 00:58:59 +01:00
evgeny 25f0e21fb9 Document history selection state and delegate, link and scrolling contracts 2026-10-28 00:58:59 +01:00
evgeny b95fd63c46 Clarify the frozen attachment snapshot and shared submission diagnostics 2026-10-28 00:58:59 +01:00
evgeny 73a5366f1c Make regression callback cleanup safe after failed assertions 2026-10-28 00:58:59 +01:00
evgeny 58c2b5644d Translate the shared channel send failure message 2026-10-28 00:58:59 +01:00
evgeny 939b458c33 Check synchronous attachment handlers cannot redirect or access a deleted composer 2026-10-28 00:58:59 +01:00
evgeny 45485f4fbc Use one owned channel submission path and preserve the emitting context 2026-10-28 00:58:59 +01:00
evgeny 195b9c5afb Keep send fault injection and callback lifetimes isolated in tests 2026-10-28 00:58:59 +01:00
evgeny 48a6f03d78 Cover channel send success, retry, cancellation and allocation failures end to end 2026-10-28 00:58:59 +01:00
evgeny 143fd51791 Transfer channel text requests through the owned composer bridge 2026-10-28 00:58:59 +01:00
evgeny 4d18a7015d Report cancelled composer submissions before releasing their owned requests 2026-10-28 00:58:59 +01:00
evgeny 3c5e52a630 Keep ordinary channel drafts until their correlated save result 2026-10-28 00:58:59 +01:00
evgeny 9be3c4af48 Reproduce ordinary draft loss and missing cancellation acknowledgments 2026-10-28 00:58:59 +01:00
evgeny 88760510da Document composer acknowledgments, attachment ownership and context snapshots 2026-10-28 00:58:59 +01:00
evgeny cdaad39cf2 Verify corrected emoji insertion through the real composer connection 2026-10-28 00:58:59 +01:00
evgeny 214af09510 Correct emoji catalog mappings and log selected Unicode bytes 2026-10-28 00:58:59 +01:00
evgeny 5f90557a36 Reproduce emoji label mismatches through real picker selections 2026-10-28 00:58:59 +01:00
evgeny d0755477f2 Clarify parentless emoji panel sizing and tab icon drawing 2026-10-28 00:58:59 +01:00
evgeny b5d4c2be8b Describe category tab rendering and palette behavior 2026-10-28 00:58:59 +01:00
evgeny 62175e04a8 Document emoji catalog side effects and picker icon lifetimes 2026-10-28 00:58:59 +01:00
evgeny 8fddf854de Verify member save requests owner shutdown if rollback remains open 2026-10-28 00:58:59 +01:00
evgeny 1515a37b8a Verify fresh role snapshots and admin transfer cancellation ownership 2026-10-28 00:58:59 +01:00
evgeny b78fd7a8e6 Match editable roles to the same owner version read from SQLite 2026-10-28 00:58:59 +01:00
evgeny bc5e6aa5a5 Document cancellation acknowledgments for member edit commands 2026-10-28 00:58:59 +01:00
evgeny 8f02e1ec2d Read member edit capabilities and fields in one GUI snapshot 2026-10-28 00:58:59 +01:00
evgeny 84bf74e034 Limit commit fault injection to the member save transaction 2026-10-28 00:58:59 +01:00
evgeny f11ab51ccc Check stale forms, read failures and member command memory ownership 2026-10-28 00:58:59 +01:00
evgeny 49cbfe1ae0 Block property saves without a successfully read member snapshot 2026-10-28 00:58:59 +01:00
evgeny d73d62db48 Reject rename saves when the initial nickname read failed 2026-10-28 00:58:59 +01:00
evgeny 2d79023c5e Verify member save rollback across nickname SQL and owner changes 2026-10-28 00:58:59 +01:00
evgeny 279d7da51c Name member save status widgets for regression checks 2026-10-28 00:58:59 +01:00
evgeny f537f302c6 Exercise member dialogs against a real worker and SQLite failures 2026-10-28 00:58:59 +01:00
evgeny 5842855583 Confirm atomic member property saves before closing the dialog 2026-10-28 00:58:59 +01:00
evgeny 7e412d950a Bind owned admin transfer requests to the selected database 2026-10-28 00:58:59 +01:00
evgeny be11fbb8b6 Keep rename dialog open until the core confirms its write 2026-10-28 00:58:59 +01:00
evgeny 096e721faa Return correlated member save and cancellation results to live GUI contexts 2026-10-28 00:58:59 +01:00
evgeny 1431507ac5 Save member nicknames and owner changes in one core transaction 2026-10-28 00:58:59 +01:00
evgeny 30a15f6cfd Document audio settings snapshots and device test behavior 2026-10-28 00:58:58 +01:00
evgeny 005d136dc7 Make Android microphone capture stoppable and cleanup resilient
Use nonblocking AudioRecord reads with data and timestamp deadlines. Serialize idempotent close, finish interrupted joins before release, isolate callback failures, and abort stereo fallback when partial-record cleanup fails.

Close failed shared readers to new subscriptions, roll back worker startup, isolate observers, preserve interrupt state, and release the last consumer exactly once. Add an independent capture-tests JVM module to the standard Android unit-test task.

Validation: 38 capture scenarios (25 AudioRecord plus 13 PCM/PTT), 455 JVM tests, assembleDebug, core build, and full check.sh (225 tests plus proxy/burst/load) passed.
2026-10-28 00:53:01 +01:00
evgeny 74035fc043 Recover Android media audio sessions with explicit resource ownership
Retry monitor and capture setup after invalidating failed attempts. Bind output leases and callbacks to registration identity, cancel confirmation timers, and keep hardware cleanup independent from device enumeration during call handoff.

Commit profiles only after successful storage writes, acquire mode ownership before Android side effects, and reject blocking operations on main or the control worker. Keep the app singleton backed by an isolated MediaAudioSession with a single executor.

Validation: 30 media session scenarios, 420 JVM tests, assembleDebug, core build, and full check.sh (225 tests plus proxy/burst/load) passed.
2026-10-28 00:28:42 +01:00
evgeny a6ae4df3ff Harden Android audio router lifecycle and route selection
Roll back failed startup, finish independent stop cleanup, invalidate stale device and Telecom callbacks, and serialize router mutations. Publish input/output targets after accepted selection and support retries of rejected requests.

Separate modern communication device selection from legacy SCO, refresh replaced descriptors, and support A2DP and USB input-only devices in per-track mode. Clarify routing and observer ownership contracts.

Validation: 45 router scenarios, 390 JVM tests, assembleDebug, core build, and full check.sh (225 tests plus proxy/burst/load) passed. Previous router fails 25 of the 30 new cases.
2026-10-27 23:53:21 +01:00
evgeny 0d45bdbad6 Own Android incoming call alerts and notification actions
Bind notification tokens to full call IDs and call kinds, cancel actions on stop, and reject decline callbacks after ringing ends. Serialize alert state on main and recover notification channel setup after Android API failures.

Keep sound independent of wake-lock and notification failures. Remove orphaned notifications on cold start, preserve idempotent alerts during duplicate delivery and language changes, and document lifecycle contracts.

Validation: 32 new lifecycle cases, 360 JVM tests, assembleDebug, core build, and full check.sh (225 tests plus proxy/burst/load) passed.
2026-10-27 23:31:56 +01:00
evgeny 5a6eabf4fc Fix Android alert profiles and playback resource ownership
Configure audio attributes before MediaPlayer preparation, keep vibration independent from sound failures, and guard loop/focus cleanup against stale callbacks. Preserve ringback players on repeated phase requests.

Add 35 alert lifecycle tests against real Kotlin sources with controlled Android API failures. Validation: 328 JVM tests, assembleDebug, core build, and full check.sh (225 tests plus proxy/burst/load) passed. Previous sources fail 32 of the 35 new cases.
2026-10-27 23:12:11 +01:00
evgeny f4ef949482 Guard Android radio configuration with worker-owned tasks 2026-10-27 22:41:30 +01:00
evgeny f71aff3e11 Document Android radio automatic capture and background window contracts 2026-10-27 21:58:02 +01:00
evgeny d6dc6234d5 Recover SIM observers without losing healthy subscription state 2026-10-27 21:49:15 +01:00
evgeny 6a38eaa4eb Compile the real SIM monitor in the Android host tests 2026-10-27 21:39:26 +01:00
evgeny 6e6260ab29 Add controlled Telephony fixtures for Android SIM monitoring 2026-10-27 21:35:45 +01:00
evgeny 142cc1a7a1 Verify Android phone state publishing and JNI restart recovery 2026-10-27 21:30:07 +01:00
evgeny 4859d03f29 Return Android phone state queue acceptance through JNI 2026-10-27 21:25:51 +01:00
evgeny 067c1cd496 Own queued Android phone state data within the worker lifetime 2026-10-27 21:23:18 +01:00
evgeny 2b55d9225a Cover Android phone command cancellation and publication ownership 2026-10-27 21:21:52 +01:00
evgeny 9e679462d4 Expose Android phone state posting through the C bridge API 2026-10-27 21:18:29 +01:00
evgeny b92727e66a Make proximity observation safe across failures and restarts 2026-10-27 20:57:24 +01:00
evgeny 0d3ffb8736 Add a host sensor fixture for ProximityManager lifecycle checks 2026-10-27 20:54:40 +01:00
evgeny da85971969 Verify CoreActivity against Android JNI and native activity modes 2026-10-27 20:44:34 +01:00
evgeny ed1f4d8ea8 Preserve pending CoreActivity intent when the native queue rejects a request 2026-10-27 20:39:19 +01:00
evgeny f941066934 Cover rejected activity commands at the native queue boundary 2026-10-27 20:11:15 +01:00
evgeny 61266986e5 Route bridge activity requests through the worker owner 2026-10-27 20:10:00 +01:00
evgeny 4bece684da Queue Android activity updates while holding the worker lifetime lock 2026-10-27 20:07:51 +01:00
evgeny 1ed110d47c Validate CoreActivity ownership before mutations and cache completed JNI requests 2026-10-27 20:02:55 +01:00
evgeny b3b04b4d5a Add regressions for CoreActivity thread ownership and request caching 2026-10-27 19:53:53 +01:00
evgeny 29e6c977f2 Document Android worker cancellation and shutdown ownership 2026-10-27 19:30:29 +01:00
evgeny 64c1019a52 Verify cancelled Android workers retain callbacks and keep SQLite owned by the core 2026-10-27 19:30:29 +01:00
evgeny aca5f806c1 Keep worker callbacks atomic and verify failed shutdown preserves owners 2026-10-27 19:16:30 +01:00
evgeny 823bae076f Run Android service shutdown off the main thread and await it before startup 2026-10-27 19:07:05 +01:00
evgeny d6ea893f16 Preserve bridge callbacks until native worker shutdown succeeds 2026-10-27 18:57:36 +01:00
evgeny d83d58c874 Keep Android shutdown independent of a retiring service 2026-10-27 18:55:29 +01:00
evgeny 1c82432de9 Own Android worker threads until cleanup and join finish 2026-10-27 18:52:46 +01:00
evgeny 69b87ef3ed Verify Android startup readiness with the main looper and native worker 2026-10-27 18:33:49 +01:00
evgeny 11b3ceaa9b Wait for Android core readiness before applying settings 2026-10-27 18:27:14 +01:00
evgeny 8e0090b54b Track Android startup until native core readiness is confirmed 2026-10-27 18:20:48 +01:00
evgeny a09b67a70f Fix the key regeneration job type in ChatViewModel 2026-10-27 18:10:09 +01:00
evgeny 848247bde6 Exercise Android identity regeneration through JNI on devices 2026-10-27 18:08:29 +01:00
evgeny 50ea132c15 Recreate the Android core when regenerating its identity 2026-10-27 18:05:06 +01:00
evgeny 2ff51d0016 Persist Android identities atomically before updating config 2026-10-27 17:51:41 +01:00
evgeny 02ec6a50a5 Verify config saves survive cancellation of their submitting scope 2026-10-27 17:35:26 +01:00
evgeny 44f6d8a5bc Verify partial node reads and reject malformed address rows 2026-10-27 17:27:04 +01:00
evgeny 30a205340a Build node diagnostics from one checked SQLite snapshot 2026-10-27 17:27:04 +01:00
evgeny dc1f3260e3 Add node page snapshot and SQL failure regressions 2026-10-27 17:27:04 +01:00
evgeny d582f2ea77 Clarify settings dialog drafts, save semantics and borrowed dependencies 2026-10-27 17:27:04 +01:00
evgeny 2dd0970295 Document storage, network and database settings page contracts 2026-10-27 17:27:04 +01:00
evgeny 65c6bbc03b Distinguish optimize SQL completion from integrity check results 2026-10-27 17:27:04 +01:00
evgeny 6ba86bb1df Add SQLite and Qt database maintenance regression tests 2026-10-27 17:27:04 +01:00
evgeny 5233513fcc Remove unused settings test scaffolding 2026-10-27 17:27:04 +01:00
evgeny 8323be7272 Cover settings string limits, snapshots and late allocation failures 2026-10-27 17:27:04 +01:00
evgeny e3b0eebc2b Eliminate borrowed save commands from the desktop settings dialog 2026-10-27 17:27:04 +01:00
evgeny 4d3b294ee9 Remove borrowed audio settings commands and duplicated serialization 2026-10-27 17:27:04 +01:00
evgeny a1f925c789 Transfer storage settings snapshots through the owned bridge API 2026-10-27 17:27:04 +01:00
evgeny f1f5d33788 Centralize owned settings and name requests in the GUI bridge 2026-10-27 17:27:04 +01:00
evgeny 05b0ef50e4 Add real desktop settings save ownership regression tests 2026-10-27 17:27:04 +01:00
evgeny 5382b686e9 Keep config snapshots and network diagnostics precise 2026-10-27 17:18:13 +01:00
evgeny 893e714422 Show a specific error when database reset cannot finish 2026-10-27 17:18:13 +01:00
evgeny 3b42547740 Report native Android database cleanup failures accurately 2026-10-27 17:18:13 +01:00
evgeny b8e7b18937 Serialize Android config transactions and await them through UI lifecycles 2026-10-27 17:18:13 +01:00
evgeny 3ba581770c Build Android configuration from one preferences and key snapshot 2026-10-27 17:18:13 +01:00
evgeny 482026e208 Add an application-owned queue for atomic Android config writes 2026-10-27 17:18:13 +01:00
evgeny 78f34a3617 Clarify stream resource and voice route ownership wording 2026-10-27 15:25:33 +01:00
evgeny 4dc02d74b8 Document sound manager recovery, session ownership and callback rules 2026-10-27 15:23:38 +01:00
evgeny 027a4b4d9e Clarify desktop voice recording and playback contracts 2026-10-27 15:21:31 +01:00
evgeny 22460a1259 Merge remote-tracking branch 'origin/master' 2026-10-27 15:18:21 +01:00
evgeny 8d8fe8d6bf Fix sound settings request ownership on rejection and cancellation 2026-10-27 15:13:26 +01:00
evgeny 821f1b8a92 Add sound settings request ownership regressions 2026-10-27 15:11:22 +01:00
evgeny 6d925fecd1 Serialize Android network snapshots across monitor and core lifecycles 2026-10-27 15:04:27 +01:00
evgeny e3aa7839ae Include socket refresh monitoring in isolated host tests 2026-10-27 15:04:27 +01:00
evgeny 97a899c0c3 Model delayed network callbacks and config workers in host tests 2026-10-27 15:04:27 +01:00
evgeny 7b526ba681 Own Android network activity registrations across start and stop 2026-10-27 15:04:27 +01:00
evgeny f89c1975f7 Expose network activity callbacks to the isolated Android host tests 2026-10-27 15:04:27 +01:00
evgeny 8a22afc42d Add controlled host fixtures for Android network listeners 2026-10-27 15:04:27 +01:00
evgeny a6ab797659 Document audio device lifetime and voice worker contracts 2026-10-27 14:47:59 +01:00
evgeny eab9223f39 Clarify audio queue, capture clock and PCM reserve contracts 2026-10-27 14:43:24 +01:00
evgeny 799a71d361 Merge remote-tracking branch 'origin/master' 2026-10-27 14:32:36 +01:00
evgeny c7dadb0539 Clarify optional audio period and PTT latch release wording 2026-10-27 14:31:31 +01:00
evgeny 4389b511bf Align audio ownership comments with lease and recovery behavior 2026-10-27 14:28:02 +01:00
evgeny 29e2afc3ce Describe shared audio capture and buffer tuning contracts 2026-10-27 14:27:20 +01:00
evgeny cb49b9d4a4 Complete desktop radio UI and audio engine contracts 2026-10-27 14:24:46 +01:00
evgeny fb461cd56c Keep merged message link range documentation concise 2026-10-27 14:23:59 +01:00
evgeny 7f0c080492 Validate saved Android socket ports before reuse 2026-10-27 14:22:32 +01:00
evgeny 080c7a5fd5 Clarify Android auto socket identity and port reuse contracts 2026-10-27 14:22:32 +01:00
evgeny 0fe14699b0 Document Android conversation targets and test message ID conversion 2026-10-27 14:22:32 +01:00
evgeny 3d48024fd8 Trim desktop message link punctuation in linear time 2026-10-27 14:22:32 +01:00
evgeny f04637a257 Keep Android message link parsing linear and validate authority 2026-10-27 14:22:32 +01:00
evgeny 3e96e429eb Preserve unsigned node IDs in Android invite previews
Encode JNI node IDs as decimal strings and decode both uint64 fields without floating point or signed parsing. Compare all invitation fields and binary contents. Cover boundary IDs, malformed JSON types, and model equality with nine JVM tests. Native build, invite codec/build tests, and both debug APK ABIs passed.
2026-10-27 14:22:32 +01:00
evgeny 254029be63 Merge remote-tracking branch 'origin/master' 2026-10-27 14:19:09 +01:00
evgeny b5063f435b Explain radio settings reload and publication ordering 2026-10-27 14:12:49 +01:00
evgeny 0bac2566c9 Merge remote-tracking branch 'origin/master' 2026-10-27 14:11:47 +01:00
evgeny 8a96e5d1c7 Verify radio settings order during reentrant reload 2026-10-27 14:10:53 +01:00
evgeny 2226f54a56 Invalidate stale radio settings requests on reload 2026-10-27 14:08:22 +01:00
evgeny b4186dfae3 Cover radio settings recovery across core restarts 2026-10-27 14:06:05 +01:00
evgeny 0132a984dd Describe bounded control flow and corrected monitor behavior 2026-10-27 14:01:25 +01:00
evgeny cb50e268a4 Update monitor usage and asynchronous TCP architecture docs 2026-10-27 14:00:49 +01:00
evgeny 89c27b8fca Clarify control Action replies and graph display contracts 2026-10-27 14:00:49 +01:00
evgeny 4daa0e7f6b Document monitor TX ownership, deadlines and subscription bounds 2026-10-27 13:59:42 +01:00
evgeny 9e0cd3eddf Test control backpressure, snapshot mutations and deferred cleanup 2026-10-27 13:56:03 +01:00
evgeny 5c2ba74e3e Document desktop PTT chords and portal lifecycle 2026-10-27 13:54:55 +01:00
evgeny ae594e1e1b Test bounded monitor TCP sends and graph reconnect behavior 2026-10-27 13:47:41 +01:00
evgeny 488d8a601f Apply control TCP backpressure and stream node snapshots by ID 2026-10-27 13:46:01 +01:00
evgeny e9391c9790 Clarify desktop video modes and encoder output contracts 2026-10-27 13:43:00 +01:00
evgeny b1510ab437 Describe camera worker lifecycle and capture signals 2026-10-27 13:39:12 +01:00
evgeny ce06fe242b Queue monitor TCP commands and validate matched responses 2026-10-27 13:38:40 +01:00
evgeny 0594be9513 Document camera IPC framing and shared buffer ownership 2026-10-27 13:38:07 +01:00
evgeny 2d12dfb975 Correct monitor graph units and refresh cleared channel values 2026-10-27 13:34:04 +01:00
evgeny 4f27ce35a5 Fix control Action acknowledgements and validate command payloads 2026-10-27 13:33:13 +01:00
evgeny 6fdba59885 Correct cache cleanup glob documentation 2026-10-27 13:32:12 +01:00
evgeny 2d255afa6c Document avatar manager jobs and Lights Out behavior 2026-10-27 13:29:54 +01:00
evgeny 8691608924 Clarify avatar cache and image viewer contracts 2026-10-27 13:27:55 +01:00
evgeny fd80b35f8f Merge remote-tracking branch 'origin/master' 2026-10-27 13:17:50 +01:00
evgeny 7eaeb0e2f9 Preserve documentation audit and Android bridge fixes 2026-10-27 13:17:22 +01:00
evgeny ae4fb89bef Keep validation errors visible after explicit join attempts 2026-10-27 12:57:20 +01:00
evgeny d730dee925 Use accurate shared join errors and preserve result status 2026-10-27 12:56:13 +01:00
evgeny fcbec01c07 Translate shared join and offer failure messages 2026-10-27 12:55:13 +01:00
evgeny 16a68652ae Add UI regression tests for join and offer result errors 2026-10-27 12:53:47 +01:00
evgeny eca03747b3 Log desktop join and offer results with request context 2026-10-27 12:53:21 +01:00
evgeny b62ca4093e Report precise avatar editor source validation failures 2026-10-27 12:30:21 +01:00
evgeny 6c1b429e91 Check avatar editor coordinates after EXIF rotation 2026-10-27 12:29:20 +01:00
evgeny d72a2c092e Log avatar preparation generation and teardown state 2026-10-27 12:28:12 +01:00
evgeny 90d6f78a9e Resolve Qt 5 collection deduction in avatar and settings code 2026-10-27 12:26:58 +01:00
evgeny f54aa17e55 Observe buttonless pointer moves to cancel interrupted crop drags 2026-10-27 12:25:03 +01:00
evgeny 803d5f6583 Trace crop pointer state when a drag loses its button 2026-10-27 12:23:12 +01:00
evgeny 8c30c8e6ce Isolate avatar preparation generations and guard reentrant signals 2026-10-27 12:19:31 +01:00
evgeny d307492d22 Cover avatar preparation cancellation and reentrant owner deletion 2026-10-27 12:17:06 +01:00
evgeny b95d3628bf Check crop preview rendering and interrupted drags 2026-10-27 12:15:17 +01:00
evgeny 0a69864d10 Guard avatar upload controls across nested file and crop dialogs 2026-10-27 12:15:17 +01:00
evgeny 9c782be562 Bound avatar crop geometry and protect asynchronous preview lifetime 2026-10-27 12:10:36 +01:00
evgeny bfc280310d Cover avatar crop input bounds and modal owner lifetime 2026-10-27 12:07:36 +01:00
evgeny c25ed7c243 Report readable causes for malformed avatar blobs 2026-10-27 11:57:30 +01:00
evgeny 138de8d446 Document retryable avatar snapshot read failures 2026-10-27 11:57:30 +01:00
evgeny 90a588cfc1 Separate unpublished avatars from malformed published storage 2026-10-27 11:57:30 +01:00
evgeny 7d4da3a54d Treat an unpublished avatar as a successful empty read 2026-10-27 11:57:30 +01:00
evgeny a701580d54 Exercise main window destruction inside channel settings exec 2026-10-27 11:57:30 +01:00
evgeny bd105c630d Validate avatar read recovery and bounded typed blobs 2026-10-27 11:57:30 +01:00
evgeny 6106fc1b5d Keep transient SQLite failures out of the avatar cache 2026-10-27 11:57:30 +01:00
evgeny 905d30ceaf Distinguish failed avatar reads from committed empty blobs 2026-10-27 11:57:30 +01:00
evgeny 95e88ac66f Reproduce avatar cache poisoning after a transient SQL read error 2026-10-27 11:57:30 +01:00
evgeny 9fdd2dc9fb Specify the native channel ID range in GUI contracts 2026-10-27 11:57:30 +01:00
evgeny d4a969dc38 Match channel IDs to the native signed integer range 2026-10-27 11:57:30 +01:00
evgeny cf4a209837 Own group avatar commands and preserve pending result identity 2026-10-27 11:57:30 +01:00
evgeny 6c0aa98508 Cover group avatar publication ownership and result correlation 2026-10-27 11:57:30 +01:00
evgeny ddb373d445 Exercise settings retries and deletion changes during confirmation 2026-10-27 11:57:30 +01:00
evgeny cba3f4f75a Translate channel settings availability and queue errors 2026-10-27 11:57:30 +01:00
evgeny 3c6200f9c3 Keep channel settings alive safely across the main window loop 2026-10-27 11:57:30 +01:00
evgeny 74bcf91407 Own channel settings requests and guard nested dialogs 2026-10-27 11:57:30 +01:00
evgeny 271cfcd2f8 Cover channel settings queue failures and nested dialog lifetime 2026-10-27 11:57:30 +01:00
evgeny f12c7d072a test(chat): read complete media manifests with JSON whitespace 2026-10-27 11:48:09 +01:00
evgeny 9b24c1f012 fix(chat): preserve media and local state on incomplete database reads 2026-10-27 11:48:09 +01:00
evgeny 7c050eb7ef fix(chat): publish complete peer caches and clean pending JOIN state 2026-10-27 11:48:09 +01:00
evgeny ce3287ac94 fix(chat): verify incoming JOIN profiles before local insertion 2026-10-27 11:48:09 +01:00
evgeny 054ade0dcd fix(chat): select invite peers and read signed addresses consistently 2026-10-27 11:48:09 +01:00
evgeny 1efd8fb26c fix(chat): validate headless requests and complete media replies safely 2026-10-27 11:48:09 +01:00
evgeny c73090759f fix(chat): commit global names with signed node and member profiles 2026-10-27 11:48:09 +01:00
evgeny 0656f24a7c fix(chat): own Whisper jobs per instance and defer route UI events 2026-10-27 11:48:09 +01:00
evgeny cb9c04ff61 test(chat): distinguish route event deletion flag from routing API 2026-10-27 11:48:09 +01:00
evgeny 33556fefea test(chat): exercise channel deletion from a topology notification 2026-10-27 11:48:09 +01:00
evgeny 189ae43a7c docs(chat): distinguish committed core readiness from complete service startup 2026-10-27 11:48:09 +01:00
evgeny a4edd34695 fix(chat): retain SQL error codes when SQLite supplies no message 2026-10-27 11:48:09 +01:00
evgeny e28386c868 fix(chat): reject outer transactions before preparing shared service resources 2026-10-27 11:48:09 +01:00
evgeny 8151fc5a9c test(chat): cover startup admission, saved channel activation, and owner-close recovery 2026-10-27 11:48:09 +01:00
evgeny 1c6d203754 fix(chat): diagnose channel catalog failures with SQL status and shape 2026-10-27 11:48:09 +01:00
evgeny fb7846780b test(chat): verify startup metadata rollback and required callback allocation failures 2026-10-27 11:48:09 +01:00
evgeny 3b0ae79527 refactor(chat): keep the chat identity bound to its core instance 2026-10-27 11:48:09 +01:00
evgeny 6c777d4426 fix(chat): reject inconsistent repeated core initialization 2026-10-27 11:48:09 +01:00
evgeny 63406a9742 refactor(chat): borrow SQLite exclusively from the running core 2026-10-27 11:48:09 +01:00
evgeny 6f9723f8ce fix(chat): commit startup metadata before activating channels and publishing readiness 2026-10-27 11:48:09 +01:00
evgeny e3bd66f0b7 test(chat): reproduce ready startup after a required subscription fails 2026-10-27 11:48:09 +01:00
evgeny ed00f1ad9a fix(chat): require both member notification subscriptions at startup 2026-10-27 11:48:09 +01:00
evgeny 1aface994f fix(routing): report nodeinfo subscription allocation failure 2026-10-27 11:48:09 +01:00
evgeny 6429d4ca23 Merge desktop audit with updated chat contracts
Keep the complete member snapshot limit at 65535 and preserve SQL finalization checks, unsigned sorting, and owned GUI commands. Retain dynamic ETCP diagnostics with checked event lengths and no partial event on allocation failure. Validated three native and six GUI test targets and rebuilt vibechat.
2026-10-27 11:12:51 +01:00
evgeny c7618ba62b Limit group autoconnect indicators to group rows 2026-10-27 10:59:43 +01:00
evgeny 20ffa3fc6c Include the channel event byte type explicitly 2026-10-27 10:59:01 +01:00
evgeny 842083cdeb Exercise valid and stale channel menu actions 2026-10-27 10:57:21 +01:00
evgeny 1e3633ee56 Document channel list lifetime and validate row counters 2026-10-27 10:56:43 +01:00
evgeny 8552cf8316 Update isolated channel consumers for owned bridge requests 2026-10-27 10:56:43 +01:00
evgeny abc2f8efb6 Preserve channel state and own presence requests safely 2026-10-27 10:55:11 +01:00
evgeny 4d4fafc00b Isolate channel request cancellation from core startup tasks 2026-10-27 10:54:26 +01:00
evgeny 50fff66412 Cover channel reload state and pending request ownership 2026-10-27 10:53:23 +01:00
evgeny f81f6299b4 Bound channel row painting and support pixel-sized fonts 2026-10-27 10:51:20 +01:00
evgeny 282bf99d4a Cover channel delegate paint bounds and pixel fonts 2026-10-27 10:50:18 +01:00
evgeny 2822fa53e3 Clarify member snapshot output and boundary test contracts 2026-10-27 10:44:53 +01:00
evgeny 6104086d5b Bound member snapshot events and sort roster without quadratic copies 2026-10-27 10:37:33 +01:00
evgeny 34a95b7733 Route member diagnostics safely across panels and modal actions 2026-10-27 10:20:07 +01:00
evgeny a8958865bc Keep member rows consistent and own queued refresh requests 2026-10-27 09:48:38 +01:00
evgeny 6390c5db6a docs(chat): record preference full suite and GUI persistence validation 2026-10-27 07:37:41 +01:00
evgeny ee37cf0a55 docs(chat): record committed preference behavior and regression evidence 2026-10-27 07:34:44 +01:00
evgeny 655733e272 docs(chat): define preference return codes and bounded trampoline inputs 2026-10-27 07:33:08 +01:00
evgeny 89e2a2f22b test(chat): reject a valid signed profile stored with the wrong SQLite type 2026-10-27 07:31:16 +01:00
evgeny 9342795008 fix(chat): check stored text types before SQLite converts their values 2026-10-27 07:31:16 +01:00
evgeny fe5cf910e1 test(chat): cover preference read errors and all queued sound form values 2026-10-27 07:29:48 +01:00
evgeny 09e3ba62ef test(chat): verify preference rollback, committed sound delivery, and request validation 2026-10-27 07:27:44 +01:00
evgeny fdb0381f5c fix(gui): retain sound preference keys until the queued request is copied 2026-10-27 07:25:26 +01:00
evgeny 81fdbf90e1 fix(chat): skip initial sound publication when preference reading fails 2026-10-27 07:25:26 +01:00
evgeny fec2cecdb3 fix(chat): commit UI preferences before reporting or broadcasting them 2026-10-27 07:24:30 +01:00
evgeny 5360453960 test(chat): reproduce committed UI state after a finalization error 2026-10-27 07:22:45 +01:00
evgeny 34e729d988 docs(chat): record owner editor fault matrix and cross-platform validation 2026-10-27 07:21:26 +01:00
evgeny 11135283e7 docs(chat): document canonical owner versions and atomic editor behavior 2026-10-27 07:17:23 +01:00
evgeny c8e1d120ba fix(android): include the owner block size contract for role parsing 2026-10-27 07:16:37 +01:00
evgeny 104b702653 test(chat): inject owner editor allocation failures before commit 2026-10-27 07:15:15 +01:00
evgeny be311cf144 test(chat): include the member display contract explicitly 2026-10-27 07:13:41 +01:00
evgeny 9e10f7a357 test(chat): cover long role histories and deletion from owner commit callbacks 2026-10-27 07:13:19 +01:00
evgeny 6224c017da fix(chat): reject truncated or malformed stored owner JSON before comparison 2026-10-27 07:13:19 +01:00
evgeny a14fdf6d83 test(chat): cover owner edit conflicts, retry isolation, histories, and GUI patches 2026-10-27 07:12:13 +01:00
evgeny 98741e47d8 docs(chat): define owner editor conflict, history, and patch contracts 2026-10-27 07:09:50 +01:00
evgeny e3ea2ef78f fix(chat): preserve unedited GUI tags and read complete role histories 2026-10-27 07:08:33 +01:00
evgeny b7f799ebe5 test(chat): target the complete owner snapshot query 2026-10-27 07:07:30 +01:00
evgeny cde8c7b8e5 fix(chat): commit owner edits against a locked baseline and retain retry state 2026-10-27 07:06:09 +01:00
evgeny a9618169ab fix(chat): load complete verified owner snapshots and validate editor input 2026-10-27 07:04:28 +01:00
evgeny 16d7fc4fb5 fix(chat): validate owner versions and complete member reads before mutation 2026-10-27 07:02:44 +01:00
evgeny 91fb996a09 test(chat): reproduce owner editor load after SQL finalization error 2026-10-27 07:02:03 +01:00
evgeny f0d864bfd9 docs(chat): record profile full suite and sanitizer fixture correction 2026-10-27 07:00:13 +01:00
evgeny 59d8065fae test(chat): clear borrowed fault selector before its scope ends 2026-10-27 06:58:14 +01:00
evgeny 247952e9ed docs(chat): record signed profile transactions and callback fault coverage 2026-10-27 06:56:56 +01:00
evgeny 8bf09d9513 docs(sqlite): clarify verified node and owner write failures 2026-10-27 06:55:54 +01:00
evgeny f66c4e928e fix(member-files): preserve the original lookup error in ACL diagnostics 2026-10-27 06:50:02 +01:00
evgeny cce633b5b0 test(chat): reject negative owner results before checking change flags 2026-10-27 06:48:29 +01:00
evgeny beac8afba8 test(chat): cover profile trust, owner rollback, avatar policy, and startup failure 2026-10-27 06:46:15 +01:00
evgeny ecb589c670 test(chat): exercise atomic profile updates, no-ops, and callback deletion 2026-10-27 06:42:32 +01:00
evgeny ebb89e1bc3 test(chat): add profile transaction faults and committed notification observer 2026-10-27 06:39:57 +01:00
evgeny 70a4c669dc test(chat): verify nested member callback cancellation and namespace lifetime 2026-10-27 06:37:09 +01:00
evgeny f1d80922c7 fix(chat): fail startup when the JOIN apply subscription cannot be installed 2026-10-27 06:35:37 +01:00
evgeny 64adb799e9 fix(chat): retain member notification data across callback cancellation 2026-10-27 06:35:37 +01:00
evgeny 6efe3000d0 fix(chat): verify existing JOIN before signing local profile updates 2026-10-27 06:27:01 +01:00
evgeny 3195167ff3 fix(chat): commit signed profile updates under one version lock 2026-10-27 06:23:48 +01:00
evgeny 1cc60a3079 fix(json): decode Unicode escapes without losing subsequent profile fields 2026-10-27 06:21:05 +01:00
evgeny 20c095f2f3 fix(chat): preserve member transaction boundaries when rollback fails 2026-10-27 06:15:20 +01:00
evgeny b52e5f5489 fix(chat): validate complete signed profile before publishing read output 2026-10-27 06:13:12 +01:00
evgeny 30d58a4bde test(chat): reproduce successful signed profile read after finalization error 2026-10-27 06:12:04 +01:00
evgeny 1843a492bc docs(chat): record full JSON export validation 2026-10-27 06:07:29 +01:00
evgeny 463041a87f docs(chat): describe complete JSON exports and listing failure tests 2026-10-27 06:06:55 +01:00
evgeny cb0b71aec1 test(chat): roundtrip every ASCII control byte in channel JSON 2026-10-27 06:00:15 +01:00
evgeny f5270db52b test(chat): verify lossless JSON, corrupted rows, and deferred channel visibility 2026-10-27 05:58:08 +01:00
evgeny 92317bf383 test(chat): add JSON SQL faults and exact-capacity contract checks 2026-10-27 05:56:46 +01:00
evgeny 29e15254a1 fix(chat): reject truncated SQLite text in exported JSON 2026-10-27 05:53:11 +01:00
evgeny d5070a5df6 fix(chat): return complete escaped JSON and propagate listing SQL failures 2026-10-27 05:51:26 +01:00
evgeny 5fe66585b6 test(chat): reproduce invalid JSON for a signed channel name 2026-10-27 05:49:58 +01:00
evgeny 774fb66bb9 docs(chat): record full member and diagnostics validation 2026-10-27 05:46:06 +01:00
evgeny cfe4c24584 docs(chat): record member, mute, and diagnostics audit evidence 2026-10-27 05:42:28 +01:00
evgeny cec3e27aab test(chat): make diagnostic name expectation explicit in member fixture 2026-10-27 05:40:10 +01:00
evgeny 5af4572ae1 test(chat): verify invalid requests and diagnostic name lookup failures 2026-10-27 05:39:05 +01:00
evgeny 618c68268c docs(chat): specify complete member snapshots and committed local mute 2026-10-27 05:38:07 +01:00
evgeny 41b3503398 fix(chat): bound diagnostic counters and invalid mute request parsing 2026-10-27 05:37:05 +01:00
evgeny fd4c13f219 test(chat): give diagnostic links a valid empty ACK queue 2026-10-27 05:35:27 +01:00
evgeny a9bc410aad test(chat): include complete ETCP types for diagnostic fixture 2026-10-27 05:34:32 +01:00
evgeny 7279967a29 test(chat): cover member limits, radio commits, and large diagnostics 2026-10-27 05:33:33 +01:00
evgeny 8de9cfb1d2 fix(chat): build complete diagnostics with checked buffers and SQL results 2026-10-27 05:31:19 +01:00
evgeny 2504dd0b40 test(chat): add member SQL and committed radio state observers 2026-10-27 05:29:34 +01:00
evgeny 1bb824fb8a fix(chat): commit radio mute before applying cache and reporting success 2026-10-27 05:24:47 +01:00
evgeny 32a8c1c48b fix(chat): publish complete member snapshots after checked SQL finalization 2026-10-27 05:23:17 +01:00
evgeny 958feb4e75 test(chat): reproduce successful member snapshot after finalization failure 2026-10-27 05:22:09 +01:00
evgeny d3c5565c91 docs(chat): record full avatar suite and sanitizer results 2026-10-27 05:20:30 +01:00
evgeny 63c92840e2 docs(chat): mark avatar audit fixes complete with targeted verification 2026-10-27 05:19:14 +01:00
evgeny 32c89c7110 docs(chat): record avatar failure reproduction and transaction validation 2026-10-27 05:16:43 +01:00
evgeny ae74b621c8 docs(chat): specify avatar transaction, failure, and callback lifetime contracts 2026-10-27 05:15:12 +01:00
evgeny 2c691f98f7 test(chat): include EVP declarations explicitly in avatar SQL fixture 2026-10-27 05:13:08 +01:00
evgeny e8b913a351 test(chat): cancel avatar peers from the committed receive callback 2026-10-27 05:12:49 +01:00
evgeny 65409f8c80 test(chat): exercise avatar transaction, schema, and key lookup failures 2026-10-27 05:12:35 +01:00
evgeny 8b16f40094 test(chat): add precise avatar SQL fault hooks and committed-state observer 2026-10-27 05:11:03 +01:00
evgeny e645ef309a fix(member-files): reject membership lookup binding and finalization errors 2026-10-27 05:09:44 +01:00
evgeny e0598ccc22 fix(chat): commit avatar updates before notification and validate SQL reads 2026-10-27 05:08:31 +01:00
evgeny bc41bb68b6 test(chat): reproduce successful avatar publication after failed hash binding 2026-10-27 05:05:40 +01:00
evgeny f378c7b28d docs(chat): record full channel creation validation 2026-10-27 05:04:46 +01:00
evgeny 04ce61e3ca docs(chat): describe atomic channel creation and verified failure paths 2026-10-27 05:02:28 +01:00
evgeny 8003413d97 test(chat): cover channel input contracts and TCP creation result 2026-10-27 05:00:27 +01:00
evgeny f1cada4c02 Cover channel crypto failures and require owner close after failed rollback 2026-10-27 04:49:39 +01:00
evgeny 01c2ebd7d8 Verify channel SQL failure rollback and complete signed profiles through an independent reader 2026-10-27 04:43:31 +01:00
evgeny 1d84765f08 Instrument channel creation SQL and hash committed schema, identities and roots 2026-10-27 04:40:36 +01:00
evgeny 60942e542b Propagate all member block binding and finalization failures before index publication 2026-10-27 04:36:09 +01:00
evgeny 1ce58cbeac Reject publication when the initial member profile signature cannot be bound 2026-10-27 04:34:57 +01:00
evgeny 956febe1d7 Report committed channel creation results through the headless API 2026-10-27 04:27:31 +01:00
evgeny 129fece0af Share channel description verification with JOIN and cleanse owned creation requests 2026-10-27 04:27:31 +01:00
evgeny 55e270898b Check channel key generation and return the committed channel identity 2026-10-27 04:26:43 +01:00
evgeny 72b3068a8b Commit new channel identity and signed root membership before runtime publication 2026-10-27 04:25:08 +01:00
evgeny c0aacb3287 Define one bounded channel description encoding for signing and verification 2026-10-27 04:23:22 +01:00
evgeny 6a6ad5c612 Store channel metadata and peer schema atomically with checked SQL operations 2026-10-27 04:19:14 +01:00
evgeny 999fe909e4 Target the exact signed JOIN body instead of counting unrelated signatures 2026-10-27 04:16:07 +01:00
evgeny a4dd4a10d5 Exercise failed root signing during channel creation against committed SQLite 2026-10-27 04:12:58 +01:00
evgeny 1c5faaffc3 Record full feedback and poll verification after moving sanitizer artifacts 2026-10-27 04:06:52 +01:00
evgeny 9b55e3c7cb Record verified feedback rollback and poll command readback contracts 2026-10-27 04:02:56 +01:00
evgeny 6c0f9c9068 Reject embedded NUL before passing animation JSON to rlottie 2026-10-27 04:00:54 +01:00
evgeny 1b0034e1ba Handle saved-setting apply errors and document committed CLI interaction results 2026-10-27 03:58:19 +01:00
evgeny a7a7e4b5a1 Cancel member actions when a click becomes a drag 2026-10-27 03:54:03 +01:00
evgeny d741eb7374 Document feedback transaction ownership, codecs and committed readback acknowledgments 2026-10-27 03:52:09 +01:00
evgeny 7f0845e50c Check readback retries and refused acknowledgments keep feedback roots unchanged 2026-10-27 03:50:16 +01:00
evgeny 739ee0802f Verify poll commit acknowledgments survive snapshot faults and reserve memory before changes 2026-10-27 03:49:38 +01:00
evgeny 9876677a5f Refresh committed feedback without repeating user actions after readback errors 2026-10-27 03:49:38 +01:00
evgeny 1666538b0c Acknowledge committed poll commands when snapshot readback fails 2026-10-27 03:49:09 +01:00
evgeny 70251fafab Verify poll SQL failures preserve history, reactions, roots and events 2026-10-27 03:45:11 +01:00
evgeny 63e76d2cf5 Add targeted SQL read fault injection for poll command tests 2026-10-27 03:44:02 +01:00
evgeny 1820128f5b Document member badge painting and ignored state flags 2026-10-27 03:43:31 +01:00
evgeny efe879bfd9 Preserve complete audio frames and reject invalid ring buffers 2026-10-27 03:42:03 +01:00
evgeny aef6aae432 Validate complete poll command reads before publication or reaction changes 2026-10-27 03:41:36 +01:00
evgeny cf7a3a0939 Check SQL reads before interpreting poll targets and command fields 2026-10-27 03:41:06 +01:00
evgeny d89a86c9dd Cover feedback page and hash SQL errors without empty success 2026-10-27 03:38:21 +01:00
evgeny 7fa20ceb79 Specify shared FFmpeg image buffer ownership 2026-10-27 03:38:12 +01:00
evgeny 96a3d05c74 Reject failed feedback page and hash reads and report transaction error codes 2026-10-27 03:37:29 +01:00
evgeny fc94fa0bfd Own feedback version selection and page publication transactions 2026-10-27 03:35:03 +01:00
evgeny b0acb285c1 Bound TGS decoding and keep invalid icons out of animation 2026-10-27 03:33:50 +01:00
evgeny a591469770 Check feedback lookups and writes and retain rollback ownership 2026-10-27 03:33:34 +01:00
evgeny 1f19720cb8 Exercise feedback lookup failures, atomic retries and rollback ownership 2026-10-27 03:32:32 +01:00
evgeny 6a4fa8065a Add targeted SQL failure injection to real signed feedback tests 2026-10-27 03:30:51 +01:00
evgeny 1e5fc2d335 Record full 213-test verification and diagnose SQL clock fixture failure 2026-10-27 03:28:40 +01:00
evgeny 3309bee7eb Compare attachment SQL timestamps using the SQLite clock 2026-10-27 03:24:22 +01:00
evgeny 41ce04e2f0 Document desktop instance activation and lock lifetime 2026-10-27 03:22:21 +01:00
evgeny 795f930dd6 Record attachment index recovery and immutable settings verification 2026-10-27 03:22:19 +01:00
evgeny 55328e18af Clarify LAN adapter thread, delivery and settings contracts 2026-10-27 03:20:41 +01:00
evgeny 8fefd510a6 Document committed outbox retries and transaction-safe preparation recovery 2026-10-27 03:20:41 +01:00
evgeny dafd3a2adc Reject malformed audio diagnostic snapshots 2026-10-27 03:19:03 +01:00
evgeny 0995a1eac3 Complete event diagnostics and clarify attachment event payloads 2026-10-27 03:17:05 +01:00
evgeny 40ae782719 Cover settings immutability, validation rollback and LAN apply retry 2026-10-27 03:15:58 +01:00
evgeny 43f9608fbf Make chat settings reads immutable and validation failures atomic 2026-10-27 03:15:15 +01:00
evgeny 27aa76f914 Match recovery fixture byte count and normal cached-file policy 2026-10-27 03:14:09 +01:00
evgeny 9fa58b4f83 Keep desktop config projection separate from core settings 2026-10-27 03:14:00 +01:00
evgeny f6f3ca8da7 Share checked desktop config batches and diagnose write failures 2026-10-27 03:14:00 +01:00
evgeny bc11c6ac00 Check attachment initialization leaves an external transaction untouched 2026-10-27 03:12:56 +01:00
evgeny 52d8ec4e49 Preserve attachment files when shutdown sees an open transaction 2026-10-27 03:12:37 +01:00
evgeny 6bbd41f49f Exercise attachment recovery against uncommitted history deletion 2026-10-27 03:12:11 +01:00
evgeny 7f8869537b End idle attachment outbox scans without invoking commit hooks 2026-10-27 03:06:26 +01:00
evgeny 108266328c Commit attachment retry reservations before network delivery 2026-10-27 03:00:30 +01:00
evgeny b9d36b42af Report manifest limits and active download refusals precisely 2026-10-27 02:59:02 +01:00
evgeny 59d51d0e80 Document atomic attachment indexes and signed-history recovery 2026-10-27 02:58:44 +01:00
evgeny 13918d1cea Preserve desktop configuration on failed writes 2026-10-27 02:57:07 +01:00
evgeny 00c4065771 Use the signed-history limit for manual attachment parsing 2026-10-27 02:50:27 +01:00
evgeny 61ed80e18c Share the ETCP monitor across desktop entry points 2026-10-27 02:49:39 +01:00
evgeny 67368ebbac Check complete signed manifests beyond the old manual download limit 2026-10-27 02:46:59 +01:00
evgeny 2165c8a1f8 Exercise real manifest recovery and bounded backfill overflow 2026-10-27 02:38:02 +01:00
evgeny 7e7cabfb3a Verify atomic manifest indexes, local intent and history recovery 2026-10-27 02:34:02 +01:00
evgeny 78d0e3f54f Retry failed decryption after a newly verified download 2026-10-27 02:31:29 +01:00
evgeny b31f974a44 Target local attachment transactions and observe recovery timers in tests 2026-10-27 02:28:42 +01:00
evgeny 1b695fe247 Own and cancel desktop bridge commands before core teardown 2026-10-27 02:25:05 +01:00
evgeny fdb11ed3ce Restore retained attachment indexes during startup and manual download 2026-10-27 02:20:02 +01:00
evgeny d9f5da130e Recover attachment indexes from history without losing message events 2026-10-27 02:18:30 +01:00
evgeny fc60ca295a Commit attachment download intent and cache restoration atomically 2026-10-27 02:16:23 +01:00
evgeny 45d5aaa7a1 Index attachment manifests atomically and classify invalid identities 2026-10-27 02:15:14 +01:00
evgeny fe75a493ef Commit immutable attachment identity before message publication 2026-10-27 02:09:17 +01:00
evgeny 6803369a13 Record verified outgoing, completion and channel deletion fixes 2026-10-27 02:05:25 +01:00
evgeny 618f6ca073 Document atomic channel deletion and post-commit file cleanup 2026-10-27 01:55:35 +01:00
evgeny c3ac2059f4 Check complete Merkle cleanup before committing channel deletion 2026-10-27 01:55:03 +01:00
evgeny 8850121984 Verify channel deletion rollback and committed cascade snapshots 2026-10-27 01:53:47 +01:00
evgeny 6a6f8db208 Preserve channel files until checked metadata deletion commits 2026-10-27 01:50:03 +01:00
evgeny cd6ca2388f Cascade attachment records with channel deletion before cleaning files 2026-10-27 01:50:02 +01:00
evgeny 34e6fecbd0 Target completion transaction faults without affecting history jobs 2026-10-27 01:49:03 +01:00
evgeny 75124e0efa Preserve independent NAT details in desktop diagnostics 2026-10-27 01:48:13 +01:00
evgeny 12c5945eed Handle unavailable cores and late status page responses 2026-10-27 01:44:40 +01:00
evgeny 77e0a3c93e Verify completion ownership through cancellation, retry and task limits 2026-10-27 01:43:50 +01:00
evgeny b2addca5ce Clarify desktop logging contracts and retain early log path 2026-10-27 01:41:23 +01:00
evgeny d22bae19b4 Fix desktop node startup rollback and state ownership 2026-10-27 01:38:26 +01:00
evgeny d7ffa1031f Exercise committed decryption claims and retryable SQL completions 2026-10-27 01:37:30 +01:00
evgeny b71c3040d4 Claim decryption before workers and retry retained completions 2026-10-27 01:35:30 +01:00
evgeny 4d19de7cce Retain attachment completions until their SQL result commits 2026-10-27 01:34:47 +01:00
evgeny 851c6723ff Verify outgoing request persistence and document commit boundary 2026-10-27 01:30:24 +01:00
evgeny d1bfd1d16c Commit outgoing attachment requests before events and delivery 2026-10-27 01:28:23 +01:00
evgeny 023bd78e16 Cover atomic outgoing attachment request creation and timer guard 2026-10-27 01:27:19 +01:00
evgeny b163642b52 Fix oversized ETCP monitor snapshots 2026-10-27 01:26:16 +01:00
evgeny 8a0654e57b Merge remote-tracking branch 'origin/master' 2026-10-27 01:23:25 +01:00
evgeny e43bcec7cb Record verified REQUEST commit fix and continue chat audit 2026-10-27 01:22:57 +01:00
evgeny 383eb6d076 Describe atomic REQUEST acceptance and committed acknowledgment tests 2026-10-27 01:17:40 +01:00
evgeny bb961dd394 Exercise rejected REQUEST, author restart and real retry before ACK 2026-10-27 01:12:11 +01:00
evgeny 4d87cab28a docs(dm): align history generation contract after rebase 2026-10-27 01:11:28 +01:00
evgeny 5bb27d1b61 docs(dm): record completed contract audit and validation 2026-10-27 01:06:45 +01:00
evgeny 9480f8029b docs(dm): finish invite and cleanup validation contracts 2026-10-27 01:06:45 +01:00
evgeny a00db6229f docs(dm): reconcile architecture and PM delivery requirements 2026-10-27 01:06:45 +01:00
evgeny ad577318d9 docs(dm): specify media protocol and worker lifetime 2026-10-27 01:06:45 +01:00
evgeny 1cfafc43a4 docs(dm): complete mailbox and storage coordinator contracts 2026-10-27 01:06:45 +01:00
evgeny 2fe024fbbb docs(dm): clarify core and history ownership contracts 2026-10-27 01:06:45 +01:00
evgeny 98a6d3cce0 Test committed REQUEST acknowledgments, duplicates and SQL failures 2026-10-27 01:05:49 +01:00
evgeny 760398b96c Commit incoming attachment requests before acknowledging them 2026-10-27 01:02:55 +01:00
evgeny 492f6adc99 Merge upstream attachment audit documentation 2026-10-27 00:57:14 +01:00
evgeny 102da344d9 Merge origin/master with Android conversation gestures 2026-10-27 00:55:47 +01:00
evgeny 15ec41f2fd Record lost REQUEST after real acknowledgment and author rollback 2026-10-27 00:55:14 +01:00
evgeny 9f7ceb5815 Document REQUEST acknowledgment before external transaction commit 2026-10-27 00:55:13 +01:00
evgeny 13039933da android: separate avatar taps from conversation context menus 2026-10-27 00:49:00 +01:00
evgeny ef8e8f2cb5 Record verified author decision fix and close its audit task 2026-10-27 00:45:43 +01:00
evgeny 7649981cbc Describe committed author decisions and rollback lifecycle 2026-10-27 00:39:18 +01:00
evgeny 401193950b Verify author decision rollback and retry across real chat restart 2026-10-27 00:15:17 +01:00
evgeny 8c36d00fd2 Test atomic attachment decisions and committed UI notifications 2026-10-27 00:13:31 +01:00
evgeny 8a7f4469dc Commit author attachment decisions and delivery queue atomically 2026-10-27 00:10:58 +01:00
evgeny 77d6e8cfd0 Record reproduced attachment decision durability failures 2026-10-26 23:50:40 +01:00
evgeny 55ad0e971a Document uncommitted or undeliverable attachment author decisions 2026-10-26 23:49:35 +01:00
evgeny e6c0271e2c Record validated chat merge and stop the audit after Pull 2026-10-26 23:41:43 +01:00
evgeny 845bf2580d Merge upstream chat revisions with attachment transaction fixes 2026-10-26 23:35:21 +01:00
evgeny 012bd650ea Record verified GRANT fix before integrating upstream chat revisions 2026-10-26 23:22:57 +01:00
evgeny b8b6636644 Document result commit guarantees and verify quiesced callback removal 2026-10-26 23:17:55 +01:00
evgeny 9839f56f8c Exercise real GRANT retry and verify committed key before network ACK 2026-10-26 23:09:49 +01:00
evgeny 03d1afbd13 Tighten result regression snapshots and expose SQL transition diagnostics 2026-10-26 23:07:50 +01:00
evgeny 935bd704ae Test attachment result transaction failures and committed acknowledgments 2026-10-26 23:06:20 +01:00
evgeny 54a6e33c5e Commit attachment result state and key before acknowledging delivery 2026-10-26 23:03:19 +01:00
evgeny ab0b866c4c Explain edit rejection causes and unchanged submissions in diagnostics 2026-10-26 22:59:26 +01:00
evgeny 4b189891f7 Keep UI test references stable across queued channel refreshes 2026-10-26 22:58:15 +01:00
evgeny 5cf823da6a Record real-network GRANT bind failure and stop attachment audit 2026-10-26 22:57:20 +01:00
evgeny 334991fbe5 Initialize the desktop edit fixture with a valid test identity 2026-10-26 22:56:21 +01:00
evgeny 52651cfa0d Document confirmed GRANT acknowledgment without saved attachment key 2026-10-26 22:55:49 +01:00
evgeny 87f312f31b Retain startup diagnostics for the desktop edit integration test 2026-10-26 22:55:05 +01:00
evgeny b19e6797c9 Preserve Android edit drafts across screen recreation and serialize saves 2026-10-26 22:54:36 +01:00
evgeny e229811e20 Document editable history protocol and update media insert callback 2026-10-26 22:54:25 +01:00
evgeny aefef51c58 Update stress and media fixtures to sign versioned group records 2026-10-26 22:52:35 +01:00
evgeny 45257bf37f Exercise desktop editing through the real menu composer core and quoted replies 2026-10-26 22:51:10 +01:00
evgeny d998b390f3 Exercise offline and live edits over real Merkle and PUSH transport 2026-10-26 22:50:20 +01:00
evgeny ec8c6e9eb1 Verify edit events bypass new-message callbacks and notifications 2026-10-26 22:49:45 +01:00
evgeny 8523448c3a Reserve desktop edit dispatch and reject malformed update events 2026-10-26 22:49:45 +01:00
evgeny 5b6c96d3d8 Test desktop composer edit confirmation cancel failure and channel changes 2026-10-26 22:47:58 +01:00
evgeny 48bdb85ff9 Preserve composer media policy and display edit times for short or emoji text 2026-10-26 22:47:57 +01:00
evgeny 8b34889a19 Check signed reply references against the revision envelope 2026-10-26 22:46:42 +01:00
evgeny e45f33e7b5 Cover revision conflicts Merkle updates stale ACKs and rollback 2026-10-26 22:46:08 +01:00
evgeny 82ef6f282f Reset only dependent group history state when adopting editable format 2026-10-26 22:46:08 +01:00
evgeny 0a75a53ed9 Correct Android attachment submission argument after editor integration 2026-10-26 22:45:04 +01:00
evgeny c39bdef344 Localize group editing controls and edit timestamps 2026-10-26 22:45:04 +01:00
evgeny 030336eaeb Record verified attachment key fix lifecycle contracts and validation 2026-10-26 22:44:14 +01:00
evgeny d0ff7a81f7 Bind deferred-author integrity fixture to its signed author 2026-10-26 22:43:46 +01:00
evgeny 65cdb5b468 Keep format-reset rollback safe and expire pending revisions by edit time 2026-10-26 22:43:38 +01:00
evgeny cad10a33e0 Test group edit persistence references and scoped dev format reset 2026-10-26 22:42:36 +01:00
evgeny 93057c9eb7 Adapt signed history regression fixtures to canonical revision envelopes 2026-10-26 22:41:56 +01:00
evgeny 1afb1010cc Wire desktop context-menu editing and update visible messages in place 2026-10-26 22:38:42 +01:00
evgeny 2a9b5748e9 Show edited time and retain revision metadata in desktop rows 2026-10-26 22:38:17 +01:00
evgeny 20350338b8 Deliver committed edits to desktop without message notifications 2026-10-26 22:38:17 +01:00
evgeny e483027e3e Edit group text in desktop composer with save cancel and draft restoration 2026-10-26 22:38:06 +01:00
evgeny b46927f40d Read group revisions and keep history windows ordered by stable IDs 2026-10-26 22:37:39 +01:00
evgeny 371aa3ac19 Dispatch correlated edit submissions through shared composer request 2026-10-26 22:37:39 +01:00
evgeny ca15dbb88a Save authored text edits and preserve group references across revisions 2026-10-26 22:36:47 +01:00
evgeny 2f35502978 Expose group message editing and committed update event 2026-10-26 22:35:46 +01:00
evgeny 809f009796 Replicate group message revisions atomically with versioned acknowledgements 2026-10-26 22:35:34 +01:00
evgeny 5c638a60db Define permanent group message identity from creation fields 2026-10-26 22:33:43 +01:00
evgeny b0d246913d Complete Android navigation after PM conversation deletion 2026-10-26 22:11:39 +01:00
evgeny 18df7cc43d Describe media backfill triggers in terms of Merkle history sync 2026-10-26 22:11:39 +01:00
evgeny 9009153cc5 Remove unused legacy chat sync commands and empty connect API 2026-10-26 22:11:39 +01:00
evgeny 5dab86e840 Initialize chat schema before storing and publishing local identity 2026-10-26 22:11:39 +01:00
evgeny f6005fd96d Remove obsolete history chain reads from chat caches and API 2026-10-26 22:11:39 +01:00
evgeny f1da7d7ee8 Align desktop history reads with Merkle schema and fix PM test setup 2026-10-26 22:11:39 +01:00
evgeny cd0329b301 Cover deleted PM composer state and log GUI deletion failures 2026-10-26 22:11:39 +01:00
evgeny 8ccad3b56b Test PM deletion through the desktop window and history snapshot generations 2026-10-26 22:11:39 +01:00
evgeny e88f3ce46a Finish PM composer cleanup and skip history requests after shutdown 2026-10-26 22:11:39 +01:00
evgeny f83033d3a1 Guard Android PM history and pending deletion across asynchronous sends 2026-10-26 22:11:39 +01:00
evgeny 5778c8aa0a Translate desktop PM deletion confirmation and errors 2026-10-26 22:11:39 +01:00
evgeny 6382c794ea Complete desktop PM deletion after the core transaction result 2026-10-26 22:11:39 +01:00
evgeny 4089944673 Guard desktop PM sends and history by conversation generation 2026-10-26 22:11:39 +01:00
evgeny e10354a0ac Keep PM composer work bound to its history generation 2026-10-26 22:11:39 +01:00
evgeny df80c43a57 Offer conversation deletion in the desktop PM context menu 2026-10-26 22:11:39 +01:00
evgeny 8e3c536cbb Bridge desktop DM deletion requests and results 2026-10-26 22:11:39 +01:00
evgeny f0b7b22f92 Read visible DM generations in the desktop conversation list 2026-10-26 22:11:39 +01:00
evgeny fd82e7bf1b Tag DM history snapshots with conversation generation 2026-10-26 22:11:39 +01:00
evgeny 71dfc9d70d Clarify attachment commit ownership and SQL failure diagnostics 2026-10-26 22:10:44 +01:00
evgeny e5ca2e174c Honor core stop before and after Android restart with lifecycle regressions 2026-10-26 22:07:39 +01:00
evgeny c75bfc618a Honor core stop in desktop owner loop and verify cleanup 2026-10-26 22:07:39 +01:00
evgeny 07f63c3047 Document audited call video and session contracts 2026-10-26 21:54:18 +01:00
evgeny 289b592598 Use distinct regression helper name and reserved uasync post API 2026-10-26 21:53:36 +01:00
evgeny 99ebee37e7 Test attachment key commit faults ownership and retry with real workers 2026-10-26 21:51:36 +01:00
evgeny 549cc202fb Commit complete attachment preparation before accepting encryption work 2026-10-26 21:49:11 +01:00
evgeny d877e8881c Record confirmed attachment key bind failure and pause chat audit 2026-10-26 21:31:00 +01:00
evgeny e33d51a34a Document attachment key persistence failure during chat audit 2026-10-26 21:29:41 +01:00
evgeny 1899b7f8cf Record verified admin and worker rebase integration 2026-10-26 20:54:08 +01:00
evgeny 589fef2aae Reconcile audit contracts with upstream post cancellation 2026-10-26 20:48:15 +01:00
evgeny 8718ee901c Record verified worker shutdown fix and negative control 2026-10-26 20:46:31 +01:00
evgeny 2c48a53c6a Clarify cancellation ownership during service shutdown 2026-10-26 20:46:31 +01:00
evgeny 9e68928222 Test worker drain with completions in a detached batch 2026-10-26 20:45:32 +01:00
evgeny 781d41e003 Disable detached worker completions before draining jobs 2026-10-26 20:45:32 +01:00
evgeny 8f3d9edf8b Record admin verification and worker completion crash 2026-10-26 20:44:42 +01:00
evgeny 998993997d Diagnose admin key derivation failures without exposing secrets 2026-10-26 20:44:42 +01:00
evgeny 5169ffd0b3 Specify admin import transaction and notification contracts 2026-10-26 20:44:42 +01:00
evgeny cc04f1c0fa Test admin import rollback, commit notification and safe retry 2026-10-26 20:44:42 +01:00
evgeny 76527de44f Store admin keys atomically and reject invalid transfer context 2026-10-26 20:44:42 +01:00
evgeny e65ceeafdc Track atomic admin import correction and regression requirements 2026-10-26 20:44:42 +01:00
evgeny 1916ba4270 Document admin key import failures and null transfer crash 2026-10-26 20:44:42 +01:00
evgeny bc623f2371 docs(call): record post-rebase validation results 2026-10-26 20:30:18 +01:00
evgeny f521bb37f4 fix(call): align audio frontend ownership with core lifecycle
Keep the borrowed audio provider on UTUN_INSTANCE across chat service restarts.
Attach and detach Android voice from the core worker and reject DSP startup
after detach. Preserve TX owner epochs, pending slots and foreign-instance RX
isolation. Remove the unused cross-thread online API and keep its check local
to call.c.

Add eight lifecycle regressions with real cores and the Android host bridge.
Audit public call, DSP, clock, headless and desktop audio contracts and record
the findings and validation scope.

Validation: voice/desktop 38/38, four native call suites, native and both Android
ABI builds, full check 204/204 plus proxy/burst/load. JVM results remain Gradle
UP-TO-DATE (155 app and 15 router). No hardware runtime validation.
2026-10-26 20:22:46 +01:00
evgeny b84a6ab3b9 docs(call): record validated Bluetooth lifecycle fix and regressions 2026-10-26 20:22:27 +01:00
evgeny 2e8885548b docs(call): clarify Android output and Bluetooth proxy ownership 2026-10-26 20:21:58 +01:00
evgeny 83df895532 build(android): run router lifecycle regressions with app unit tests 2026-10-26 20:21:58 +01:00
evgeny f724703156 test(android): cover Bluetooth router teardown and callback races 2026-10-26 20:21:58 +01:00
evgeny 34133e4f6f test(android): isolate router application and logging fixtures 2026-10-26 20:21:58 +01:00
evgeny 8cbadd821f test(android): model Bluetooth proxy ownership and device callbacks 2026-10-26 20:21:58 +01:00
evgeny 161422b2cd test(android): add isolated host context and clock fixtures 2026-10-26 20:21:58 +01:00
evgeny de83d7bbf4 fix(android): own Bluetooth proxies across router lifecycle 2026-10-26 20:21:58 +01:00
evgeny 3987295707 Report exact DM SQL prepare errors without duplicate step failures 2026-10-26 19:50:03 +01:00
evgeny 497792da73 Commit exact DM custody tombstones before acknowledging deletion 2026-10-26 19:45:51 +01:00
evgeny 3f195c058b Retain DM coordinator bodies when assignment scans fail 2026-10-26 19:42:00 +01:00
evgeny 2fce4641ec Exercise DM cleanup SQL failures and durable ACK retries 2026-10-26 19:39:04 +01:00
evgeny 8642b04e6d Cover worker teardown with completions in the active post batch 2026-10-26 19:15:32 +01:00
evgeny 93ec12226f Cancel pending posts from active dispatch batches 2026-10-26 19:12:37 +01:00
evgeny 28a5d0509f Expire DM test custody only after confirmed storage 2026-10-26 18:42:45 +01:00
evgeny 25a8b905fc Accept verified DM originals after storage expiry 2026-10-26 18:42:45 +01:00
evgeny ab8713d88f Record post-rebase sync validation on current master 2026-10-26 18:34:45 +01:00
evgeny c5075354d0 Record completed sync audit scope and refresh historical design links 2026-10-26 18:27:49 +01:00
evgeny 1456f02c03 Reconcile completed member and Merkle documentation audit status 2026-10-26 18:27:49 +01:00
evgeny b48f9e7a19 Close history Merkle migration with final validation results 2026-10-26 18:27:49 +01:00
evgeny 750ccf638d Verify history migration results and checkpoint revision races 2026-10-26 18:27:27 +01:00
evgeny bf94b240c8 Wake the lifecycle fixture after asserting standby restoration 2026-10-26 18:27:27 +01:00
evgeny 7d6197b5eb Restore requested activity after forced-active stream regression 2026-10-26 18:27:27 +01:00
evgeny d532fe062e Avoid recursive standby retries on allocation failure 2026-10-26 18:27:27 +01:00
evgeny f92077e601 Isolate stream wake state and require full roots in sync stress 2026-10-26 18:27:27 +01:00
evgeny e00d1f5212 Diagnose Merkle source profiles and identify both exchange endpoints 2026-10-26 18:27:27 +01:00
evgeny 669ad86e57 Document shared history Merkle protocol and bounded transactions 2026-10-26 18:27:26 +01:00
evgeny df39999624 Initialize media history model and admit the purge test peer 2026-10-26 18:27:26 +01:00
evgeny 316bacd8c8 Share isolated history model setup across sync fixtures 2026-10-26 18:27:26 +01:00
evgeny f42f002eb1 Test atomic page rollback, late author keys and model run policy 2026-10-26 18:27:26 +01:00
evgeny fefc0ee003 Preserve Android standby retry waits and clarify history contracts 2026-10-26 18:27:26 +01:00
evgeny 0444a4bfd9 Resume admitted history peers and gate automatic Merkle work by policy 2026-10-26 18:27:26 +01:00
evgeny a7b556d9a8 Verify long equal-count history divergence converges by full roots 2026-10-26 18:27:26 +01:00
evgeny 2833c53644 Cover handshake RNG failures and diagnose mismatched round tokens 2026-10-26 18:27:26 +01:00
evgeny a46091cb19 Yield the event loop between bounded history maintenance batches 2026-10-26 18:27:26 +01:00
evgeny 3a4143183d Cover stable history cursors, deleted authors and bounded TTL cleanup 2026-10-26 18:27:26 +01:00
evgeny 3fe697e1a9 Log history pause and await verified roots in empty peer fixtures 2026-10-26 18:27:26 +01:00
evgeny d76eb067bb Preserve signed history through schema conversion and resume both peers 2026-10-26 18:27:26 +01:00
evgeny 1402413ab9 Exercise history Merkle roots over live peers and invitation recovery 2026-10-26 18:27:26 +01:00
evgeny a87ece370c Revalidate history peers after membership and cover preflight integrity 2026-10-26 18:27:26 +01:00
evgeny 012cbdcd49 Register history after shared Merkle startup and validate page cursors 2026-10-26 18:27:26 +01:00
evgeny e822f9fef7 Pass leaf bounds to immediate page models and report disconnects 2026-10-26 18:27:26 +01:00
evgeny 5e5ed41c19 Replace history chain exchange with canonical Merkle data model 2026-10-26 18:27:26 +01:00
evgeny 41c2a77fe6 Expose per-model work policy and checkpoint results for history 2026-10-26 18:27:26 +01:00
evgeny a079381c0a Test cancelled Merkle rounds against the fresh responder challenge 2026-10-26 18:27:26 +01:00
evgeny 45c282e0b7 Bind Merkle rounds to fresh initiator and responder tokens 2026-10-26 18:27:26 +01:00
evgeny 137e30ec82 Require a fresh responder token when reattaching Merkle sessions 2026-10-26 18:27:26 +01:00
evgeny d20f31c165 Plan canonical history reconciliation on the shared Merkle engine 2026-10-26 18:27:26 +01:00
evgeny 5ae5fd066d Close history integrity fixes and record remaining sync audit scope 2026-10-26 18:27:26 +01:00
evgeny 143788cdcd Document verified history deletion and message-specific delivery 2026-10-26 18:26:58 +01:00
evgeny aa97e053cd Ensure SQLite allocation failures cannot authorize history deletion 2026-10-26 18:26:58 +01:00
evgeny 7853fb0fc6 Keep chain reads and delivery confirmation failures explicit 2026-10-26 18:26:58 +01:00
evgeny 557c98aeae Cover history preservation, corruption cleanup and exact delivery ACKs 2026-10-26 18:26:58 +01:00
evgeny fd53b443a0 Preserve history on verification errors and identify delivery ACKs 2026-10-26 18:26:58 +01:00
evgeny 26432be0e4 Point delivery audit finding to the failing SQL statement 2026-10-26 18:26:58 +01:00
evgeny 6734035734 Record db_sync verification data loss, deletion policy and delivery SQL defect 2026-10-26 18:26:58 +01:00
evgeny 068eddefd8 Flag db_sync data loss on verification errors and invalid duplicate input 2026-10-26 18:26:31 +01:00
evgeny b0bf27de61 Close DM expiry fix with fault restart and sanitizer evidence 2026-10-26 18:06:14 +01:00
evgeny 9fe4e834fe Describe DM expiry reconciliation and await restarted test link 2026-10-26 18:02:29 +01:00
evgeny 59e694bd83 Check all SQL results during DM storage release 2026-10-26 17:54:45 +01:00
evgeny 5788d2557b Test DM release rollback and expiry recovery after restart 2026-10-26 17:54:45 +01:00
evgeny cb08faae04 Reconcile DM storage expiry and release assignments atomically 2026-10-26 17:49:26 +01:00
evgeny dda4a780e3 Document unreconciled DM media expiry after SQL failure 2026-10-26 17:42:45 +01:00
evgeny 3dc5eb502d Record post-rebase DM validation 2026-10-26 17:31:42 +01:00
evgeny 87d24b722b Document verified DM name normalization and regressions 2026-10-26 17:27:18 +01:00
evgeny d09ed0316f Preserve UTF-8 boundaries in DM peer names 2026-10-26 17:27:18 +01:00
evgeny fa76447f53 Close Android mute fix with native Android and full-suite verification 2026-10-26 17:14:58 +01:00
evgeny 79b06846c6 Test Android bridge mute after real AEC and AGC across unmute 2026-10-26 17:14:18 +01:00
evgeny 000b28027c Preserve Android raw capture through AEC and apply mute before Opus 2026-10-26 17:14:18 +01:00
evgeny bdaff406be Forward Android frame mute through the native capture bridge 2026-10-26 17:14:18 +01:00
evgeny f83e27260d Record rebased DM verification and UTF-8 name truncation defect 2026-10-26 17:07:18 +01:00
evgeny b6588d0594 Preserve DM state on database failures and document contracts 2026-10-26 16:55:20 +01:00
evgeny 03be46c339 Record desktop voice checks and reproduce Android mute signal after AEC 2026-10-26 16:34:23 +01:00
evgeny 5fee00c451 Clarify desktop voice contracts and document Android mute ordering defect 2026-10-26 16:31:11 +01:00
evgeny 54a7522dd9 Record headless PCM framing fix and full regression results 2026-10-26 16:08:35 +01:00
evgeny 0ad80f3906 Clarify frame assembly and keep reference PCM arithmetic signed 2026-10-26 15:58:11 +01:00
evgeny 7c4e6f9972 Test complete headless frames across queue and short-write boundaries 2026-10-26 15:55:08 +01:00
evgeny 913ae531ec Queue complete headless PCM frames and account for overflow drops 2026-10-26 15:51:35 +01:00
evgeny ae9d3f0103 Record headless PCM backpressure framing failure and repair plan 2026-10-26 15:35:03 +01:00
evgeny 45834692a7 Document headless PCM framing defect and independent codec ownership 2026-10-26 15:33:11 +01:00
evgeny 03b050949c Close radio mesh regression with full merged-branch verification 2026-10-26 15:16:13 +01:00
evgeny bf4d14c46e Guard mesh gate node indices before matching endpoints 2026-10-26 15:09:35 +01:00
evgeny 1bc27aa2ca Verify radio mesh deduplication independently of arrival order 2026-10-26 15:08:08 +01:00
evgeny ec795b4764 Explain false radio mesh failure and preserve rebase audit 2026-10-26 15:01:59 +01:00
evgeny ff394c7a6a Preserve final mesh duplicate read outside diagnostic mode 2026-10-26 14:57:41 +01:00
evgeny 21af2e0f35 Observe late mesh packets within the running event loop 2026-10-26 14:57:41 +01:00
evgeny d943c0ddca Poll diagnostic mesh tail after the original loop stops 2026-10-26 14:57:41 +01:00
evgeny c2908a6c58 Trace radio mesh packet paths and original stop conditions 2026-10-26 14:57:41 +01:00
evgeny 5587c60603 Record successful proxy integration checks after TX fix 2026-10-26 14:57:41 +01:00
evgeny faaa5a835a Record repeated full-check failure in unrelated radio mesh test 2026-10-26 14:57:41 +01:00
evgeny 2429688382 Document call audio TX fix and independent radio mesh flake 2026-10-26 14:57:41 +01:00
evgeny f0df3d8848 Make stale TX regressions assert observable behavior first 2026-10-26 14:57:24 +01:00
evgeny bbc9f750c3 Cover call audio TX ownership across lifecycle transitions 2026-10-26 14:57:24 +01:00
evgeny e11eca93bb Isolate call audio TX tasks by instance binding epoch 2026-10-26 14:57:24 +01:00
evgeny 266406261a Validate call audio ownership plan against lifecycle boundaries 2026-10-26 14:57:24 +01:00
evgeny a38b1f1e0b Record call audio audit scope and unchanged implementation checks 2026-10-26 14:57:24 +01:00
evgeny 6f8164d7cd Record call audio rebind counter defect and ownership plan 2026-10-26 14:57:24 +01:00
evgeny a4d8637f45 Document stale call audio tasks corrupting pending accounting 2026-10-26 14:57:09 +01:00
evgeny ea3685d8f5 Record final call admission contract and environment checks 2026-10-26 14:57:09 +01:00
evgeny c7387c2879 Record passing call admission and voice verification 2026-10-26 14:57:09 +01:00
evgeny 198cf52791 Document call admission fix and live regression coverage 2026-10-26 14:56:54 +01:00
evgeny 9838384d43 Authenticate admission fixture members for radio interaction 2026-10-26 14:56:54 +01:00
evgeny 56a06da35f Cover call admission, ID reuse and retained sessions on live cores 2026-10-26 14:56:54 +01:00
evgeny 96d99a183d Reject overlapping outgoing calls and registered call IDs 2026-10-26 14:56:54 +01:00
evgeny 7e69595372 Record call audit baseline checks and exact admission location 2026-10-26 14:56:54 +01:00
evgeny c5eb17e59b Record outgoing call admission defect and verification plan 2026-10-26 14:56:54 +01:00
evgeny aa09459c70 Document missing busy admission guard for outgoing calls 2026-10-26 14:56:06 +01:00
evgeny aea8ca7236 Close feedback admission fix with full integration and sanitizer verification 2026-10-26 14:44:03 +01:00
evgeny 1634499d84 Document active feedback, credential dependencies and permission round lifecycle 2026-10-26 14:44:03 +01:00
evgeny f3be00438b Assert profile updates leave feedback idle and preserve outer-commit change flags 2026-10-26 14:44:03 +01:00
evgeny 953e094c92 Restrict feedback invalidation to credential, key and admission changes 2026-10-26 14:44:03 +01:00
evgeny 5cae3d095d Keep the full-cursor comment beside its pagination test 2026-10-26 14:44:03 +01:00
evgeny c5a09c1413 Check permission round teardown, blocked error handoff and safe restart callbacks 2026-10-26 14:44:03 +01:00
evgeny 789e5663f7 End unauthorized feedback rounds while preserving watches for member restoration 2026-10-26 14:44:03 +01:00
evgeny 4278eb7851 Wait for incoming transport before late-peer group setup and check restored publishing 2026-10-26 14:44:03 +01:00
evgeny ea1c17593b Test full chat restart and late-peer convergence across member exclusion and restoration 2026-10-26 14:44:03 +01:00
evgeny bf941bfd14 Preserve precise author failure reasons and guard SQLite column allocation 2026-10-26 14:44:03 +01:00
evgeny 4b394b75e0 Cover feedback revocation, purge, delayed cross-channel credentials and transactional faults 2026-10-26 14:44:03 +01:00
evgeny 490b30b956 Update dependent feedback atomically with membership and filter poll results 2026-10-26 14:44:03 +01:00
evgeny 8925503d59 Retain inactive feedback while rebuilding and serving only verified active states 2026-10-26 14:44:03 +01:00
evgeny e7ae4025e2 Expose dataset registration for dependent model startup boundaries 2026-10-26 14:44:03 +01:00
evgeny 42808450b5 Distinguish member author admission, pending credentials and verification errors 2026-10-26 14:44:03 +01:00
evgeny 9c1b8e3e03 Record feedback plan dependency and cross-channel audit gaps 2026-10-26 14:44:03 +01:00
evgeny e1e9d63925 Record excluded-author feedback restart audit findings 2026-10-26 14:44:03 +01:00
evgeny 8e2036b435 Document feedback startup failure for excluded historical authors 2026-10-26 14:44:03 +01:00
evgeny e56790649a Record passing member purge integration and sanitizer validation 2026-10-26 14:44:02 +01:00
evgeny 16c64ca626 Describe Merkle index ownership and mark old sync plan historical 2026-10-26 14:42:59 +01:00
evgeny 26b6eca150 Clarify Merkle pull lifecycle, progress timeout and audit scope 2026-10-26 14:42:59 +01:00
evgeny 81712e74cd Document atomic member purge and verification error contracts 2026-10-26 14:42:59 +01:00
evgeny e4de937953 Use public READY checks in physical member purge regression 2026-10-26 14:42:59 +01:00
evgeny 8b525c06a9 Verify physical purge preserves radio and transport on rollback 2026-10-26 14:42:59 +01:00
evgeny fe182e4acc Check cleanup retries, request ownership and stream rollback 2026-10-26 14:42:59 +01:00
evgeny 87b22966b7 Exercise member cleanup transaction and verification failures 2026-10-26 14:42:59 +01:00
evgeny fbe3e7a279 Make member cleanup atomic and publish removals after commit 2026-10-26 14:42:59 +01:00
evgeny 1f3f1d9a34 Separate missing member verification keys from SQLite failures 2026-10-26 14:42:59 +01:00
evgeny fe7bcb334c Distinguish Ed25519 verification errors from invalid signatures 2026-10-26 14:42:59 +01:00
evgeny f79b9261c7 Record passing stream audit verification and remaining scope 2026-10-26 12:55:48 +01:00
evgeny 6689cf543b Clarify source signing key and interval estimate documentation 2026-10-26 12:54:52 +01:00
evgeny 409d9f3ca9 Record completed stream header audit and align native guide 2026-10-26 12:53:40 +01:00
evgeny de535e1713 Distinguish source mute state from immutable audio codec 2026-10-26 12:47:46 +01:00
evgeny 8533dfc303 Clarify asynchronous publisher and viewer session contracts 2026-10-26 12:45:31 +01:00
evgeny 0f54f5ef66 Specify stream playback clock and audio thread contracts 2026-10-26 12:43:25 +01:00
evgeny c5292c9994 Document stream member admission and wire codec contracts 2026-10-26 12:42:14 +01:00
evgeny 495972cfe6 Clarify stream ring and media cursor ownership and results 2026-10-26 12:40:44 +01:00
evgeny b25acce11b Specify stream queue accounting and timing metric snapshots 2026-10-26 12:33:15 +01:00
evgeny 080b14a51d Document stream flow lifetimes, leases and relay ownership 2026-10-26 12:31:01 +01:00
evgeny 5259c746d1 Record INFO expiry fix and complete IPv6-isolated verification 2026-10-26 12:19:42 +01:00
evgeny b116b20b11 Exercise INFO expiry under real ETCP backpressure 2026-10-26 12:04:31 +01:00
evgeny c148351259 Close expired nine-byte stream INFO requests with overload reason 2026-10-26 12:04:31 +01:00
evgeny 3956711c16 Record expired stream INFO probe defect and isolated evidence 2026-10-26 12:04:31 +01:00
evgeny 840bc9a6b0 Align stream guide with session ownership and current audit status 2026-10-26 12:04:31 +01:00
evgeny 27ed4c74ff Clarify stream session ownership and supplier selection contracts 2026-10-26 12:04:31 +01:00
evgeny 962a2df3f8 Add local channel deletion to desktop channel settings 2026-10-26 10:47:39 +01:00
evgeny d67e0b0c23 Report desktop startup failures and validate generated configs 2026-10-26 10:12:30 +01:00
evgeny 9ab9d01907 Fix Android config storage limits for the strict parser 2026-10-26 10:12:30 +01:00
evgeny 4abebdf35c Record passing stream publisher checks after integration 2026-10-26 09:58:45 +01:00
evgeny ced9dc2340 Merge remote-tracking branch 'origin/master' 2026-10-26 09:50:34 +01:00
evgeny 8627e4f6a5 Record stream packing fix and native regression coverage 2026-10-26 09:50:34 +01:00
evgeny a056b2c262 Describe publisher packing budgets and queued audio rejection 2026-10-26 09:50:34 +01:00
evgeny 017a326a13 Cover stream publisher packing boundaries and actor progress 2026-10-26 09:46:24 +01:00
evgeny 1a9fe0a33b Fix stream publisher audio packing and video progress 2026-10-26 09:44:16 +01:00
evgeny 8e74619501 Make chat joins atomic and test invitation lifecycle failures 2026-10-26 09:19:35 +01:00
evgeny bb6ed1723f Close instance audit with full integration and sanitizer results 2026-10-26 09:09:00 +01:00
evgeny 435ca204fe Clarify that repeated core start preserves the application running flag 2026-10-26 09:08:42 +01:00
evgeny 8640b45e99 Record instance lifecycle fixes, contracts and sanitizer audit evidence 2026-10-26 09:08:42 +01:00
evgeny 4219d5ce1d Specify reload behavior for repeated server sections in public header 2026-10-26 09:08:42 +01:00
evgeny 85e87cc9f8 Read radio hop list through aligned storage 2026-10-26 09:08:42 +01:00
evgeny e577daf6ab Describe conservative reload for duplicate and resolved IPv6 selectors 2026-10-26 09:08:42 +01:00
evgeny 0d6d1aeb19 Restart instances with duplicate server names to preserve all endpoints 2026-10-26 09:08:42 +01:00
evgeny a58f3516ab Clarify first-opened file logger ownership across instances 2026-10-26 09:08:42 +01:00
evgeny 44103038c0 Preserve selected global log path and verify database owner reload 2026-10-26 09:08:42 +01:00
evgeny 06ad255b95 Align instance guide and core lifecycle documentation with audited code 2026-10-26 09:08:42 +01:00
evgeny 9201d8c4f7 Use aligned PCM samples in headless radio integration fixture 2026-10-26 09:08:42 +01:00
evgeny ceaf71caf8 Align headless PCM before passing frames to Opus 2026-10-26 09:08:42 +01:00
evgeny 817e3744ee Reclaim cancelled heap timers before checking async resource counters 2026-10-26 09:08:42 +01:00
evgeny 68ead8c7dc Expose remaining async resource owners in instance regression test 2026-10-26 09:08:42 +01:00
evgeny 59f900810e Check headless clients, timer rollback and external subscription cleanup 2026-10-26 09:08:42 +01:00
evgeny 0d14613244 Document instance ownership, lifecycle, reload and callback contracts 2026-10-26 09:08:42 +01:00
evgeny f5488adab0 Release headless audio contexts, clients and timers on chat stop 2026-10-26 09:08:42 +01:00
evgeny fd93f39c78 Release remaining transport subscriptions at instance destruction 2026-10-26 09:08:42 +01:00
evgeny 1d92591780 Validate headless bindings and honor ephemeral ports in chat startup 2026-10-26 09:08:42 +01:00
evgeny 234c21fb6a Report outstanding allocations before instance teardown assertion 2026-10-26 09:08:42 +01:00
evgeny 88801318bb Include complete ETCP connection type in instance contract test 2026-10-26 09:08:42 +01:00
evgeny 9992228dcb Cover instance callbacks, constructor failures and selective/full reload 2026-10-26 09:08:42 +01:00
evgeny 4e32de8e65 Drain instance-owned sockets and registries without saved callback neighbors 2026-10-26 09:08:42 +01:00
evgeny a7d2333deb Reload complete endpoint settings and restart changed resource owners 2026-10-26 09:08:42 +01:00
evgeny 5e63bc2a42 Share atomic network index construction and typed socket teardown 2026-10-26 09:08:42 +01:00
evgeny b44ea19b4b Close config parser audit after the full integration suite 2026-10-26 09:08:42 +01:00
evgeny d681d5abf0 Record parser boundary, rollback and sanitizer audit evidence 2026-10-26 09:08:24 +01:00
evgeny 7ea1fa9954 Validate instance construction and retain the global log path safely 2026-10-26 09:08:24 +01:00
evgeny 372b7d7f65 Retain removed instance callbacks until nested dispatch completes 2026-10-26 09:08:24 +01:00
evgeny e5e61723d8 Inject config read faults through fortified fread as well 2026-10-26 09:08:24 +01:00
evgeny 7fe77a49c9 Restore console diagnostics after parser dump assertions 2026-10-26 09:08:24 +01:00
evgeny 9888820146 Describe parser validation, list order and service defaults 2026-10-26 09:08:23 +01:00
evgeny 9a7091b595 Document strict parser ownership and validate provided peer keys 2026-10-26 09:08:23 +01:00
evgeny 1041849030 Reject oversized service strings and cover scalar bounds 2026-10-26 09:08:23 +01:00
evgeny eff0446f8d Align boolean settings and timer bounds across parser and updater 2026-10-26 09:08:23 +01:00
evgeny e4852c82b4 Check service scalars and proxy ports before storing configuration 2026-10-26 09:08:23 +01:00
evgeny 99d52afd3f Validate global scalar options before narrowing or timer conversion 2026-10-26 09:08:23 +01:00
evgeny 4abeafa20e Normalize malformed key duplicates before strict config verification 2026-10-26 09:08:23 +01:00
evgeny 70b7823993 Use debug and socket test APIs with their declared contracts 2026-10-26 09:08:23 +01:00
evgeny 46bf5b8541 Test parser rejection, CIDR boundaries and allocation rollback 2026-10-26 09:08:23 +01:00
evgeny d21af324ac Validate network keys before use and keep secret keys out of dumps 2026-10-26 09:08:23 +01:00
evgeny f581fa423b Reject configuration when an option handler or allocation fails 2026-10-26 09:08:23 +01:00
evgeny bff354f9cc Validate complete protocol versions, network keys and endpoint ports 2026-10-26 09:08:23 +01:00
evgeny 7593120bd9 Parse CIDR, control endpoints and storage sizes without truncation 2026-10-26 09:08:23 +01:00
evgeny 869421cc05 Audit LAN docs and enforce stream member exclusions
Document LAN transport contracts and the completed part of the stream audit. Reject owner-deleted members before profile verification and add network lifecycle coverage for source, supplier and local member removal.

Validation: seven stream checks and all 195 core tests pass; proxy 9/9, burst and load 8/8 pass.
2026-10-26 08:55:31 +01:00
evgeny 6bd4690c3b Date transport audit completion after final sanitizer checks 2026-10-26 00:01:10 +01:00
evgeny 018f8a20f3 Close transport header audit and record integration validation 2026-10-26 00:00:11 +01:00
evgeny 13f379621a Keep route ACK fault tests effective with fortified libc calls 2026-10-25 23:57:53 +01:00
evgeny 8bacf73e86 Intercept fortified recvfrom in socket monitor fault tests 2026-10-25 23:56:40 +01:00
evgeny f327f7f434 Describe REALITY codec, protected stream and relay lifecycle 2026-10-25 23:53:34 +01:00
evgeny 5e7eb6290f Test STUN recheck with an existing and a newly discovered path 2026-10-25 23:52:41 +01:00
evgeny bdf0bdf873 Close REALITY relay cleanly when event monitoring fails 2026-10-25 23:52:41 +01:00
evgeny 2f4830a3d0 Recheck each STUN path once and clarify result validity 2026-10-25 23:51:47 +01:00
evgeny f37ab0824d Complete REALITY relay half-close on socket hangup 2026-10-25 23:49:47 +01:00
evgeny 7e526521eb Document REALITY session ownership and test relay failure cleanup 2026-10-25 23:48:38 +01:00
evgeny 88b05c07d9 Harden REALITY record lifetime, relay cleanup and input contracts 2026-10-25 23:40:56 +01:00
evgeny 47bf724748 Include errno in call ring I/O fault test 2026-10-25 23:37:09 +01:00
evgeny 2949101ff8 Test per-thread call traces, counter wrap and dump I/O errors 2026-10-25 23:30:56 +01:00
evgeny c0365ebb38 Keep teardown traces per thread and retain history across wrap 2026-10-25 23:29:45 +01:00
evgeny 9a13989c7e Document SOCKS ownership and cover empty UDP and input limits 2026-10-25 23:26:24 +01:00
evgeny 36900780f2 Preserve SOCKS stream data and make control startup cancellable 2026-10-25 23:25:16 +01:00
evgeny 6660f59b3f Fix automatic endpoint ownership, retries and scan teardown 2026-10-25 23:16:17 +01:00
evgeny f8c299c2e3 Test socket monitor address loss, callbacks and init faults 2026-10-25 22:58:54 +01:00
evgeny 9296b36366 Make socket monitoring bounded and cancel pending notifications 2026-10-25 22:56:45 +01:00
evgeny fe6b79127f Record passing packet dump sanitizer validation 2026-10-25 22:46:21 +01:00
evgeny dd81f7a665 Record packet dump fix and FreeBSD socket monitor finding 2026-10-25 22:44:55 +01:00
evgeny ce30ec5fe6 Align packet dump and CRC32 contracts with implementations 2026-10-25 22:43:43 +01:00
evgeny c3fa6d4db1 Exercise packet dump limits, protocols and fragment output 2026-10-25 22:32:22 +01:00
evgeny 235c2c4f4d Bound IPv4 packet dump reads and handle fragments 2026-10-25 22:32:22 +01:00
evgeny 9fb939ff24 Document unsafe packet dump reads found during audit 2026-10-25 22:32:22 +01:00
evgeny e035f53ab6 Fix stale socket references in connectivity probes 2026-10-25 22:30:52 +01:00
evgeny 8b0002e489 Disable UTUN by default in chat frontends and retain core bootstrap transports 2026-10-25 22:10:06 +01:00
evgeny fcc4a2cf42 Ignore zero-port addresses in connection manager candidates 2026-10-25 21:01:03 +01:00
evgeny 539f09004c Use channel member snapshots for desktop participant details 2026-10-25 20:04:59 +01:00
evgeny c832a5d7a5 Unify member snapshots and fix Android member status flicker 2026-10-25 19:29:44 +01:00
evgeny f09eb81381 Fix NAT stress test time helper on Windows 2026-10-25 18:24:07 +01:00
evgeny 59b3b9bd34 Check remote and local member-file ACL refusals separately 2026-10-25 18:10:58 +01:00
evgeny b3d3df5c57 Exercise signed LAN endpoints over isolated IPv6 interfaces 2026-10-25 18:06:46 +01:00
evgeny e75bd460b8 Avoid identity reload when removing expired LAN candidates 2026-10-25 18:00:56 +01:00
evgeny 77b43363fb Run signed LAN integration separately from other network tests 2026-10-25 18:00:01 +01:00
evgeny 199839b273 Cover socket reuse and TCP LAN recovery and document lifecycle 2026-10-25 17:58:18 +01:00
evgeny 8dd28fb90a Distinguish LAN nonce failures from ordinary unknown devices 2026-10-25 17:55:35 +01:00
evgeny 94a76e569e Respect SOCKS policy and report LAN lookup failures precisely 2026-10-25 17:55:34 +01:00
evgeny 09c7e0c937 Test signed LAN discovery, replay rejection and NCD recovery 2026-10-25 17:52:08 +01:00
evgeny 469f95c596 Apply nonce-verified LAN endpoints to offline NCD transports 2026-10-25 17:47:08 +01:00
evgeny 88c2d7a4e3 Share LAN interface matching for socket announcements and NCD 2026-10-25 17:44:52 +01:00
evgeny e8713e7a2c Load known node identities for verified LAN candidates 2026-10-25 17:43:21 +01:00
evgeny 28c447370a Add signed LAN endpoint announcements and nonce proofs 2026-10-25 17:42:40 +01:00
evgeny f276fda411 Restore Android core activity after internal restart 2026-10-25 17:20:42 +01:00
evgeny c26b626c35 Handle comments consistently in desktop socket protocol settings 2026-10-25 16:35:54 +01:00
evgeny 7bcdccc4d6 Verify autosocket protocol modes across IPv4 and IPv6 lifecycle 2026-10-25 16:35:54 +01:00
evgeny fc8a06547d Test automatic socket protocol configuration and Android port reuse 2026-10-25 16:35:54 +01:00
evgeny b1a07df606 Translate automatic socket protocol settings 2026-10-25 16:35:54 +01:00
evgeny 9d465bce16 Add Android autosocket protocol selection to network settings 2026-10-25 16:35:54 +01:00
evgeny eda65ece52 Generate Android autosockets per selected protocol and retain ports 2026-10-25 16:35:54 +01:00
evgeny ae85423e39 Add automatic socket protocol selector to desktop settings 2026-10-25 16:35:54 +01:00
evgeny f3bc5acacc Persist automatic socket protocols in desktop configuration 2026-10-25 16:35:54 +01:00
evgeny 556f6e0b9e Add selectable protocols for automatic transport sockets 2026-10-25 16:35:54 +01:00
evgeny 270b9fcf80 Merge remote-tracking branch 'origin/master' 2026-10-25 16:30:55 +01:00
evgeny 288b8091ce Document targeted recovery and retain scheduler diagnostics 2026-10-25 16:26:14 +01:00
evgeny 46f6c03147 Keep requests queued by callbacks for the next scheduler batch 2026-10-25 16:19:40 +01:00
evgeny 87dd27d022 Test targeted chat synchronization across peer events and service lifecycle 2026-10-25 16:13:53 +01:00
evgeny 2eade0a7c0 Scope deferred chat synchronization to channel and peer 2026-10-25 16:11:11 +01:00
evgeny 0e17756e19 Show stream playback buffer chains on desktop and Android
Send coalesced per-hop telemetry every 200 ms ahead of video DATA using the shared ETCP transport. Measure pending frames before ETCP handoff, retain stale and unavailable node values, and display the worst path with playback tempo.

Bump stream wire protocol to v7. Validate with 187 core tests, 21 desktop player tests, two Android indicator tests, and an Android APK build.
2026-10-25 15:21:23 +01:00
evgeny 5b5221189c Buffer stream PCM before playback and recover underruns without resets 2026-10-25 12:51:19 +01:00
evgeny 677f79881c Diagnose stream audio underruns and fixture timing jitter 2026-10-25 12:30:30 +01:00
evgeny 3ffacbfe37 Document stream recovery validation and existing Qt audio flake 2026-10-25 12:06:35 +01:00
evgeny c3cf86b9e0 Recover streams after five seconds and start from complete keys
Preserve queued media under backpressure, advertise retained keyframe ranges in stream wire v6, and reopen stalled subflows without resetting playback.

Cover large fragmented keys, sustained slow delivery, missing subflows, buffer eviction and stale bootstrap snapshots in integration regressions.
2026-10-25 12:03:49 +01:00
evgeny 4a785c2598 Improve stream recovery, delivery diagnostics and buffer indicators
Preserve essential FEC parts under pressure, coordinate stale-data gaps and reopen source subflows in parallel after transport failures.

Add per-peer network and playback diagnostics, Android and desktop buffer indicators, and regression coverage for Wi-Fi switching and congestion.
2026-10-25 01:18:02 +02:00
evgeny b7d74d8f87 Verify every NCD control ID byte and clarify new-socket scope 2026-10-24 20:27:57 +02:00
evgeny db0d7a1f47 Document NCD startup rollback reentrant lifetime and address snapshots 2026-10-24 20:24:22 +02:00
evgeny 9844fdefcb Align automatic socket documentation with shared NCD link matching 2026-10-24 20:24:22 +02:00
evgeny a353797edb Specify NCD ownership callbacks matching and fixed little-endian control wire 2026-10-24 20:22:45 +02:00
evgeny b83a28becc Keep NAT remeasurement fixture isolated from new direct helper links 2026-10-24 20:20:09 +02:00
evgeny a3f004772a Align ETCP fixtures with NCD identity and failed-open contracts 2026-10-24 20:20:09 +02:00
evgeny 702389d8d9 Retain NCD entry and transport through synchronous CLOSE callbacks 2026-10-24 20:18:15 +02:00
evgeny 88004f11ca Trigger NCD CLOSE on an empty unsuspended queue 2026-10-24 20:17:13 +02:00
evgeny 1f6f815917 Reproduce NCD teardown during synchronous CLOSE queue notification 2026-10-24 20:15:30 +02:00
evgeny 2a7266e17f Check IPv6 scope on the matching NCD link 2026-10-24 20:15:30 +02:00
evgeny e137778ba2 Exercise IPv6 REALITY guards and reentrant node snapshot replacement 2026-10-24 20:15:30 +02:00
evgeny 496a3c788e Cover NCD candidate policy constructor cancellation and nested removal 2026-10-24 20:15:30 +02:00
evgeny 772c0728e1 Use one NCD address matcher for opens updates and new sockets 2026-10-24 20:15:30 +02:00
evgeny b23d5e6047 Unify NCD opens and retain owners across failed startup and callbacks 2026-10-24 20:15:30 +02:00
evgeny 4695fe4371 Register NCD transport events atomically and reuse ETCP close lifecycle 2026-10-24 20:15:30 +02:00
evgeny 28c951ed9e Reject stale NCD notifications and validate complete control frames 2026-10-24 20:15:30 +02:00
evgeny 2cc5a6df91 Reproduce NCD startup failures stale events and malformed control 2026-10-24 20:15:30 +02:00
evgeny 10d6ddee38 Wait for target discovery before member file network transfers 2026-10-24 20:15:30 +02:00
evgeny 4314dff390 Expose CM and topology failures in member file network diagnostics 2026-10-24 20:14:58 +02:00
evgeny c311bb7a1f Prevent deferred CM readiness from restarting cancelled file transfers 2026-10-24 20:14:58 +02:00
evgeny b387c4b127 Preserve unowned member download temporaries on refusal and cancellation 2026-10-24 20:14:29 +02:00
evgeny 6d6afb4faa Align FULL and inline loss counters in dummynet documentation 2026-10-24 19:57:24 +02:00
evgeny e996e09f6e Specify dummynet datagram timing reconfiguration and filter ownership contracts 2026-10-24 19:57:24 +02:00
evgeny 8d035a065a Cover partial dummynet startup rollback without resource leaks 2026-10-24 19:57:24 +02:00
evgeny 0df339c384 Verify inline filters with real UDP delivery and independent ETCP teardown 2026-10-24 19:57:24 +02:00
evgeny c341a9b6cc Record passing full validation after ETCP diagnostics merge 2026-10-24 19:45:37 +02:00
evgeny 897cc007e1 Merge current master and reconcile completed member-files fixes 2026-10-24 19:38:40 +02:00
evgeny 79e37f64f8 Document member files audit findings and verified fixes 2026-10-24 18:47:06 +02:00
evgeny 4d729a2ff0 docs(member-files): align lifecycle and receipt retry behavior 2026-10-24 18:47:06 +02:00
evgeny 4f96926f6b docs(member-files): complete public request and transfer contracts 2026-10-24 18:47:06 +02:00
evgeny c1da55ad87 docs(member-files): clarify storage lifecycle and internal owners 2026-10-24 18:47:06 +02:00
evgeny f7b064a11a docs(member-files): specify wire codec and worker ownership 2026-10-24 18:47:06 +02:00
evgeny 41f0aa2439 fix(member-files): reject local FIFO without blocking disk worker 2026-10-24 18:47:06 +02:00
evgeny 1c1b2b2026 test(member-files): detect blocking open of local FIFO sources 2026-10-24 18:47:06 +02:00
evgeny 488577ac0e test(member-files): verify timed replay and bulk removal 2026-10-24 18:47:06 +02:00
evgeny 4d40b6f5f7 fix(member-files): bound busy retries by receipt lifetime 2026-10-24 18:47:06 +02:00
evgeny 2d8384830d test(member-files): cover bulk mutations and lost result replay 2026-10-24 18:47:06 +02:00
evgeny 0c8e9eef3c fix(member-files): retry busy mutations within replay guarantees 2026-10-24 18:47:06 +02:00
evgeny 20b4975d57 fix(member-files): prevent restart after early transfer cancellation 2026-10-24 18:47:06 +02:00
evgeny 0f42f92feb fix(member-files): clean up only owned download temporaries 2026-10-24 18:47:06 +02:00
evgeny 0b2dac3c5b test(member-files): retain route precondition after ABI diagnosis 2026-10-24 18:47:06 +02:00
evgeny 5df6d0c8f3 test(member-files): log startup state before transfer 2026-10-24 18:47:06 +02:00
1041 changed files with 99594 additions and 27818 deletions

No files matched your search

+2
View File
@@ -77,6 +77,8 @@ callgrind.out.*
tests/*.log
tests/*.trs
tests/logs/
/db_a/
/db_b/
# Mass update logs
logs/
-1
View File
@@ -843,7 +843,6 @@ cmake .. && make -j4
| `ptt_hotkey_manager.h/cpp` | Глобальный PTT: физические клавиши Windows/X11, системный портал Wayland |
| `ptt_key_chord.h` | Хранение физических сочетаний с отдельными левыми/правыми модификаторами и запись одновременного нажатия |
| `ptt_portal.h/cpp` | D-Bus-сессия GlobalShortcuts для Wayland, настройка сочетаний и обработка нажатий/отпусканий |
| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов |
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
| `videoplayer_window.h/cpp` | Окно просмотра видео |
+1 -1
View File
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
cd src
EXE_NEEDS_REBUILD=0
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_error.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
for src in $CORE_SOURCES; do
if [ -f "$src" ]; then
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+473
View File
@@ -0,0 +1,473 @@
BLE GATT → ETCP (chatgui desktop + Android)
Текущее состояние (29.10.2026):
Write Without Response/Notify, HIGH и PHY2M включены. BLE version3 добавляет
16-bit ID каждого DATA GATT-value, накопительный ACK по отдельной характеристике
не чаще15ms и окно12500байт на направление (фиксированные2Мбит/с ×50ms).
Локальный completion не возвращает кредит. Формат и таймауты — doc/ble_transport.txt.
Предыдущие исправления готовности BlueZ, старого ManufacturerData, incoming HIGH
и потолка DATA RTO сохранены. Ограничение очереди само по себе не считается
подтверждённым исправлением всей ETCP-нагрузки; аппаратные результаты ниже.
Общий код: tools/cgathui_common/ble_etcp.c/h.
Платформенные backend остаются в lib/ble и android_ble.c; Qt не участвует в протоколе.
UTUN binary этот модуль не собирает. При активном UTUN-сервисе общего instance
BLE-драйвер не передаёт ETCP: CHAT и UTUN используют общий conn/normalizer,
поэтому разделить их уже упакованные пакеты внутри линка нельзя.
Обнаружение и объявления при этом остаются доступны.
Владелец в потоке uasync:
ble_etcp_create(ua, instance, &config)
ble_start(ble_etcp_radio(driver))
ble_advertise / ble_set_incoming — через заимствованный radio
ble_etcp_neighbors(driver, output, capacity) — копия свежих соседей
ble_etcp_connect(driver, node_id) — явное подключение к обнаруженному соседу
ble_etcp_destroy(driver) — обязательно до instance/uasync, вне callbacks
instance=NULL оставляет сырой BLE и discovery для аппаратных проб; штатные
frontend передают instance. config.callbacks — наблюдатели; received/writable
штатного ETCP принадлежат драйверу. Сырые TX/RX нельзя читать параллельно ETCP.
Таблица в управляющей структуре драйвера, не в instance и не в UI:
64 записи {node_id, UTF-8 name, endpoint, RSSI, seen_us}; срок свежести 30 секунд.
Объявление обновляет адрес и RSSI. По истечении срока запись удаляется.
Desktop формирует видимый список из snapshot общего драйвера. Android получает
тот же C API через android_ble_driver(). node_id/name в эфире не аутентифицированы.
Автоматическое подключение проверяется раз в секунду и при появлении соседа:
- только работающий CHAT-сервис, полная разрешённая запись мембера source=0/deleted=0;
- нет живого BGP-пути в конкретной группе, включая маршрут через посредника;
- группа получает TOPO_PEER_REQUEST и владеет NCD уже в CONNECTING;
- GATT прикрепляется к тому же ETCP_CONN, которым владеет NCD;
- один GATT к обнаруженному node_id разделяется запросами нескольких групп;
- запрос снимается на READY, удалении группы/членства либо через 20 секунд;
готовую сессию продолжает держать сама группа;
- повтор GATT ограничен интервалом 5..6 секунд; появление объявления само по себе
не создаёт подключения к постороннему узлу.
Каждый GATT-канал предоставляет packet_link_driver: ctx/ua/name, bind/get_state,
send/set_rx_enabled/stop/destroy. Контракт — doc/packet_link.txt.
max_packet=1500 — размер непрозрачного пакета BLE, включая заголовки ETCP.
Более длинное пользовательское сообщение дробит нормализатор ETCP; получившиеся
пакеты BLE транспорт дополнительно фрагментирует под фактический ATT MTU.
TX limit=1 включает пакет в процессе фрагментации. У адаптера нет второй TX-очереди:
send сразу передаёт ll_entry в ble_tx_put. Готовность читается через ble_tx_ready;
после завершения целого пакета writable будит ETCP. При отказе владение не меняется.
Дополнительно ble_tx_ready учитывает ACK-кредит: после локальной отправки целого
пакета writable выдаётся, только если есть место в удалённом окне. При полном
окне ACK освобождает кредит и будит отправителя; INIT/keepalive проходят тот же лимит.
RX использует ll_queue общего транспорта и включается packet_link потребителем.
DATA передаётся через Write Without Response (client → server) и Notification
(server → client). BLE send завершает только локальную отправку; доставку и повторы
обеспечивает ETCP. INVITE остаётся отдельным Write Request/Indication каналом.
BLE ACK подтверждает приём GATT-value драйвером, не доставку целого ETCP-пакета.
После BLE HELLO/ACCEPT первым целым кадром каждой стороны идёт 40 байт:
'V' 'C' 'E' 'T' 01 00 00 00 | 32 байта X25519 public key
Драйвер сверяет производный node_id с HELLO и ключом уже существующего ETCP_CONN,
затем прикрепляет динамический линк. Это ещё не доказательство владения ключом:
его обеспечивает штатный зашифрованный INIT и handshake эпох ETCP. Следующие
кадры — неизменённые зашифрованные ETCP-пакеты, без IP-заголовков/добавочного framing.
До завершения обмена ключами действует дедлайн 20 секунд.
Диагностика линка в обеих GUI использует общий etcp_link_get_snapshot: подпись BLE,
физическое направление из ble_get_outgoing и фактические link_state/link_status
ETCP. GATT READY сам по себе не включает зелёный индикатор. Адреса IP, порты и имя
сетевого сокета для BLE отсутствуют. До прикрепления к ETCP попытка GATT не входит
в список ETCP-линков; протокольный is_server не подменяется направлением GATT.
BLE callbacks только обновляют состояние/будят отложенную обработку. Закрытие
GATT делается вне этих callbacks; packet_link откладывает destroy. При выключении
frontend закрывает свои ETCP-линки, отсоединяет ctx от manager и уничтожает радио;
поздний destroy ctx не обращается к уже уничтоженному manager. Ключи не логируются.
Категория диагностики discovery; для handshake/маршрутов — etcp, connection, bgp, ncd.
Проверки:
- tools/chatgui/build/test_ble_etcp: настоящий BLE common protocol + fake async GATT,
encrypted ETCP handshake, MTU23, 1500-byte duplex, потеря DATA после успешного
локального send и восстановление ETCP, TX busy/ownership/resume,
несовпадающий public key, teardown, реальные SQLite/NCD/BGP и READY двух групп.
- test_desktop_ble: UI policy и discovery через общий snapshot.
- lib/ble/tests/test_ble_transport.c: MTU23/247/517, пропущенные фрагменты,
bounded queues, независимые соединения и stop.
- Android: ./gradlew assembleDebug assembleDebugAndroidTest.
Ручная аппаратная проверка (один процесс VibeChat/probe на BLE-адаптер):
adb -s SERIAL shell am instrument -w -e etcp true -e seconds 180 \
com.utun.chat.test/com.utun.chat.BlePeerProbe
tools/chatgui/build/ble_etcp_probe 150
Обратное направление: instrumentation -e initiator true, desktop ... 150 listen.
ETCP-probe объявляет тестовое имя; desktop подключается только к нему.
Временно включается ETCP_ID_BLE_PROBE echo, выключенный в обычном приложении.
32 пакета по 1500 байт сверяются побайтно; успех — RESULT ... verified=32 failed=0.
Instrumentation восстанавливает Bluetooth-политику в finally. PHY2M и HIGH priority
запрашивает native backend; фактические параметры/результат запроса смотреть в логах.
Нагрузочная проба: ble_etcp_probe seconds [connect|listen] [data|invites] [packets] [window].
По умолчанию packets=32/window=1. Для заполнения канала: например,
tools/chatgui/build/ble_etcp_probe 90 listen data 4096 16
и Android с -e etcp true -e initiator true -e seconds 100.
Обратная роль: desktop connect и Android без initiator. В полёте не более window
запросов (1..64); каждый из packets (1..1000000) сверяется побайтно.
THROUGHPUT считает verified_bytes_each_direction и aggregate_bytes_per_second
за время от первого send до последнего проверенного echo, без GATT/ETCP handshake.
Это двунаправленный echo goodput, не скорость PHY. Неполный прогон имеет ненулевой exit.
При неполном прогоне THROUGHPUT complete=0 учитывает всё окно до остановки,
включая простой после последнего echo; RESULT failed=1 и ERROR содержат target/sent/verified.
До первого send разрыв/смена адреса возвращает пробу к discovery; разрыв во время
передачи завершает её с ошибкой. UTUN_TEST_DEBUG=1 включает ETCP trace/connection debug
на desktop для подробного разбора ACK/RTO без изменения штатных фильтров приложения.
Перед пробой выключить BLE обычного desktop GUI: два сервиса с одинаковыми UUID
на одном адаптере делают GATT discovery неоднозначным.
Проверено 2026-10-29:
- desktop CMake и Android arm64-v8a/x86_64 APK собираются;
- ./check.sh: 230/230 + proxy/burst/load; выбранные GUI/ETCP/BLE: 6/6;
- test_ble_etcp под ASAN/UBSAN с detect_leaks=1: без ошибок;
- Android (RZ8R72MG64B) инициирует GATT к Linux: 32×1500 байт echo совпали,
PHY2M, MTU517, фактический connection interval=12 (15 ms);
- Linux инициирует GATT к тому же Android: несколько ETCP-пакетов передаются,
затем HCI Connection Timeout (0x08). Сырой ble_peer_probe без ETCP также
воспроизводит HCI 0x08 (подтверждено btmon). Этот сбой нижнего BLE-стека
не устранён данным драйвером; обе аппаратные роли нельзя считать проверенными.
Приглашения через Bluetooth
---------------------------
Общий владелец регистрирует заимствованный chat_local_provider в instance;
сам список соседей остаётся в ble_etcp. chat_local объединяет LAN и BT по node_id.
При отправке выбирается доступный LAN endpoint, иначе BLE; при синхронном отказе
LAN допустим BLE. После принятой отправки транспорт сам выдаёт один конечный
статус: асинхронный таймаут не запускает рассылку по другому транспорту.
Явная отправка приглашения создаёт GATT при необходимости, без проверки членства
или BGP-пути. При активном UTUN приглашения доступны без добавления BLE в ETCP.
Характеристика f61a9023-4abc-45ac-98e1-761df4c13811: Write + Indicate.
Клиент пишет запросы/ответы в неё; сервер адресует indications одному клиенту.
DATA и INVITE имеют отдельные ll_queue и сборщики фрагментов, но одну незавершённую
GATT-операцию на соединение. Планировщик чередует фрагменты, HELLO имеет приоритет.
DATA TX использует tx_limit, INVITE TX допускает один целый пакет; RX ограничены
64 пакетами на канал. Необходимы подписки на обе характеристики.
После сборки INVITE: version(1)=1, kind(1)=1 запрос/2 ответ,
epoch(8 BE), request_id(8 BE), status(1), payload.
Эпоха случайная ненулевая на экземпляр драйвера. Запрос содержит готовую join-ссылку
1..1100 байт (тот же предел, что LAN), status=0. Ответ без payload: status=0 FAILED,
1 RECEIVED, 2 FORBIDDEN. RECEIVED означает доставку в обработчик приложения,
не вступление в группу. ATT completion сам по себе доставку приложению не подтверждает.
Ожидающих отправок до 8, дедлайн 30 с, до 3 попыток с интервалом 8 с.
Разрыв соединения/остановка завершают pending с FAILED. Повторы принятого запроса
подавляются 60 с по peer/epoch/request и SHA256 содержимого (кеш 64 записи).
Заполненный кеш отказывает новым запросам, не вытесняя ещё действующую защиту.
Отправитель не получает гарантии отсутствия доставки при FAILED/потере ACK.
[chatserver] bluetooth_invites_enabled=0 — отдельное разрешение, по умолчанию выключено.
Проверяется при каждом запросе, в том числе на существующем GATT. Не влияет на DATA,
LAN-приглашения или исходящую отправку. Новому входящему GATT также нужно разрешение
«Allow incoming Bluetooth connections». Desktop и Android показывают переключатель
во вкладке Bluetooth. JOIN выполняется только по подтверждению пользователя.
Содержимое приглашения и пароль не выводятся в логи (категория discovery).
Формат join-ссылки не изменён: для последующего JOIN по-прежнему нужны её IP-адреса.
Проверка: test_ble_transport (MTU23/247/517, независимые каналы), test_ble_bluez
(два клиента, адресные fd), test_ble_etcp (приложение/ACK, запрет, повторы,
потеря ACK, совместный ETCP, teardown и GATT по запросу без ETCP).
Физическая проба: ble_etcp_probe 60 listen invites; Android BlePeerProbe
с -e etcp true -e initiator true -e invites true -e seconds 50.
Проба использует синтетическую ссылку, не запускает JOIN и восстанавливает политику.
Проверено 2026-10-29 на Android RZ8R72MG64B и Linux/BlueZ:
- Android инициирует GATT: приглашение 939 байт туда/обратно, ACK обоих приложений;
параллельно проверены все 32 ETCP echo-пакета по 1500 байт.
- Desktop инициирует GATT: те же приглашения и ACK в обе стороны; за окно 45 с
успели завершиться 7/32 ETCP echo-пакетов, затем проба остановлена по лимиту времени.
Это не полный нагрузочный проход обратной роли.
- Windows backend проверен кросс-компиляцией MinGW/C++WinRT, без физического Win BT.
- Общие тесты: 230/230; отдельные BLE/BlueZ/UI и ASAN/UBSAN пройдены.
Write Without Response / Notification: проверка 2026-10-29
--------------------------------------------------------
Android, BlueZ и WinRT используют быстрый DATA, INVITE сохраняет Request/Indication.
Android HIGH/MTU517/2M сохранены; WinRT запрашивает ThroughputOptimized при наличии
API Windows 11. BlueZ D-Bus не предоставляет аналогичного per-link профиля.
Desktop GUI/Linux, Android APK+instrumentation (arm64/x86_64), WinRT cross-build PASS.
./check.sh: 230/230 + proxy/burst/load; BLE common/BlueZ/identity/ETCP/UI: 5/5;
BLE ASAN/UBSAN: 3/3; Android app/router unit tests: 770/770.
Телефон RZ8R72MG64B, один desktop BLE-сервис на адаптере:
- Android central: 512/512 ETCP echo по 1500 байт, window=16, по 768000 байт
в каждом направлении за 61.694 с; суммарный goodput 24897 B/s (12448 B/s
на направление). Приглашения 939 байт и ACK приложений прошли в обе стороны.
PHY2M TX/RX, MTU517; для Android central подтверждён interval=12 (15 ms).
- Сырой BLE, Android central: 96/96 echo, по 45636 байт за 4.473 с,
суммарно 20407 B/s. Размеры пакетов различаются, напрямую с ETCP не сравнивать.
- Desktop central: приглашения/ACK прошли, но за окно 85 с ETCP подтвердил
только 20/512 echo. Повтор без приглашений, window=1: 0/32 echo за окно 65 с,
после первого send 50.060 с повторялись INIT/INIT_RESPONSE и DATA RTO,
DATA_RTT_us=0; BLE-сборщик не регистрировал отбросов. Причина не установлена;
исправление этого сбоя не входит в подтверждённый результат профиля скорости.
Журналы текущего сеанса: /tmp/ble-speed-android-central-{desktop,logcat}.log,
/tmp/ble-speed-desktop-central-{desktop,logcat}.log,
/tmp/ble-speed-desktop-central-serial-retry-desktop.log и
/tmp/ble-speed-desktop-central-serial-live-logcat.log.
После аппаратных проб обычный desktop VibeChat перезапущен новой сборкой;
Android instrumentation восстановила обычную Bluetooth-политику в finally.
Диагностика очередей и аудит ошибок 2026-10-29
---------------------------------------------
Последующие пробы уточнили причину просадки desktop central. Локальный completion
WriteValue(type=command) означает приём фрагмента стеком, а не его передачу по радио.
tx_limit=1 ограничивает только нашу ll_queue; очередь BlueZ/ядра этим не ограничена.
Доказательства по журналам, без сравнения часов разных устройств:
- /tmp/ble-errors-final-hci.btsnoop и /tmp/ble-speed-errors-final-desktop.log:
сопоставлены 882 исходящих целых кадра по seq/размеру/FNV-отпечатку ciphertext.
От локального BLE completion до последнего ACL-фрагмента в HCI проходит
3.548..20.167 с; следовательно, очередь находится перед границей HCI TX.
Для 675 совпавших входящих кадров HCI RX → BLE callback занимает 0.6..32.9 мс,
обычно 1..2 мс. Часовой сдвиг форматов времени исключён по HCI RX/callback.
- ETCP в этой пробе повторяет DATA по RTO=1000000 us, пока исходные кадры
ещё находятся в нижней очереди. Повторные копии увеличивают задержку.
Начальный INIT также повторяется примерно через 50 мс и содержит padding.
- Результат контрольного запуска: 42/128 echo по 1500 байт, window=16,
63000 подтверждённых байт на направление за 65.730 с после первого send,
complete=0; общий лимит запуска 75 с. drops=0 и missing=0 на BLE-сборщике.
- Более ранний cause-load1: desktop локально завершил 233 кадра, Android получил
только первые 40; обратно завершено 262, получены первые 76. У полученных
кадров совпали размеры/отпечатки, внутренних разрывов нет — отсутствуют хвосты.
Дополнительная задержка относительно первого кадра выросла на 15.982/10.012 с.
- Сбой перемежающийся: отдельная проба desktop central window=1 прошла 32/32
за 11.148 с, суммарно 8612 B/s. Это не подтверждает устойчивость под нагрузкой.
В контрольной пробе Android также каждые ~6 с подключался к соседу
4A:2F:B0:6D:A6:35 со старыми свойствами RX=8 (Write), TX=32 (Indicate).
Сервис найден со status=0, но не поддерживает новые DATA-свойства. Причина отказа
теперь явно указывает отсутствующий Write Without Response/Notification.
Влияние этих параллельных подключений на скорость отдельно не измерено.
Локальный completion недостаточен для оценки фактического продвижения кадров.
До изменения wire-протокола необходимо проверять готовность GATT, профиль
соединения и поведение ETCP при RTT больше секунды. Последующее расследование
ниже подтвердило ошибки готовности/профиля и воспроизвело очередь без ETCP.
Произвольная задержка между send и смена на AcquireWrite сами по себе не дают
гарантии ограниченной очереди. Дополнительный credit-протокол пока не вводился.
Аудит диагностики:
- Android: ошибки SDK/callback сохраняют status, роль/адрес/link/channel;
отказы CCC сохраняют код API33, MTU — requested/reported/effective;
неожиданные закрытия/отклонённые чтения/записи имеют WARN, потерянный callback
или таймер Handler — ERROR, timeout server writer содержит его состояние.
- JNI: переполнение mailbox содержит generation/capacity/discarded;
поздние события удалённого link видны на TRACE. Тестовые уровни discovery/debug
восстанавливаются при detach и не остаются включёнными после instrumentation.
- BLE common: разрывы последовательности целых кадров учитываются отдельно
как missing; partial/drop не смешивается с ними. Некорректные фрагменты,
замена неполного кадра, отказ allocation и completion имеют контекст ошибки.
- BlueZ: ошибки recv/reply/socketpair сохраняют errno/текст; D-Bus ошибки
сохраняют имя и сообщение. WinRT: HRESULT и ATT_error/status доступны при
WARN, включая discovery/subscription, без необходимости включать DEBUG.
- ETCP: проверяются RNG salt/sc_encrypt и фактические результаты отправки
INIT/keepalive; busy — DEBUG, отказ — WARN/ERROR. Успешный лог называется
submitted, поскольку он не подтверждает доставку получателю.
Для сопоставления кадров и BLE ACK: debug=debug; для сводок очередей — discovery=debug.
Категория debug содержит DATA TX/RX с seq каждого GATT-пакета, ACK TX/RX с ID и кредитом,
а также wire TX/RX с отдельным seq, длиной и отпечатком целого кадра.
Для RTO/ACK ETCP — etcp=trace, connection=debug, keepalive=debug.
На desktop UTUN_TEST_DEBUG=1 включает trace диагностической категории;
Android BlePeerProbe включает её на время пробы. Ключи/ссылки приглашений не выводятся.
Проверки после аудита: C-ядро, desktop GUI/probe, Android APK/instrumentation
для arm64-v8a/x86_64 и WinRT cross-build собираются. BLE transport/identity/BlueZ,
четыре desktop packet-link/BLE теста и ETCP connect/packet_link/session проходят.
HCI можно снимать через sudo btmon -w FILE; обычному пользователю monitor запрещён.
Обычный desktop VibeChat после пробы восстановлен, политика Android восстановлена.
Причина длительного ожидания и исправление входящего HIGH
---------------------------------------------------------
Это очередь перед HCI TX, а не единая пауза ожидания PHY negotiation:
- /tmp/ble-ready-baseline-hci.log: ACL Connected success, MTU517, LL TX/RX=251,
PHY TX/RX=2M success до отправки ETCP INIT. Linux central выбрал interval=45ms,
latency=0 и supervision timeout=420ms. В этой пробе также был HCI timeout 0x08.
- В Android HIGH запрашивался только из исходящего BluetoothGatt callback.
Входящий GATT server запрашивал 2M, но не connection priority, поэтому Linux
central сохранял медленный профиль. PHY2M сам по себе interval не сокращает.
- /tmp/ble-ready-raw-{hci,desktop}.log: без ETCP и его повторов прошли все 96
raw echo; по 45636 байт на направление за 27.585s, суммарно 3309B/s.
Все 96 кадров сопоставлены с HCI по seq/длине/отпечатку. Последний исходящий
кадр попал в HCI через 26.035s после локального completion, входящие callback
следовали за HCI RX через 1..2.7ms. Следовательно, ETCP не является необходимым
условием возникновения очереди: стек принимает burst быстрее, чем обслуживает ACL.
Контроллер сообщил 6 LE TX-буферов по 251 байту; по Completed Packets они заняты.
- При изменении interval 45→7.5ms в отдельной пробе частота HCI TX увеличилась
примерно в шесть раз. После обновления параметры подтверждены событием
LE Connection Update Complete status=0; долговременного pending negotiation нет.
hcitool lecup имеет собственные особенности ожидания результата/длины event:
вывод команды и её exit недостаточны, согласование проверяется по HCI event.
- ETCP усиливает перегрузку: INIT вначале повторяется примерно каждые 50ms
с padding; DATA RTO ограничен 1s даже при уже измеренном KA RTT=5..6s.
В результате повтор может попасть в очередь раньше физической отправки оригинала.
На этапе исправления incoming HIGH алгоритмы INIT/RTO ещё не менялись;
последующее исправление RTO описано ниже.
Отдельно найден мешающий discovery: D-Bus sender :1.70, PID3047 kdeconnectd,
каждые 60s устанавливает Transport=bredr. BlueZ объединяет его с нашим Transport=le,
и контроллер выполняет Inquiry по 10.24s почти непрерывно. Штатное disable-backend
объявило BluetoothLinkProvider disabled, но не остановило старую discovery-сессию.
В изолированной пробе потребовался краткий restart KDE Connect с этим backend
выключенным; отсутствие Inquiry подтверждено HCI до восстановления backend.
Медленный drain/повторы сохранились и без Inquiry: это дополнительный фактор,
а не достаточное объяснение всех задержек. Backend KDE Connect затем возвращён
в enabled; его LAN backend и постоянные настройки Bluetooth не менялись.
Исправления:
- BlueZ не открывает кешированные UUID до Connected=1 и ServicesResolved=1.
После resolve повторно читается snapshot/MTU. Регрессионный private-bus тест
с доступным кешем и ServicesResolved=false падал до исправления, теперь проходит;
после сигнала resolve используется актуальный MTU517 вместо прежнего MTU23.
В аппаратной пробе видны оба snapshot: connected=1/resolved=0, затем resolved=1.
- GetManagedObjects используется только как metadata snapshot: закешированное
ManufacturerData больше не обновляет discovery/last_seen. Иначе старый RPA
телефона мог выдаваться за актуальный endpoint. Новые объявления поступают
только из live InterfacesAdded/PropertiesChanged. Регрессионный тест со старым
ManufacturerData в snapshot падал до исправления и проходит после него.
- Android после первой входящей CCC открывает BluetoothGatt handle к тому же ACL
исключительно для requestConnectionPriority(HIGH). У BluetoothGattServer нет
соответствующего публичного метода. Новый handle не выполняет discovery/DATA,
хранится у входящего Link и закрывается при его teardown. Ошибка настройки
логируется WARN и не отменяет работоспособный GATT server.
- /tmp/ble-ready-priority-fixed-{hci,logcat}.log: incoming HIGH accepted=true;
контроллер получил remote request 11.25..15ms, latency=0, timeout=5000ms,
Linux ответил success, Update Complete success установил interval=15ms.
PHY2M/MTU517 сохранены. Клиентский handle переиспользовал существующий ACL.
Ручной HCI настройки и отключения KDE Connect в этой пробе не было.
- В этой нагрузочной пробе сопоставлены 715 TX кадров: локальный completion→HCI
занял до 1.713s вместо ранее измеренных 20..26s. У 684 входящих кадров
HCI→callback занял 0.8..2.8ms. Очередь существенно уменьшилась, остаточные
задержки и недостаточный для такого RTT RTO требуют отдельной работы.
За окно 80s подтверждено 266/512 echo, complete=0, после первого send 70.876s,
суммарно 11259B/s. Это неполная нагрузочная проба, не успешные 512/512.
Дополнительные журналы: /tmp/ble-ready-isolated-*, /tmp/ble-ready-quiet-*,
/tmp/ble-ready-priority-check-*, /tmp/ble-ready-dbus.log.
Поздние повторения ETCP с HIGH всё ещё нестабильны: 115/128 и 27/128 за разные
окна; один профиль не устранил проблему ETCP целиком, все такие пробы complete=0.
Чистое радио после HIGH: /tmp/ble-ready-raw-noscan-fresh-* (несмотря на имя файла,
pauseScan в аргументах этого запуска НЕ было, scanning оставался включённым):
96/96, по 45636 байт за 4.021s, суммарно 22700B/s. У 96 совпавших исходящих
кадров очередь до HCI достигала 3.753s: большой burst всё ещё буферизуется.
/tmp/ble-ready-raw-noscan-valid-*: scanning Android остановлен с явным логом,
96/96 за 10.620s, суммарно 8594B/s. Остановка scanning не дала ускорения;
одиночная парная проба не доказывает отсутствие любого его влияния.
Обе пробы начинались после сброса оставшегося ACL предыдущего теста/GUI.
Первые две попытки сравнения прервались до передачи с le-connection-abort-by-local
и не учитываются. Raw probe теперь повторяет discovery после разрыва до первого
send; разрыв после начала передачи остаётся ошибкой.
Радио-проба умеет выбирать точный node_id, чтобы не подключаться к соседнему
устройству со старым протоколом:
ble_peer_probe connect 80 1657b3ea8281c0a8
UTUN_TEST_DEBUG=1 включает отпечатки кадров также в raw probe.
Проверки кода: C-ядро, desktop GUI/probes, Android APK/instrumentation arm64/x86_64
собираются; Android unit tasks, private-bus BlueZ и четыре desktop BLE/packet-link
теста проходят. Пробы восстанавливают GUI, Android-политику и временное состояние
KDE Connect. Сборку APK и запуск instrumentation нужно выполнять последовательно:
одна проба во время установки завершилась Process crashed и не учитывается.
Изоляция старого протокола и RTO при RTT больше секунды
------------------------------------------------------
Сосед 1588291579282de3 публиковал DATA RX=Write/TX=Indicate. Android пытался
подключиться к нему примерно каждые 6s и обнаруживал несовместимые свойства.
Новый wire-profile требует объявления VC/version=2; версия 1 отклоняется
на discovery, до connectGatt. JNI передаёт scan prefix из общего C codec,
Kotlin не хранит отдельную версию. Нужны новые сборки на обоих пирах.
В /tmp/ble-ready-priority-v2-* нет этих подключений: 128/128 echo за10.623s,
суммарно36149B/s, TX queue до0.554s. Длинные повторения остались нестабильны:
/tmp/ble-ready-desktop-load-v2-* 49/512, /tmp/ble-ready-android-central-v2-* 439/512.
В desktop-load-v2 при KA_RTT_us=2025400 повтор DATA назначался через1000000us;
далее наблюдался RTT2..6s с тем же потолком. Попытки удалялись раньше ACK,
повторы увеличивали очередь и мешали получить действующие DATA/BBR samples.
etcp_tx_rto_us теперь сохраняет ceil(1.2*max(DATA_RTT,KA_RTT)+1.5*jitter+3000us),
минимум10ms и начальное1s без RTT; ограничение сверху — представимость uint32.
Переполнение формулы считается в uint64 и логируется WARN при насыщении.
test_etcp_windows проходит все63 сценария, включая RTT2s/ACK1.5s без повтора,
таймер7.8672s и предельные значения. Формат DATA/ACK и модель BBR не менялись.
Аппаратная проба после исправления: /tmp/ble-ready-rto-desktop-v2-*.
MTU517, HIGH принят, PHY2M. За71.328s подтверждено183/512 echo, complete=0,
суммарно7697B/s; это НЕ успешный нагрузочный проход. Сопоставлены534 TX кадра:
local completion→HCI до4.583s; у503 RX кадров HCI→callback0.7..2.5ms.
До первого INIT_RESPONSE отправлены24 INIT (вначале каждые~50ms), после готовности
линия продолжает передавать кадры. Исправление RTO не ограничивает очередь ОС.
Начальный RTO без RTT всё ещё1s, и в этой пробе он сработал до первого измерения.
Нужен отдельный выбор ограничения стартовых INIT/нижней TX-очереди; изменение
одного таймера не считается подтверждённым устранением всех задержек BLE.
При полном check обнаружена некорректная фикстура test_etcp_server_lifecycle:
синтетический линк MTU=0 не мог отправлять keepalive, но тест ожидал роста счётчика.
После аудита ошибок счётчик увеличивается только при принятии пакета транспортом.
Фикстура теперь явно задаёт результат отправки через codec без сетевого I/O;
добавлена проверка, что busy/error сохраняют счётчик, а успешная отправка увеличивает.
Обратная роль после RTO: /tmp/ble-ready-rto-android-central-v2-repeat-*.
За76.272s подтверждено475/512 echo, complete=0, суммарно18683B/s. У1133 TX кадров
local completion→HCI до1.497s; у1111 RX кадров HCI→callback0.3..2.6ms.
Первая попытка /tmp/ble-ready-rto-android-central-v2-* получила Android GATT133
до открытия/передачи; она не является измерением скорости. Повтор выполнил обмен,
но также не закончил512 пакетов в заданном окне80s. Начальная настройка HIGH/PHY
асинхронна: запрос не подтверждает её завершения, фактический результат виден в HCI.
Длинная очередь остаётся и после успешного LE Connection Update Complete.
Итоговые проверки: ./check.sh230/230, proxy/burst/load PASS (178s);
test_etcp_windows63 сценария и исправленный server_lifecycle PASS. Desktop GUI/probe
и Android APK/instrumentation arm64/x86_64 пересобраны; APK установлен на A52.
Обычный desktop VibeChat и Android-политика после проб восстановлены.
BLE version3: ACK и ограничение очереди (29.10.2026)
---------------------------------------------------
Общий C-драйвер добавляет flow_id BE16 к каждому DATA GATT-value и отдельную
ACK-характеристику f61a9024-4abc-45ac-98e1-761df4c13811 (Write Request/Indication).
ACK содержит только последний полученный flow_id BE16. Public DATA/INVITE API
сохраняет владение ll_entry; backend имеет отдельный внутренний wire-канал ACK.
До opened обязательны все три подписки; ранний HELLO отвергается с WARN.
Все DATA, включая INIT/keepalive/bootstrap, расходуют одно окно12500байт на направление.
Кредит возвращает только удалённый ACK; ring хранит размеры, не payload.
При полном окне TX останавливается, ACK продолжает работать; подтверждение будит writable.
Без прогресса ACK15s действует самостоятельный watchdog, не зависящий от native pending.
ACK объединяется до последнего ID, следующий разрешён через15ms после completion
предыдущего. Это также гарантирует >=15ms между отправками и даёт DATA/INVITE время
между ACK даже при медленном ATT. Первоначальный безусловный приоритет ACK давал
голодание DATA: /tmp/ble-ready-ack16-desktop-v3-* получил0/512 за233.032s,
хотя окно строго соблюдалось, HCI TX queue была <=0.753s. Raw-проба до исправления
планировщика /tmp/ble-ready-raw-ack16-v3-* дала64/96, complete=0.
Новая точная регрессия20ms ACK completion + непрерывный RX падала до исправления;
после него встречный DATA приходит примерно за21ms. Изменения ETCP для этого не нужны.
Проверки: общий transport (MTU23/247/517, дуплексное окно, ACK>=15ms,70000 IDs,
повторы/неверный ACK, ранний HELLO, настоящий15s watchdog), identity, private-bus BlueZ,
ETCP/packet-link/UI проходят. ASAN/UBSan с detect_leaks=1:3/3 PASS.
./check.sh230/230 + proxy/burst/load PASS (180s). Android APK/instrumentation
arm64-v8a/x86_64, app/router unit tasks и desktop GUI/probes собираются;
WinRT backend с ACK проходит MinGW/C++WinRT cross-build, физического Win radio нет.
Аппаратная проверка после исправления планировщика:
- /tmp/ble-ready-ack16-fair-desktop-v3-*: Linux central, Android A52 peripheral.
512/512 echo, по768000байт в каждую сторону за72.344s, суммарно21232B/s.
Максимум DATA flight:8713байт desktop,4442 Android, оба меньше12500.
Минимальный интервал отправок ACK:27.958ms desktop,31.561ms Android.
С HCI сопоставлены1220 TX/1057 RX кадров по seq/размеру/отпечатку:
local completion→HCI TX <=0.174s, HCI RX→callback0.6..2.9ms.
Это успешный полный проход, не прежняя незавершённая проба на183/512.
- /tmp/ble-ready-ack16-fair-android-central-v3-*: Android central, Linux peripheral.
512/512 echo, по768000байт в каждую сторону за115.141s, суммарно13340B/s.
Максимум DATA flight:10204байта desktop,4411 Android, оба меньше12500.
Минимальный интервал отправок ACK:59.451ms desktop,44.817ms Android.
С HCI сопоставлены1148 TX/899 RX кадров: local completion→HCI TX <=0.358s,
HCI RX→callback0.4..1.7ms. В обоих запусках подтверждены MTU517/PHY2M/15ms.
Скорость указана суммарно для echo; это не полезная скорость2Мбит/с.
После обеих проб обычный desktop GUI перезапущен новой сборкой, Android-политика
восстановлена instrumentation finally. Настройки KDE Connect не менялись.
+341
View File
@@ -0,0 +1,341 @@
BLE транспорт VibeChat (lib/ble)
Область модуля
---------------
Независимая от Qt библиотека предоставляет manufacturer-броадкасты и адресные
двусторонние GATT-сессии. Один локальный GATT-сервис обслуживает до 16 соединений.
Броадкаст — повторяемое ОС объявление, а не гарантированно доставляемая датаграмма.
Общий codec ble_identity.h: VC (2 байта), версия 3 (1), node_id BE64 (8),
имя UTF-8 (0..13). Имя сокращается по границе символа. Всего 11..24 байта.
Приглашения и автоматический выбор соседей реализованы в tools/cgathui_common (doc/ble_etcp.txt); транспорт не запускает JOIN/ETCP.
Node ID передаётся в HELLO для сопоставления сессий; подлинность он не доказывает.
Публичный API: lib/ble/ble_transport.h.
Платформенный контракт: lib/ble/ble_backend.h.
Desktop GUI запускает библиотеку по настройкам «Bluetooth» (по умолчанию выключена).
Потоки и время жизни
-------------------
Все функции и callbacks выполняются в одном переданном uasync-потоке.
Из GUI отправлять команды через существующий gui_bridge_post_bound_fn/owned_fn.
Остановить BLE до остановки/уничтожения этого uasync; из callbacks запрещены
stop/destroy/disconnect (отложить действие в uasync). send/read допустимы.
Входящие аргументы callbacks заимствованы до возврата; endpoint можно скопировать.
ID соединения монотонен в пределах handle transport. Старый ID не переиспользуется
после stop/start; после destroy ни один ID/указатель этого transport не действителен.
Порядок использования
--------------------
1. ble_create(ua, config): задать ненулевой node_id, manufacturer company_id,
callbacks и context. Linux adapter=NULL выбирает первый адаптер, иначе hciN.
Windows использует системный адаптер, adapter должен быть NULL.
2. ble_start(): асинхронная регистрация сервиса и scanning; ждать status(BLE_READY).
3. ble_advertise(data,len): 0..24 произвольных байта; NULL,0 выключает объявление.
company_id должен быть одинаковым у участников. 0xffff — только эксперимент.
advertising(READY) означает регистрацию/запуск ОС, не получение соседом.
Обновления могут объединяться до последнего значения; точный период не задаётся.
4. broadcast callback получает endpoint, bytes и RSSI (127 = отсутствует).
ble_connect(endpoint, expected_node) самостоятельно выполняет GATT discovery,
подписку и HELLO. expected_node=0 принимает любой ненулевой ID кроме своего.
5. validate вызывается для полученного HELLO/ACCEPT до допуска кадров. NULL
разрешает всех; ненулевой результат callback разрешает конкретный запрос.
connection(READY) сообщает согласованную сессию. GATT/ACL само по себе не READY.
6. Для отправки выделить ll_alloc_lldgram(length), заполнить dgram и len,
вызвать ble_tx_put. При успехе entry принадлежит transport, иначе остаётся
вызывающему. WOULD_BLOCK: сохранить entry и ждать writable, затем повторить.
7. received уведомляет о целых кадрах. ble_rx_get извлекает ll_entry;
после обработки освободить queue_dgram_free(entry), queue_entry_free(entry).
8. ble_disconnect закрывает свою сессию. ble_destroy выполняет stop и освобождение.
Память и backpressure
--------------------
Для каждого канала link: отдельные ll_queue TX/RX и RX-буфер сборки.
DATA TX ограничен config.tx_limit (0 означает 64), INVITE TX — одним пакетом,
RX — 64 пакетами. Текущий TX входит в лимит. writable выдаётся при освобождении
места после заполнения, без необходимости сначала получить WOULD_BLOCK.
ble_channel_tx_put/tx_ready/rx_get выбирают DATA/INVITE; ble_tx_put/tx_ready/rx_get
— сокращения для DATA. Для INVITE используются invite_received/invite_writable. Не обращаться к
внутренним очередям напрямую: API сохраняет правила владения при переполнении.
RX при заполнении отбрасывает целый новый кадр с WARN; старые кадры сохраняются.
Кадр означает непрозрачный буфер длиной 1..1500 байт ВКЛЮЧАЯ Ethernet-заголовок,
если он присутствует. Это не Ethernet payload MTU 1500 (которому нужно >=1514).
На link не больше одной незавершённой GATT-передачи. Она хранит свой fragment
до завершения ОС. Очередь ядра/Qt не используется как неограниченный TX-буфер.
DATA дополнительно ограничен неподтверждёнными GATT-values: фиксированные2Мбит/с
×50ms/8 =12500байт на направление, включая flow/framing headers. Локальный sent
не возвращает этот кредит: его освобождает накопительный ACK удалённого драйвера.
При заполнении ble_tx_ready=false; ACK будит writable после появления места.
Текущий кадр может остановиться посреди фрагментации, INVITE и ACK продолжают работать.
Предел12500 — расчётный размер по фиксированной скорости, не гарантия фактических50ms
при медленном радио. Сверх окна остаётся только ограниченная собственная TX-очередь.
Переподключение не переносит старые кадры и не повторяет неоднозначные отправки.
Wire protocol
-------------
UUID service: f61a9020-4abc-45ac-98e1-761df4c13811
UUID RX: f61a9021-4abc-45ac-98e1-761df4c13811 (Write Without Response)
UUID TX: f61a9022-4abc-45ac-98e1-761df4c13811 (Notify)
UUID INVITE: f61a9023-4abc-45ac-98e1-761df4c13811 (Write + Indicate)
UUID ACK: f61a9024-4abc-45ac-98e1-761df4c13811 (Write + Indicate)
RX/TX названы относительно сервера; API одинаков для обеих ролей.
Версия объявления3 означает DATA Write Without Response/Notify с16-bit flow ID
и обязательной ACK-характеристикой; версии1/2 отклоняются до GATT-подключения.
Desktop и Android должны использовать
новую версию вместе. Scan prefix Android приходит из C codec через JNI start,
номер версии в Kotlin не дублируется. Это отдельный протокол от BLE_EXAMPLE
и tools/ble-test-android; автоматическая совместимость с ними не заявляется.
Все многобайтные целые big-endian, без выравнивания C-структур.
Каждое DATA GATT-value начинается с flow_id(2), затем одно из тел ниже.
INVITE содержит только тело FRAME без flow_id; ACK содержит только last_flow_id(2).
HELLO: [type=1:1][source_node:8][target_node:8], Central -> Peripheral.
ACCEPT: [type=2:1][source_node:8][target_node:8], Peripheral -> Central.
DATA: [type=3:1][frame_id:2][total_len:2][offset:2][payload:1..].
HELLO/ACCEPT идут только по DATA, сборка DATA и INVITE независима.
frame_id увеличивается modulo 65536 отдельно для каждого направления и канала.
Планировщик чередует фрагменты DATA/INVITE; готовый ACK имеет высший приоритет,
затем HELLO/ACCEPT. До HELLO обязательны подписки DATA/INVITE/ACK с обеих сторон.
Размер значения <= min(ATT_MTU-3,512), минимальный ATT MTU=23.
Полезный DATA-фрагмент:11байт при MTU23,235 при247,503 при517; HELLO/ACCEPT занимают19.
Нулевой offset начинает новый кадр и заменяет незавершённую предыдущую сборку.
Далее обязательны тот же frame_id/total_len и offset==число принятых байтов.
Пропуск, неверная длина, новое начало или 15-секундный таймаут сбрасывают сборку;
в RX помещается только целый кадр. Повторы целых кадров выполняет потребитель.
DATA, включая HELLO/ACCEPT и начальный обмен ключами, использует Write Command
и Notification без ATT-подтверждений. Подтверждения/повторы данных выполняет ETCP;
потеря начального обмена завершает попытку по существующему дедлайну подключения.
INVITE сохраняет Write Request и Indication. Завершение локального send не означает
чтения кадра приложением и не защищает от заполнения RX.
ACK и управление окном
----------------------
flow_id начинается с1, увеличивается modulo65536 для каждого DATA GATT-value,
включая HELLO/ACCEPT, bootstrap, ETCP control и DATA. ACK — последний полученный
flow_id; при пропуске последующий ID освобождает кредит предыдущих отправок,
но НЕ утверждает их доставку приложению. Сборщик/ETCP обнаруживают потери отдельно.
Дубликат/старый DATA не выдаётся повторно, текущий ACK может быть повторён.
Кольцо хранит только размеры неподтверждённых GATT-values, без копии payload.
В окне не более1250 минимальных DATA-values по10байт; это меньше половины16-bit
пространства ID. Modulo-разность ACK однозначно проверяется по активному окну.
Старый/повторный ACK не освобождает кредит, ACK вперёд за отправленное окно закрывает
соединение с WARN. При reconnect все flow ID/кредит/таймеры создаются заново.
ACK отправляется отдельным uasync-таймером, не polling-таймером100ms. Поступившие
ID объединяются до последнего. Следующий ACK разрешён не раньше15000us после
completion предыдущего: между ACK есть время для DATA/INVITE даже при медленном ATT.
Интервал между принятыми backend ACK тем самым также не меньше15000us.
Один ACK в процессе отправки и один объединённый ожидающий; очереди ACK нет.
ACK не расходует DATA-кредит и не требует встречного DATA. Его Write Request/
Indication подтверждается ATT; ошибка/зависание завершают соединение с диагностикой.
15s без продвижения ACK закрывают link даже при успешных локальных completions.
Дубликаты ACK этот срок не продлевают. Close/stop отменяют ACK-таймер.
Linux
-----
BlueZ D-Bus, клиент sd-bus из libsystemd; зависимость от Qt отсутствует.
Собственная приватная D-Bus connection: закрытие освобождает registrations и scan.
Основной процесс ОС не обязан быть systemd, но нужны системный D-Bus и bluetoothd.
Connectable-объявление UUID и пользовательский manufacturer-броадкаст занимают
два advertising instances при одновременном включении. Оба connectable:
адрес manufacturer-маяка должен позволять GATT connect (отдельный broadcast
instance может иметь другой random address). Нехватка ресурса — ошибка.
Входящий WriteValue содержит device; серверный AcquireNotify(device,mtu) получает
отдельный SOCK_SEQPACKET socketpair для каждого клиента и канала. DATA fd принимает
notifications: успешная запись завершает локальный send без ожидания ATT ACK.
INVITE/ACK fd передают indications; байт1 в обратную сторону подтверждает ATT completion.
EAGAIN сохраняет фрагмент и ждёт writable; синхронных callbacks send нет.
Value/Confirm без адреса клиента для серверной передачи не используются.
Выходящий клиент: DATA WriteValue(type=command), INVITE/ACK WriteValue(type=request),
StartNotify для TX, INVITE и ACK; открытие после всех трёх подписок.
Публичный Device1 D-Bus API не задаёт per-link PHY/connection interval: согласованием
управляют ОС и peer; Android запрашивает 2M также в серверной роли.
Не вызывается глобальный Device1.Disconnect: ACL может обслуживать другие сервисы.
Проверено с BlueZ 5.85; требуется серверный AcquireNotify с indication completion.
Windows
-------
WinRT BluetoothLEAdvertisementWatcher/Publisher и GattServiceProvider.
GATT-клиент использует WriteValueWithResultAsync, сервер — адресный
NotifyValueAsync(value, subscribedClient): DATA — notify-only, INVITE/ACK — indicate-only.
DATA пишет с WriteWithoutResponse и подписывается на Notify; INVITE/ACK сохраняют
WriteWithResponse/Indicate. Сервер отвечает Respond только на WriteWithResponse.
После открытия исходящего GATT при наличии IBluetoothLEDevice6 (Windows 11)
запрашивается ThroughputOptimized. Request хранится до close линка и закрывается
отдельно. GetConnectionParameters/GetConnectionPhy и события изменений логируют
фактический интервал/latency/timeout и TX/RX PHY. Установка 2M этим API не доступна;
Windows выбирает PHY с peer. Отсутствие/отказ API не ломает GATT-подключение.
Результаты WinRT сериализуются в uasync через bounded mailbox (256 событий).
Опоздавшие callbacks используют weak ownership и ID, закрытые handles не трогают.
Перед остановкой uasync снять источники событий, отменить операции и posted_task.
WinRT может сохранять физический ACL для других сервисов. Гарантия close относится
к нашей сессии и ресурсам; принудительный разрыв общего ACL в API не обещается.
Нужен адаптер с central+peripheral и Windows 10 2004 / build 19041 или новее:
помимо GATT server API/BytesSent требуется BluetoothAddressType принятого объявления.
Наличие свойства проверяется при старте, интерфейса EventArgs2 — на фактическом событии;
отсутствие даёт явную причину отказа. Версия ОС сама по себе поддержку драйвером не гарантирует.
Радио Windows необходимо проверить на реальном Windows-ПК; Wine этого не заменяет.
Сборка и проверка
----------------
Linux: пакет libsystemd-dev и dbus-daemon (dbus-run-session).
MSYS2 UCRT64: mingw-w64-ucrt-x86_64-cppwinrt; C++20, windowsapp.
cmake -S lib/ble -B /tmp/utun-ble-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/utun-ble-build -j4
ctest --test-dir /tmp/utun-ble-build --output-on-failure
test_ble_transport: реальные common transports с управляемым backend; MTU
23/247/517, duplex, пропущенный фрагмент, TX/RX full, несколько пиров,
отказ validate и уничтожение во время отправки; проверяется учёт памяти.
test_ble_bluez: отдельный dbus-run-session с подставным BlueZ; настоящее
экспортирование сервиса/объявлений, два AcquireNotify fd, адресные подтверждения,
приём кадров и независимое закрытие. К системному Bluetooth тест не обращается.
test_ble_winrt (Windows): настоящий mailbox и hresult_error без радио; исключение
одного link не останавливает другого, ошибка radio прекращает доставку batch,
shutdown отменяет posted_task и поздние callbacks. Тест можно запускать в Wine;
он не проверяет драйвер, доступность Bluetooth API и физическую передачу.
ble_probe 3: ручная проверка реального радио (scan, GATT registration, публикация
и замена тестового manufacturer-объявления); не подключается к чужим устройствам.
Успех требует READY радио, первоначальной и обновлённой публикации. Одного
scan/GATT ready недостаточно. Для диагностики ожидания ресурсов: ble_probe 15.
Логи: discovery=debug. INFO — состояния и режим DATA; DEBUG — подписки и сводка трафика;
WARN/ERROR — причины отброса/отказа. Содержимое кадров/объявлений не выводится.
Отладка Bluetooth на Windows
---------------------------
В существующей секции [debug] файла vibechat.cfg установить discovery=debug;
общий [gui] debug_level может оставаться info. Журнал задаёт [gui] debug_file
(по умолчанию chatgui.log рядом с конфигом). После изменения фильтра перезапустить GUI.
Ранние ошибки подготовки INI всегда пишутся в chatgui.log до чтения настроек лога;
их точные причины также показаны в окне ошибки независимо от фильтра.
INFO: семейство/версия Windows (DeviceFamilyVersion), доступность обязательного
BluetoothAddressType, wire=3, каналы DATA/INVITE/ACK. Waiting и Stopping publisher
имеют разные причины состояния; Waiting означает ожидание ресурсов публикации.
DEBUG: адаптер (BLE, central/peripheral, extended advertising, поддержка 2M/Coded),
этап/результат каждой служебной async-операции, состояния рекламы и сканирования,
адрес и роль соединения, список UUID/свойств характеристик, CCCD, MTU/PDU,
сводка трафика, закрытие с pending-состоянием и очистка mailbox.
HRESULT записывается с именем события, конкретной отмеченной операцией, link ID
и текстом Windows даже при выключенном DEBUG/TRACE. Например:
event=Watcher.Received event_link=0: ... phase=Received.BluetoothAddressType link=0 HRESULT=0x80004002.
Регистрация обработчиков помечена auto_revoke: E_NOINTERFACE на этом этапе
нужно отличать от ошибки доступа к свойству в уже принятом событии.
Ошибка события с известным link ID закрывает этот link; ошибка общего события
переводит radio в failed. Повторный запуск после общей ошибки: выключить Bluetooth
в настройках приложения, применить, затем включить и применить ещё раз.
BluetoothError записывается
числом и именем, GATT status — числом, ATT protocol error — числом либо -1 (нет).
GATT status: 0=Success, 1=Unreachable, 2=ProtocolError, 3=AccessDenied.
PHY_2M_supported — возможность адаптера, НЕ измеренная скорость соединения;
-1 у необязательных возможностей означает отсутствие API в этой версии Windows.
Для отдельных кадров, ATT-фрагментов и переходов backpressure использовать discovery=trace.
WinRT TRACE содержит channel (0=DATA, 1=INVITE, 2=ACK), направление, длину и elapsed_us
локальной отправки до её обработки в uasync; это время включает ожидание mailbox.
DEBUG показывает число подписчиков DATA/INVITE/ACK, недостающие подписки конкретного
устройства и ATT-ошибку записи CCCD. Первое входящее значение перечитывает подписки,
если Windows ещё не доставил SubscribedClientsChanged.
На DEBUG раз в 5 секунд активного соединения и при его закрытии пишется BLE traffic:
TX/RX_Bps полезных байтов BLE-кадров (оба канала вместе, до вычета заголовков ETCP),
число кадров/фрагментов/отбросов, среднее/максимальное локальное ожидание GATT,
размеры DATA/INVITE очередей и возраст pending. Локальный TX не равен доставке.
Отдельная сводка flow показывает flight bytes/packets, peak_bytes, ID/ACK и число
ACK TX/RX. INFO печатает фиксированную скорость/окно/интервал/разрядность ID.
Категория debug (debug=debug) содержит каждый DATA TX/RX с seq, type и длиной,
ACK TX/RX с ID, освобождённым кредитом и остатком flight. Повторные ACK видны как ignored=stale.
DATA seq — 16-битный ID GATT-пакета, соответствующий ID в ACK; type: 1=HELLO, 2=ACCEPT, 3=FRAME.
BLE wire TX/RX содержит отдельный seq целого кадра, длину и отпечаток tag для сверки двух узлов.
DATA/ACK TX означает принятие backend, wire TX — завершение локальной отправки целого кадра;
удалённый ACK подтверждает приём GATT-пакетов. Содержимое пакетов в эти логи не выводится.
Повторные радиообъявления не засоряют DEBUG.
Desktop GUI
-----------
Настройки → Bluetooth: включить транспорт, рекламировать ID/имя, принимать входящие.
В [gui] bluetooth_flags: bit 0 — транспорт, bit 1 — объявление, bit 2 — входящие;
по умолчанию 0. Save сохраняет флаги и будит uasync, без перезапуска ядра.
Системный переключатель Bluetooth пользователь включает в ОС.
DesktopBle создаётся/уничтожается в runLoop UtunNode и использует общий C API.
GUI читает копию состояния; список обнаружения ограничен 64 узлами с TTL 30 секунд.
Изменение имени обновляет объявление. Входящая валидация пока разрешает всех.
UtunNode::bluetoothTransport() — заимствованный handle ТОЛЬКО для uasync-потока;
кадры остаются в RX-очередях до чтения потребителем. Автоматических JOIN/ETCP нет.
Ручная проверка с Android BlePeerProbe initiator=true: build/ble_desktop_probe.
Проба проверяет готовность радио/объявления, обнаружение и входящую GATT-сессию,
затем останавливает transport до уничтожения uasync.
Android
-------
Общие ble_transport.c/ble_identity.c компилируются в libutun.so; android_ble.c
реализует ble_backend.h через BleRadio.kt (Android BluetoothGatt/Server/LE).
Данные и callbacks core — в instance_lite uasync. Android callbacks сериализованы
HandlerThread, JNI mailbox ограничен 256 событиями, каждое несёт generation.
eventfd немедленно будит uasync при новом событии; polling-таймера между фрагментами нет.
Переполнение mailbox останавливает radio с ошибкой. На соединение не более одной
ожидающей отправки; сервер notifications/indications обслуживает адресно, по очереди.
DATA использует WRITE_TYPE_NO_RESPONSE, CCC ENABLE_NOTIFICATION_VALUE и confirm=false;
INVITE/ACK — WRITE_TYPE_DEFAULT, ENABLE_INDICATION_VALUE и confirm=true. Оба направления
ждут локальный callback готовности стека, даже когда ATT ACK отсутствует.
Политика входящих ble_set_incoming закрывает только входящие сессии.
Настройки используют DataStore; Bluetooth off останавливает только радио приложения.
Android 12+: SCAN/CONNECT/ADVERTISE runtime permissions; системное включение BT
через ACTION_REQUEST_ENABLE. На Android 8–11 сканирование требует геопозицию.
Длительная работа принадлежит существующему HeadlessService (connectedDevice).
Перед teardown worker вызывает android_ble_detach, старые callbacks отбрасываются.
android_ble_transport() возвращает общий C handle только для использования в worker.
В обычном режиме нет автоматического connect/echo или подключения пакетов к ETCP.
Физическая проверка
-------------------
./gradlew assembleDebug assembleDebugAndroidTest (в tools/chatgui-android)
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
Выдать приложению разрешения Bluetooth, включить системный Bluetooth.
Запустить одновременно:
adb shell am instrument -w -e seconds 90 -e initiator false \
com.utun.chat.test/com.utun.chat.BlePeerProbe
/tmp/utun-ble-build/ble_peer_probe connect 75
Обратные роли: initiator=true и ble_peer_probe listen 75.
Instrumentation временно включает echo, возвращает сохранённую политику после
теста. Успех определяет desktop RESULT ok=1: 96 пакетов размеров
1/13/14/20/244/512/1499/1500, побайтовая сверка, TX WOULD_BLOCK и writable.
Не запускать этот тест одновременно с обычным использованием приложения.
Профиль скорости Android
------------------------
После подключения client просит CONNECTION_PRIORITY_HIGH и ATT MTU 517.
Обе роли запрашивают PHY_LE_2M для TX/RX при поддержке локального контроллера.
Запрос не гарантирует смену PHY: onPhyUpdate/onPhyRead логируют фактические TX/RX.
При отсутствии поддержки остаётся согласованный PHY. S2/S8 — параметры только
LE Coded PHY (дальность/скорость), с выбранным 2M применяется NO_PREFERRED.
У Android BluetoothGattServer нет public requestConnectionPriority; входящий link
использует дополнительный BluetoothGatt handle того же ACL только для HIGH.
Запрос/результат логируются, handle закрывается при teardown; нового ACL для настройки нет.
Сам UUID/advertising остаётся legacy
1M для обнаружения; PHY 2M относится к установленному соединению.
Проверка 2026-10-29
------------------
Физический Q8 DA6B8P01, Android 14 (API 34), Linux BlueZ на этом ПК.
Обе роли подключения: RESULT ok=1, sent=96, verified=96, blocked=1, discovered=1.
Каждое направление: 45636 байт с побайтовой сверкой; пакеты до 1500 включительно.
После профиля скорости: desktop central 19.801 с, Android central HIGH 9.977 с.
Это elapsed теста с подтверждениями/echo, не измерение предельной канальной скорости.
Q8 сообщает isLe2MPhySupported=false; readPhy возвращает status=6. PHY 2M
реализован как запрос, его фактическое согласование на этом телефоне не проверено.
Общий C codec/transport и BlueZ mock: ASAN+UBSAN, leak detection — 3/3 PASS.
Сборки Android arm64-v8a/x86_64, desktop GUI Linux и WinRT cross-build проходят.
Физический Windows radio и несколько одновременных Android-клиентов не проверены.
Проверки ACK version3
---------------------
test_ble_transport: MTU23/247/517, duplex12500-byte bound при задержанных удалённых ACK,
completion сам не освобождает окно, дубликаты/неверный ACK, переход65535→0
на70000 GATT-values, интервал ACK>=15ms и настоящий15s ACK watchdog без native pending.
Отдельная регрессия имитирует20ms completion ACK при непрерывном входящем DATA:
до исправления планировщика встречный DATA не приходил за250ms; после — примерно21ms.
Таймер отсчитывается после completion ACK, чтобы новый ACK не вытеснял DATA/INVITE.
Private-bus BlueZ проверяет обязательность третьей подписки, адресные ACK indications
двух клиентов и ACK Write Request в исходящей роли. test_ble_etcp проверяет настоящий
ETCP/crypto, потери DATA и INVITE, NCD/BGP и lifecycle через новый flow prefix.
File diff suppressed because it is too large. Load diff
+83 -11
View File
@@ -9,6 +9,9 @@ HEVC Main с Main/High tier (High начиная с level 4), 8-bit 4:2:0, progr
HRD при наличии, геометрию и независимый IDR. Фактический level проверяется после
открытия encoder. CAPS — 64 байта, header — 56 байт; старый формат не поддерживается.
VBV является локальной настройкой software encoder, в сетевом контракте его нет.
Проверка KEY в call_video.c ограничена envelope; разбор нужных полей VPS/SPS/PPS/IDR
выполняет call_video_bitstream. Он не декодирует entropy payload и не заменяет decoder.
У HEVC High tier также действуют консервативные Main-tier бюджеты HRD/CPB.
Результат запуска decoder (generation + accepted) передаётся отдельно от router ACK:
неподдержанный формат вызывает fallback в Auto, потеря references — запрос KEY.
Камера, разрешение, частота до 60 fps, кодек и диапазон битрейта меняются во время звонка.
@@ -22,7 +25,8 @@ SoundTouch и очередь устройства. Алгоритм регули
Показ имеет собственное абсолютное расписание с плавным темпом по суммарному запасу до 1.6x;
audio clock ограничивает опережение videoPTS-audioPTS до +40мс. Устойчивый долг при ограничении
частоты экрана снимается пропуском только показа уже декодированных кадров.
Подробности и границы измерения — в разделе «Независимое расписание показа» ниже.
Подробности — в разделах «Часы показа и сквозная очередь», «Независимый desktop-планировщик»
и «До 60 fps и восстановление presentation»; раннее «Независимое расписание показа» историческое.
Искусственная тишина/тоны не продвигают исходную позицию. Формат видео меняется по PTS;
новое поколение не уничтожает старые кадры, соответствующие ещё не прозвучавшей речи.
@@ -43,8 +47,69 @@ audio clock ограничивает опережение videoPTS-audioPTS до
Состояние звонка/регулятора/роутера — uasync. Камера и encoder/decoder — платформенные
workers. Ограниченный mailbox, одно ожидающее уведомление. Команды bitrate имеют
call_id, video_generation и request_id; одно исполнение и последнее желаемое значение.
Принятая настройка не заменяет измерение фактического выхода. Shutdown отменяет
уведомления/таймеры, останавливает producers, затем освобождает очереди и codecs.
Принятая настройка не заменяет измерение фактического выхода. Остановка освобождает
уведомления/таймеры, очереди и codecs в порядке владельцев: desktop присоединяет workers
перед close port; Android ставит stopped, присоединяет RX, закрывает port, затем TX handler
закрывает камеру/encoder и завершает свой looper. Это разные контракты завершения.
Границы модулей и wire (сверено с кодом 26.10.2026)
call_video.h: чистые проверки/сериализация доступны workers, receiver имеет одного владельца.
Limits — независимые границы, полностью нулевой limit означает отсутствие
проверенного профиля. Format: ненулевой generation, чётные размеры 16..4096, 1..60 fps,
rotation=0/90/180/270, target ceiling 300000..4000000 бит/с, один profile=1/2.
Поля формата, включая maximum_bps, неизменны внутри generation; текущий bitrate — отдельно.
frame_id и capture PTS строго возрастают сквозь поколения; пропуск frame_id требует KEY.
CAPS: четыре limits по 16 байт: encode AVC, encode HEVC, decode AVC, decode HEVC.
Внутри limit: width:2, height:2, fps:2, pixels_per_second:4, maximum_bps:4, level:1, profiles:1.
Header фрагмента (смещения в байтах):
0 call_id:8 8 generation:8 16 frame_id:8 24 capture_pts_us:8
32 width:2 34 height:2 36 fps:2 38 rotation:2
40 codec:1 41 flags:1 42 level:1 43 profile:1
44 frame_size:4 48 offset:4 52 maximum_bps:4
Все многобайтовые поля CAPS/header — BE. Длина payload 1..1200, полный кадр <=2 МиБ.
Parse возвращает view внутри входного пакета; fragment.data уже указывает на payload
данного offset. Локальный received_us в wire не входит. Control имеет отдельный формат:
type:1 + call_id:8 + revision/generation:8, для CAPS ещё 64 байта; оба uint64 — native endian,
как в текущей сигнализации call. Это описание действующего кода, не обещание cross-endian совместимости.
call_video_receiver хранит одну частичную сборку. Offset идёт непрерывно; повтор/перестановка,
разрыв frame_id, регресс PTS/поколения и смена формата без нового generation нарушают цепочку.
Возраст сборки >2с проверяется на следующем push; отдельного таймера сборки нет.
Готовый буфер заимствован до следующего push/reset/destroy; очередь готовых кадров — у владельца.
call_video_transport принадлежит uasync: control/audio/video используют отдельные router services,
control и video требуют подпись и шифрование. CAPS повторяются каждые 500мс до прикладного ACK.
Configure может применить локальное состояние и затем вернуть -1 при отказе отправки CAPS.
TX FIFO: 32 кадра, размах PTS <=500мс, суммарно <=2 МиБ; общий admission учитывает router debt.
Голова ждёт audio_end_pts и CAPS_ACK. Flush выдаёт до 16 фрагментов, продолжает через call_soon;
send_q/full debt ждут send_ready/audio/tick. Возраст головы >2с сбрасывает зависимую цепочку.
Tick 50мс обновляет регулятор; timeout encoder-команды 12с выключает admission до configure_rate.
Callback заимствует payload до возврата и не вызывает изменяющий call API реентерабельно.
call_video_port — единственный процессный mutex/mailbox. Attach/detach — uasync, workers используют
потокобезопасные команды. Open того же id сохраняет RX/TX/peer/wake/received counter;
другой id очищает их. Open/format/applied/range/decoded подтверждают приём команды,
проверка активной сессии и исполнение происходят позже в uasync.
Send=1 означает копирование пачки в mailbox, transport может позже отвергнуть её с key_serial.
Send=0 не принимает ничего; transport busy сохраняет непринятый хвост без нового KEY.
TX mailbox: до 8 кадров по 2 МиБ (до 16 МиБ); RX: до 8 кадров/4 МиБ.
Pull передаёт владение data, освобождение u_free; RX wake вызывается под port mutex, без входа в port API.
До detach останавливают пользователей и снимают transport callbacks через close с обработкой команды
либо завершение call session. Сам detach не заменяет остановку транспорта.
call_video_rate — чистая модель 500мс окон с 50мс шагом; единицы — уникальные encoded bytes,
без fragment headers. Invalid sample не меняет состояние/результат. Чистый step допускает до
100 Мбит/с; диапазон звонка 300..4000 кбит/с ограничивают parser и transport.
call_video_playout не владеет изображениями: WAIT сохраняет голову, SHOW резервирует до presented,
DROP_ORDER/DROP_LATE освобождают только decoded presentation силами владельца.
Desktop считает общий запас как received-completed-discarded без mutex порта;
Android складывает снимок port.rx_frames с локальными pending/inputs/ready/awaiting.
Desktop смена пользовательского режима отменяет ещё не переданную worker-пачку и заменяет encoder;
это отличается от смены bitrate с полным drain старого encoder. Android при смене геометрии
сначала drain, отправляет pendingFrames и ждёт tx_idle перед format. Готовые RX-изображения
старого поколения сохраняются до своего показа на обеих платформах.
Применение bitrate на ключевом кадре (уточнение пользователя)
Worker хранит pending-команду и применяет её перед кодированием ближайшего
@@ -99,8 +164,11 @@ GOP продолжается. Android сначала меняет bitrate чер
Видео следует аудио при догоне, underrun, смене устройства и формате видеотрека.
Завершение звонка безопасно во время переключения камеры, bitrate и decoder.
Далее журнал этапов разработки. Старые описания wire/profile/VBV ниже исторические;
актуальный контракт приведён в начале, последние проверки — в конце документа.
Далее журнал этапов разработки. Формулировки «текущий», «действующий», «пока» и результаты
проверок относятся к дате соответствующего этапа; старые wire/profile/VBV/буферы/алгоритмы
не задают нынешний контракт. Актуальная сводка приведена выше, подробности поздних этапов
уточняют расписание, High tier, 60 fps и прогрев. Исторические аппаратные результаты и логи
при сверке документации 26.10.2026 заново не воспроизводились.
Проверка SoundTouch, 14.10.2026
test_soundtouch_clock сравнивает положение реальных четырёхсэмпловых фрагментов
@@ -922,7 +990,7 @@ Windows-диагностика: AudioDiag теперь работает чере
video late drop содержит точные PTS/AV/возраст первого позднего кадра за окно, playout —
диапазоны всех late кадров. Измерения не корректируют Windows callback-estimate аудиочасы.
Восстановление без пропусков, 19.10.2026 — действующее поведение
Восстановление без пропусков, 19.10.2026 — базовый этап; уточнения 19–20.10 ниже
Encoded-кадр после принятия сохраняет ID/PTS и владельца до передачи дальше. TX port
удерживает непринятый хвост пачки и повторяет его при уведомлении writable. Новая пачка
не принимается во время обработки старой. Busy=0 не меняет references и не запрашивает KEY;
@@ -949,7 +1017,8 @@ Decoder получает кадры строго последовательно,
входы decoder ограничены отдельно восемью, чтобы output lookahead не блокировал запуск GPU.
Основной backlog хранится encoded, а не большим RGB FIFO или удержанием всех аппаратных outputs.
Playout больше не содержит DROP_LATE/DROP_SCHEDULE. Invalid/order остаётся диагностируемой ошибкой.
На этом этапе playout не содержал DROP_LATE/DROP_SCHEDULE; 20.10 возвращён только DROP_LATE
для устойчивого долга при ограничении частоты экрана. Invalid/order остаётся диагностируемой ошибкой.
CallTimeline публикует rate_ppm из фактического SourceSpan SoundTouch -> PCM на текущей позиции устройства.
Первоначальный вариант управлял темпом по audioPTS-framePTS. Его заменила описанная ниже схема
с подтверждённым временем показа, общей глубиной конвейера и небольшим рабочим запасом.
@@ -1004,8 +1073,9 @@ N = полные encoded в port + pending input worker + принятые decod
+ decoded FIFO + один выбранный, но ещё не подтверждённый GUI/Surface кадр.
Перемещение между стадиями не меняет N; исключение кадра происходит после подтверждения показа
или решения DROP_LATE для уже декодированного кадра (дополнение 20.10.2026 ниже).
Снимок port.rx_frames берётся под mutex после локальных перемещений RX worker. Сетевая частичная
сборка не считается полным кадром. Очереди отправителя не видны этому счётчику.
На Android снимок port.rx_frames берётся под mutex после локальных перемещений RX worker.
Desktop использует атомарный cumulative received-completed-discarded (см. независимый планировщик ниже).
Сетевая частичная сборка не считается полным кадром. Очереди отправителя не видны этому счётчику.
RX admission возобновляется при появлении места, без искусственного ожидания полного drain FIFO.
Уведомление port будит desktop RX немедленно; callback снимается до освобождения владельца.
@@ -1018,7 +1088,8 @@ submit/receive независимы: EAGAIN сохраняет непринят
SHOW резервирует кадр, presented(PTS, MONOTONIC) подтверждает его потребление и продвигает часы.
Desktop ACK приходит после paintEvent; Android — после SurfaceTexture/RenderThread commit, не по OnFrameRendered.
Это подтверждения платформенного потребления, а не измерение физического scanout экрана.
Срок следующей головы фиксируется один раз: последнее ACK + период format / speed - оценка dispatch-to-ACK.
Срок следующей головы фиксируется один раз: фаза последнего показа + период format / speed
минус оценка dispatch-to-ACK. Фаза плавно корректируется по ACK; большая пауза привязывает её к ACK.
Оценка lead сглаживается и принимает обычный ACK за целый цикл экрана; выбросы больше 1.25 периода +1мс игнорируются.
Пустая очередь сохраняет привязку: готовый через 12мс следующий кадр не показывается раньше своего обычного срока.
Поздно пришедшая голова выходит сразу после готовности; разрыв capture PTS не повторяется ожиданием.
@@ -1031,7 +1102,8 @@ Desktop ACK приходит после paintEvent; Android — после Surfa
Опережение аудио по-прежнему ограничено 40мс; невалидные аудиочасы запрещают новый показ без очистки FIFO.
Windows ожидает абсолютный срок через high-resolution waitable timer + event, без spin/timeBeginPeriod.
Отказ high-resolution API явно логируется, остаётся regular timer; полный отказ handles использует condition_variable.
Отказ high-resolution API явно логируется, остаётся regular timer; при отсутствии необходимых handles
desktop start отклоняет запуск видео. Runtime ошибка ожидания передаётся RX для лога и остановки workers.
Stop и данные прерывают ожидание. Фактический overshoot остаётся в video RX worker wake_late_max_us.
CPU encoder: libx264 threads=4/sliced-threads=1; libx265 pools=4/wpp=1/frame-threads=1.
File diff suppressed because it is too large. Load diff
+182 -50
View File
@@ -2,6 +2,76 @@
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
Статус аудита 26.10.2026: проверка начата с голосовой сигнализации, call.c прочитан целиком.
Выявленный дефект исходящего допуска исправлен: call_start отклоняет новую попытку при любой
незавершённой обычной сессии или приглашении рации до создания session/CM/INVITE.
Новый ID получает ENDED/BUSY; уже зарегистрированный ID, включая ENDED до очистки,
отклоняется без событий, чтобы отказ не завершил прежний звонок. Новый уникальный ID после ENDED разрешён.
Штатный test_call_admission на двух настоящих ядрах проверяет состояния обеих сторон,
повторы ID, CHAT/DM, сохранение первого звонка/аудиоприёма, ENDED и взаимодействие с рацией.
Полная сверка аудиостека, видео и платформенных разделов не завершена.
Доказательства и вариант исправления: [call_audit_findings.txt](call_audit_findings.txt).
Продолжение аудита: полностью прочитаны call_audio/jitter/delay/timeline/tones.
Дефект TX-счётчика при повторной привязке call_audio исправлен отдельной эпохой instance:
задачи прежней привязки не меняют новый pending, не отправляют медиа и не записывают позднюю статистику.
Смена звонка того же owner сохраняет учёт занятых слотов до callbacks; повтор init идемпотентен.
Предел8 относится к действующей привязке; старые tasks освобождаются самим uasync при callback/destroy.
Постоянные регрессии с настоящими Opus/uasync:13/13 PASS, голосовой C++ профиль4/4 PASS;
со старым voice-архивом шесть новых сценариев выявляют прежний дефект.
Это проверка внутренних posted tasks, аппаратный/сетевой звонок не воспроизводился.
Реализация, проверенный план и границы — разделы6–8 отчёта; полный аудит call/ ещё не завершён.
Проверка headless: TCP-сервис call_headless имеет собственные кодеки instance и выполняет callbacks в uasync;
он не использует DSP/jitter движка call_audio. Исправлен разрыв фрейма при переполнении исходящей очереди:
header/PCM собираются до отправки, принимается или отбрасывается целый кадр, partial send сохраняет его хвост.
PCM-потери учитываются отдельно для клиента/сервиса с WARN и итогом при закрытии/destroy.
Постоянная регрессия с TCP/Opus/uasync:11/11 PASS, весь поток сравнивается с независимым decoder;
старый код выявляется на границе очереди. Штатный e2e1/1 и check203/203 PASS,proxy9/9,burst1/1,load8/8.
Реализация и границы проверки — раздел12 отчёта. Полная проверка платформенных I/O остаётся впереди.
Продолжение платформенной проверки: Android mute исправлен передачей исходного PCM и snapshot muted
через JNI в call_audio_feed_pcm_muted. AEC/AGC продолжают обрабатывать микрофон, mute применяется перед Opus.
Постоянная регрессия настоящего bridge/AEC/AGC/Opus:5/5 PASS, включая следующий unmute и старый ранний mute как контроль.
Voice/desktop-профиль30/30, Android timing/trace unit14/14, assembleDebug arm64-v8a+x86_64 и check203/203 PASS.
Это компонентная проверка и сборка; аппаратный Android-звонок не запускался. Диагностика — раздел13,
внедрение и границы — раздел14 отчёта; полный аудит голосового/платформенного кода ещё не завершён.
Продолжение Android I/O: исправлено владение BluetoothHeadset proxy при позднем callback,
disconnect сервиса и stop/start. Поколение запроса и общий монитор router защищают принятие/закрытие;
disconnect сохраняет владение до closeProfileProxy. Постоянные JVM-регрессии15/15 PASS,
включая64 конкурентных callback/stop; прежний исходник выявляется в трёх отказных сценариях.
Android timing/trace/buffer/recovery26/26, voice/desktop30/30 и assembleDebug для двух ABI PASS.
Это управляемая модель lifecycle API, без Bluetooth/Binder и акустической проверки телефона.
Доказательства, полный профиль и ограничения — раздел15 отчёта; общий аудит остаётся частичным.
Продолжение сверки контрактов: call_clock/delay/jitter/jitter_window/timeline/tones.h проверены целиком,
уточнены правила потоков, единицы/сброс счётчиков, поколения и ошибки часов. Исправлены устаревшие
описания минимального запаса (20мс), Linux capture (базовый запрос 10мс) и существующей desktop-адаптации.
Повторный voice/desktop-профиль30/30 PASS; реализации не менялись. Подробности и оставшаяся область — раздел16 отчёта.
При продолжении обнаружен и после согласования удалён неиспользуемый Android online-API,
напрямую читавший uasync-очереди из вызывающего потока. Проверка доступности осталась внутри `call_start`;
публичного межпоточного запроса больше нет. Android/JVM170/170, native/две ABI APK и check204/204 PASS.
Диагностика, изменение и границы проверки — раздел17 отчёта; остальные частичные отметки сохранены.
Дефект регистрации call_audio после stop/start чата исправлен: заимствованный audio_ops хранится
у UTUN_INSTANCE и переживает пересоздание call_ctx. Повторный init не требуется; идемпотентный init
того же владельца сохраняет эпоху и занятые TX-слоты. Android worker привязывает voice до SERVICE_STARTED
и снимает привязку перед уничтожением ядра, включая частичный запуск; GUI запускает только DSP.
Новые восемь lifecycle-регрессий и общий voice/desktop-профиль38/38 PASS.
Диагностика до исправления — раздел18, реализация и границы — раздел19 отчёта.
Следующая сверка закрыла публичные аудиоконтракты call.h/call_audio.h/call_headless.h/call_proto.h
и desktop call_audio_engine.h. Уточнены сроки жизни callback/аргументов, фиксированный wire-формат,
завершение и восстановление устройств. Изменения только в комментариях/doc; voice38/38 и четыре
набора сигналинга/headless PASS. Подробности — раздел20 отчёта.
Сверка видеоконтрактов и внутреннего `call_priv.h` завершена: все21 заголовок `src/call/` закрыты,
также проверены17 C/C++ видеозаголовков и `doc/call_video.txt` (разделы22–23 отчёта).
Уточнены владение сессией/видеотреком, асинхронное завершение и смысл счётчиков; реализации не менялись.
Общий аудит архитектуры и платформенных разделов остаётся частичным, новая аппаратная приёмка не выполнялась.
## 1. Область
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
@@ -45,13 +115,16 @@ Android обычного звонка показывает своё уведом
|---|---|---|---|
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
| `src/call/call_audio.h/c` | C | frontend с I/O и CMake-тесты | Opus, AGC, raw/prepared capture, pull с тонами; регистрация через `call_audio_ops` |
| `src/call/call_tones.h/c` | C | общий voice-контур | генератор тонов (glitch/ended), синус |
| `src/call/call_jitter.h/cpp` | C++ | общий voice-контур | FIFO Opus, сетевой запас, decode-on-pull; C API |
| `src/call/call_delay.h/cpp`, `call_timeline.h/cpp` | C++ | общий voice-контур | обработка задержки PCM и карта исходного PTS до устройства |
| `src/call/call_headless.h/c` | C | autotools и CMake core | отдельный TCP PCM-сервис с собственными кодеками, без call_audio DSP |
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется): сигналинг `call.c`
и headless TCP PCM присутствуют в ядре. Общий call_audio DSP собирается отдельно
через `voice_sources.cmake` для desktop/Android и компонентных CMake-тестов.
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
@@ -75,6 +148,10 @@ Desktop направляет сообщения FFmpeg и Qt в общий `debu
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
Структуры `call_proto.h` упакованы без padding; целые поля передаются в порядке байтов хоста.
Пакет INVITE имеет фиксированные38 байт: ровно один descriptor AUDIO/OPUS, 48кГц mono, 32кбит/с.
Согласования аудиоформата нет: поля descriptor не настраивают decoder, frontend создаёт его с фиксированными параметрами.
`expires_us` отправляется как NTP-время now+2с; приём требует `now < expires_us <= now+4с`.
Сигналинг (`0x36`):
@@ -109,7 +186,7 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capt
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), целевой bitrate32кбит/с, ~50 кадров/с.
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
@@ -118,16 +195,16 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capt
## 4. Машина состояний
```
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
Caller: IDLE → OUTGOING(INVITE) → [WAIT_DECISION(PHONE_BUSY) → OUTGOING(force)] → RINGING → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → [WAIT_DECISION, без INCOMING] → RINGING(INCOMING + sent RINGING) → ACTIVE(accept) → ENDED
```
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL / PHONE_BUSY`.
Таймауты:
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
(`OUTGOING` + `RINGING` + `WAIT_DECISION`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
держит маршрут живым;
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
@@ -144,8 +221,10 @@ NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии.
ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого.
При одновременном завершении стороны независимо подтверждают команды друг друга.
После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается
ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок.
После ACK сессия удаляется на ближайшем tick завершения (до300мс); без собственной завершающей команды
при наличии CM handle либо роли callee запись удерживается2.1с (`CALL_RING_DEAD_TB+1000`).
Это также сохраняет tombstone от позднего INVITE; константа `CALL_TEARDOWN_DELAY_TB=5000` не используется.
Остановка ядра отменяет таймеры без повторных отправок и событий ENDED.
Запоздалые кодограммы вызова при идущем разговоре (`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
@@ -181,6 +260,9 @@ CM handle инициирует оптимальное подключение и
## 6. Потоки и жизненный цикл
`call_session`, CM handle, очередь TX и таймеры принадлежат uasync-потоку.
`call_init/destroy` относятся к сервису чата; заимствованный audio provider хранится у ядра и переживает stop/start чата.
Отдельный `call_set_media_recv_cb` живёт в call_ctx, требует запущенный модуль и сбрасывается при его уничтожении.
Trampoline освобождают переданный heap-аргумент, но не удерживают instance: владелец обеспечивает его срок жизни до callback.
Аудиопоток кодирует PCM и постит `call_send_media`; приём в uasync кладёт Opus в `call_jitter`.
Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку.
Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока.
@@ -207,15 +289,30 @@ ACTIVE запрашивается до создания аудиоустройс
Реальное время запуска/переключения зависит от Android и требует проверки на устройстве.
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
CallAudioRouter используется также MediaAudioController: start может выполняться из I/O, Bluetooth callbacks — на main.
start/stop и принятие/отключение BT-профиля сериализуются существующим монитором router.
Каждый запрос getProfileProxy получает поколение; stop инвалидирует его до освобождения ресурсов.
Поздний connected закрывает полученный proxy через создавший его adapter, не меняя новый lifecycle.
Текущий proxy и adapter сохраняются до stop, в том числе после onServiceDisconnected:
отключение сервиса не отменяет владение, а getConnectedDevices при недоступном сервисе возвращает пустой список.
При stop ссылки отсоединяются до closeProfileProxy; старый disconnected после restart игнорируется.
Пока принятый запрос не доставил proxy, закрыть его через публичный API нельзя: очистка выполняется при позднем connected.
Логи CallAudioRouter содержат поколение, proxy, результат запроса, retained/closed/stale и ошибки API.
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером
два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру.
Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической
смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop.
Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс.
Выход принадлежит playback worker. Для подтверждённого speaker на API31+ сначала пробуется AAudio:
принимаются48кГц mono PCM16, LOW_LATENCY, burst1–480 и запрошенное устройство.
Иначе используется AudioTrack; гарнитуры сразу идут через AudioTrack. Начальная заявка рабочего буфера —
один блок PCM (20мс) для AAudio, два (40мс) для AudioTrack; стартовый порог AudioTrack на API31+ следует принятому размеру.
Ёмкость выделенной памяти может быть больше. Callback на main фиксирует подтверждение выхода;
повторное открытие и настройку выполняет playback worker, callback прежнего AudioTrack игнорируется и снимается при закрытии.
Подачу PCM задаёт свободное место в устройстве, без дополнительного таймера 20мс.
Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт.
При смене устройства или восстанавливаемом отказе сохраняется ещё не принятый устройством хвост блока.
Новый локальный playback head получает базу общего счётчика принятого PCM; прежняя очередь устройства
не переигрывается и не считается доказанно прозвучавшей. Core clock приостанавливается в той же эпохе;
новое подтверждение позиции возобновляет его, JB сохраняется.
Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены.
Лог `playback buffering` показывает причину настройки, запрошенный/принятый размер, порог и ёмкость;
ежесекундный `audio summary` — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN.
@@ -243,8 +340,10 @@ Backend публикует только числовой снимок в огр
Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе
не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.
Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс;
фактический период сообщает `audio: call-capture initialized`. Сборщик по-прежнему принимает произвольные чанки.
Для захвата звонка desktop сохраняется общий запрос 20мс; `AudioDevice::requestLowLatency`
для Linux/PulseAudio заменяет его запросом 10мс при каждом открытии, включая recovery/fallback.
Фактический период сообщает `audio: call-capture initialized`: backend может округлить заявку.
Сборщик принимает произвольные чанки и по-прежнему формирует кадры Opus/AEC по 960 сэмплов (20мс).
Секундные замеры задержки при `call=debug`:
- `voice-io: capture latency`: возраст первого сэмпла перед native feed (включает сам блок 20мс),
подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
@@ -281,8 +380,8 @@ Backend публикует только числовой снимок в огр
Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать.
PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb;
эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback.
Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь,
жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.
Сравнительные Linux-замеры запроса 10мс и границы аппаратной проверки описаны в [linux_audio_recovery.md](linux_audio_recovery.md).
Запрос периода устройства не меняет размер кадра Opus/AEC, жизненный цикл подготовки Android и таймер показа видео.
На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch:
после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один
@@ -292,16 +391,18 @@ PTS/возраст используют локальный MONOTONIC, интер
После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию;
после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или
пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно.
Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует
Открытие нового выхода начинает подбор с начальной заявки backend (AAudio20мс, AudioTrack40мс). Непринятая меньшая заявка фиксирует
нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение.
Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock.
Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду
Сам подбор размера не делает flush/recreate; смена выхода переоткрывает backend в playback worker.
Частичная запись PCM сохраняется. После смены размера/выхода через секунду
повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер.
Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns;
достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют
проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку.
На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager,
изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.
На desktop voice-выход принадлежит SoundManager; `AudioDevice` при backend underrun/overflow
может увеличить запрошенный период capture/voice-output и переоткрыть устройство через recovery.
Это отдельная политика backend, без Android-проб уменьшения AudioTrack на один PCM-блок.
Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс)
в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи;
@@ -339,22 +440,38 @@ Stop дожидается reporter, включая непрерываемый An
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
Целевой запас — `clamp(max-min, 20, max_reserve_ms)` мс по изменению задержки доставки:
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
Начальное и повторное накопление ждёт этот запас плюс размер запрошенного PCM-блока;
после первой полной выдачи остаётся целевой запас. Увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
`CallDelay` имеет режимы Direct/Stretch/Drain. Direct выдаёт PCM напрямую и при необходимости
удаляет отдельные сэмплы с ускорением до 2%, доводя разность входной/выходной позиций до границы 960 сэмплов.
Это не синхронизация частот физических устройств. Нижняя огибающая за 150–200мс подавляет пилу пакетизации.
При избытке не менее 120мс включается SoundTouch, при избытке не более 40мс либо низком запасе
заканчивается выдача готового DSP-выхода, затем возвращается Direct с сохранением исходного PCM-хвоста.
В SoundTouch темп плавно растёт до 1.6x без изменения высоты голоса:
линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
При временном отсутствии pull сетевой FIFO продолжает накапливаться. Явный reset playback I/O
сбрасывает PCM/DSP/локальную эпоху часов и сохраняет только свежий сетевой запас Opus.
Переполнение FIFO удаляет старейший кадр и увеличивает счётчик dropped.
Локальная epoch относится к playback-карте. Переоткрытие выхода с сохранением PCM-позиций
использует suspend/progress в прежней epoch; полный playback reset создаёт новую epoch.
`CallTimeline` принимает абсолютную позицию от reset и локальный срок её вывода.
Якорь старше 250мс или suspend даёт `valid=0` до нового progress; регрессия позиции/времени наблюдения,
невозможная позиция либо ошибка/переполнение карты запрещают часы до полного reset.
PTS приходит со шкалы удалённого захвата, из него вычитается задержка Opus encoder.
Локальные тишина/тоны удерживают PTS, удалённый mute продолжает шкалу источника.
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
@@ -384,16 +501,22 @@ SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
## 9. GUI-события (extend `chat_event.h` + `gui_bridge.h` + android)
```
CHAT_EVT_CALL_INCOMING 32 [ch_id_len][ch_id][caller_node_id:8][call_id:8]
CHAT_EVT_CALL_RINGING 33 [call_id:8]
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
CHAT_EVT_CALL_INCOMING 34 [ch_id_len:1][ch_id][caller_node_id:8][call_id:8]
CHAT_EVT_CALL_RINGING 35 [call_id:8]
CHAT_EVT_CALL_ACCEPTED 36 [call_id:8]
CHAT_EVT_CALL_ENDED 37 [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED 38 [call_id:8][reason:1]
CHAT_EVT_CALL_STATS 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
CHAT_EVT_CALL_ERROR 40 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_PATH 42 [call_id:8][text:var]
CHAT_EVT_CALL_CONNECTION 44 [call_id:8][CONN_TYPE:1][next_hop_node_id:8]
CHAT_EVT_CALL_PHONE_BUSY 66 [call_id:8][reserved:1]
```
Это идентификаторы `chat_event.h`; desktop использует соответствующие `GUI_EVT_CALL_*`
из `gui_bridge.h` либо общую константу CHAT. Android-маршрут устройства приходит через
платформенные callbacks, отдельного общего события `CHAT_EVT_CALL_AUDIO_ROUTE` нет.
## 10. Основные функции публичного API
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
@@ -405,11 +528,15 @@ void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_nod
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
const struct call_capture_time* timing);
```
Онлайн-проверка перед созданием сессии — внутренняя функция `call.c`, выполняется в uasync.
Группы и их очереди принадлежат этому потоку; чтение из GUI/JNI напрямую недопустимо.
Неиспользуемый синхронный Android online-API удалён. `call_start` проверяет доступность
при обработке команды и сообщает отказ через `CHAT_EVT_CALL_ERROR`.
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
Захват и воспроизведение используют по одному аудиопотоку на направление.
@@ -421,7 +548,9 @@ void call_audio_release(uint64_t call_id);
```
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
и маску mute. Для raw capture `feed_pcm_muted` принимает настоящий микрофон и флаг mute;
AEC/AGC обрабатывают микрофон, перед Opus применяется mute. Android передаёт этот флаг вместе с исходным
кадром через JNI; раннего зануления нет. `call_audio_release` вызывается после остановки обоих направлений I/O.
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
в uasync-потоке; полный контракт — в `call_video_transport.h`.
@@ -431,9 +560,13 @@ void call_audio_release(uint64_t call_id);
## 11. Платформенный I/O
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
+ события в `gui_bridge_impl.cpp`.
- **Desktop:** `CallAudioEngine` управляет отдельным miniaudio capture и двумя workers `VoiceAudioIo`.
Capture callback только копирует PCM; capture worker выполняет stereo-reference AEC и передаёт
prepared PCM с маской mute в core AGC/Opus. RX worker независимо вызывает `call_audio_pull_pcm`.
`SoundManager` смешивает голос с остальными источниками на общем выходе и возвращает тот же итоговый
stereo PCM как AEC-reference. Stop закрывает capture, ждёт capture worker, отсоединяет выход,
ждёт RX worker и освобождает core.
При неисправном capture async close сохраняет объекты и lease до завершения uninit; RX остаётся активным.
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
`availableCommunicationDevices`; fallback `setSpeakerphoneOn` + `startBluetoothSco`),
@@ -442,16 +575,15 @@ void call_audio_release(uint64_t call_id);
## 12. Сборка
- `src/Makefile.am` + `tests/Makefile.am`: `call.c` (C, без `.cpp`). `src/call/` в include path.
- `tools/chatgui/libutun/CMakeLists.txt`: +`call_audio.c`, `call_tones.c`,
`audio_jitter.cpp`, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter,
InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86,
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
`LANGUAGES C CXX`, те же файлы.
- `src/Makefile.am` + `tests/Makefile.am`: сигналинг, headless PCM и C-модули видео; общий C++ voice DSP сюда не входит.
- `tools/chatgui/libutun/CMakeLists.txt`: рекурсивный GLOB C-ядра, включая `call.c`/`call_headless.c`;
`call_audio.c`/`call_tones.c` исключены из core. `tools/chatgui/CMakeLists.txt` создаёт `utun_voice`
по общему `src/call/voice_sources.cmake`: call_audio/tones, call_jitter/delay/timeline, radio/stream voice и SoundTouch.
- Android `app/src/main/cpp/CMakeLists.txt` использует тот же `voice_sources.cmake`;
`libutun_lite/utun_sources.cmake` исключает общий voice-контур из списка C-ядра.
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL`; актуальные категории и их число брать из этого header.
## 13. Порядок задач
+55
View File
@@ -0,0 +1,55 @@
Аудит config_parser — 26.10.2026
Полностью прочитаны config_parser.c/.h, config_parser_doc.md и общий config_ini.h.
Для согласования строгого разбора также повторно сверены config_updater.c/.h/_doc.md.
Разбор сохраняет модель и владение free_config; запуск ресурсов остаётся у instance.
Исправления
- IPv6 /32 больше не подменяется /128; префиксы проверяются до сужения к uint8_t.
Control allow=/0 задаёт нулевую маску без сдвига на 32; IPv6 control allow отклоняется.
- Ошибки известных обработчиков и выделения памяти отклоняют всю модель.
Все частичные списки освобождаются, включая client links и не завершённую секцию.
- Числа проверяются целиком до преобразования: знаки, хвост, диапазон и overflow.
Keepalive interval помещается в uint16_t; таймеры ограничены до умножения.
Bool унифицирован: 0/1, no/yes, false/true. Версия — ровно три десятичных октета.
- Адресные порты проверяются как decimal 0..65535, SOCKS — 1..65535.
Control ip/port даёт одинаковый результат в обоих порядках; ошибки getaddrinfo не теряются.
- Storage sizes проверяют UINT64_MAX, множитель M/G и полный хвост; -1 не становится безлимитом,
переполнение не становится нулевым лимитом. Слишком длинные строковые настройки отклоняются.
- Network ID ограничен 56 битами; требуются оба 32-byte HEX ключа.
Предоставленные ключи идентичности/пиров проверяются целиком; пустые необязательные остаются незаданными.
- CFG_CLIENT_LINK сохраняет имя local_srv. Повторный обычный addr сбрасывает режим dynamic IPv6.
- Флаг tcp_proxy_client enabled=no не включается снова следующей опцией той же секции.
Наличие service-секции задаёт её default enable и при отсутствии опций.
- Print_config не печатает private/signing keys; Networks выведены за пределы firewall-цикла.
Устаревший append ключей проверяет вход, запись и fclose; не объявляется атомарным updater.
- Updater выбирает итоговую идентичность/REALITY по последнему вхождению, но исправляет также
синтаксически плохие ранние дубли выбранными значениями. Из-за такого дубля ключи не генерируются заново.
Это нужно, поскольку строгий парсер проверяет каждое вхождение. Булевы REALITY enabled согласованы.
Контракты и ограничения
Обновлены заголовок и документ: порядок списков, необъединяемые named-секции, remote-only links,
синхронный DNS и первый getaddrinfo-адрес, Windows socket init, default-значения и границы,
повторные опции, поведение неизвестных секций/debug/gui и независимость parser/updater/start.
Bootstrap invite printer использует конфигурационные адреса и нулевые channel_id/join_key;
это не снимок фактических bind/NAT-адресов и не invite конкретного канала.
Нативные сборки/runtime Windows и FreeBSD в этом этапе не выполнялись.
Проверки
- Полный check.sh: 195/195 PASS, 0 failed/0 skipped, proxy9/9, burst1/1, load8/8,
134217728 байт, 169 секунд; /tmp/config-instance-audit-first-check.log.
- Профиль 6/6 PASS: config_parser, config_updater, config_debug, reality_config, ncd_config, services.
- Test_config_parser: 53 ошибочных конфигурации отвергнуты одинаково из файла/буфера;
10 отказов аллокаций с полным rollback, IPv4/IPv6 CIDR и control order, предельные размеры/порты,
скрытие секретов и /dev/full при append; баланс памяти сохранён.
- ASAN/UBSan/LSan: те же 6/6 PASS с detect_leaks=1/ halt_on_error=1.
Все встроенные lib/src/Opus инструментированы согласованно в отдельной копии
/tmp/utun-config-instance-asan-20261026; рабочие объекты не смешивались с sanitizer-объектами.
Первый updater sanitizer-прогон обнаружил обход test mock через glibc __fread_chk;
checked wrapper добавлен, повторный прогон всех файловых и allocation faults PASS.
- Логи: /tmp/config-parser-audit-contract.log, /tmp/config-parser-audit-updater.log,
/tmp/config-parser-audit-debug.log, /tmp/config-parser-audit-reality.log,
/tmp/config-parser-audit-ncd.log, /tmp/config-parser-audit-services.log.
Санитайзеры: соответствующие /tmp/config-parser-audit-asan-*.log,
сборка /tmp/config-parser-audit-asan-build.log и /tmp/config-parser-audit-asan-tests-build.log.
Общая сборка: /tmp/config-instance-audit-full-build.log.
+150 -12
View File
@@ -44,19 +44,35 @@ AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная пор
Автор подписывает все поля до sig. conv связывает автора с получателем.
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
Размер data в API отправки ограничен 1024 байтами; для file сюда входит открытый
descriptor (56 байт), поэтому на зашифрованные метаданные остаётся до 968 байт.
Парсер входящего wire допускает до 1024 байт ciphertext plaintext отдельно от descriptor.
seq — положительное int64, ts — миллисекунды NTP-времени instance.
```text
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
```
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
2=voice, 3=video, 4=JPEG, 5=PNG. Имя — basename до 255 байт, без разделителей, управляющих символов
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
JPEG/PNG имеют размеры с учётом EXIF, нулевую длительность и пустой waveform;
MIME `image/jpeg`/`image/png` определяется kind и не зависит от имени файла.
Desktop и Android проверяют формат и размеры средствами своего декодера до постановки
в `attachment_send`; исходные байты изображения сохраняются без перекодирования.
В сообщении показаны превью, разрешение и размер, нажатие открывает просмотр с масштабированием.
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
локального commit и до подтверждения получателем; S/M видят только ciphertext.
Имя собеседника ограничено `DM_PEER_NAME_MAX` (63 байта без NUL): создание,
загрузка и снимок бесед сохраняют только целые UTF-8 code points. Для 32 букв «Я»
сохраняется 31 буква (62 байта). При повреждённой кодировке, включая NUL внутри
SQL TEXT, остаётся корректный префикс; WARN содержит conv_id, смещение и длину.
Снимок нормализует также имя из nodes при пустом peer_name. Чтение не меняет БД;
при следующей штатной записи беседы нормализованное имя сохраняется вместе с ней.
Подробности исправления: [dm_audit_findings.txt](dm_audit_findings.txt), раздел9.
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
@@ -66,6 +82,7 @@ tags, точный размер и хеш; владелец публикует
## Владение и данные
- `dm_core`: беседы, история, outbox и подтверждение получателя.
- `dm_history`: поколения, hidden, точные отпечатки удалённых сообщений и манифест локальных путей.
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
@@ -83,6 +100,9 @@ dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
dm_outbox(conv_id, seq, body, retry_at)
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
dm_history(conv_id, generation, hidden)
dm_deleted_messages(conv_id, dir, seq, body_hash, media_id, media_receipt)
dm_local_files(path, conv_id, generation, busy, retry_at)
```
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
@@ -93,10 +113,41 @@ Dedup — точный ключ (conv,dir,seq) с проверкой сохра
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
но сохраняет БД. При старте очередь продолжает работу.
Локальное удаление (`dm_history_delete`) сохраняет запись беседы, ключи и счётчики,
скрывает её (`dm_history.hidden`) и увеличивает постоянное `generation`.
В одной транзакции удаляются история, outbox и локальные media-записи, а SHA256
точных удалённых тел сохраняется в `dm_deleted_messages` по `(conv_id,dir,seq)`.
Файловая квитанция сохраняется только если она уже была получена; удаление само её не создаёт.
После commit отменяются незавершённые операции; физическая уборка файлов выполняется отдельно.
Это локальная операция: история адресата и промежуточные копии ею не отзываются.
Точный входящий удалённый повтор получает квитанцию сообщения без повторного создания
истории/файла и без открытия скрытой беседы. Иное тело с тем же ключом удалённой записи
отклоняется. Впервые доставленное сообщение, включая меньший seq, открывает беседу;
явный `dm_start` также снимает hidden без смены поколения. Отложенные отправки
проверяют generation, чтобы старый запрос не попал в повторно открытую беседу.
Загрузка беседы различает `FOUND`, `NOT_FOUND` и `ERROR`: отсутствие означает
только успешно завершённый SELECT без строки. Ошибка SQL, повреждённые ключи
или отказ X25519 прекращают открытие/приём/отправку/приглашение без пересоздания
беседы и сброса счётчиков. Полный снимок выдаётся только после успешной загрузки.
Исправление и отказные проверки описаны в [dm_audit_findings.txt](dm_audit_findings.txt).
Таймеры работают раз в 1с: outbox A и текстовая очередь S обрабатывают до 32 задач;
локальные файлы endpoint, custody M и media jobs S — до 8 задач каждый.
Обычный retry_at — через 5с; исходящий файл с квитанцией проверяется через 3600с,
неудачная уборка локального пути повторяется через 30с. Это время допуска следующей
попытки, не гарантия доставки за указанный срок. Перед отправкой проверяется backpressure router.
Потеря пути сохраняет durable задачу; stop отменяет таймеры, но сохраняет БД.
При старте медиа сбрасывает retry_at/downloading; текстовые очереди используют сохранённый retry_at.
Все DM API вызываются в uasync-потоке. Порядок запуска в `chat_service_start`:
mailbox (включая media coordinator), core/history, media/file_transfer; к этому времени
SQLite, router и media_async уже живы. Порядок остановки: media_quiesce прекращает
новые задачи и закрывает transfer/CM handles; media_async_destroy завершает workers
и callbacks с живыми owners; затем уничтожаются media, core и mailbox, после них группы/чат.
SQLite принадлежит ядру и остановкой DM не закрывается. `dm_media_destroy` при живой
работе логирует ошибку и оставляет owner; заменять drain таким вызовом нельзя.
## Подтверждения и протокол
@@ -147,24 +198,69 @@ TTL текстовых сообщений не введён; недельный
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
поздний STORED не возрождает завершённую доставку.
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK
либо завершения подтверждённого TTL назначения; поздний STORED не возрождает доставку.
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
При проверке квоты M ошибки SQL чтения занятого места логируются и прекращают
обработку STORE без `REJECT` и регистрации custody. Назначение M сохраняется на S;
следующий STORE приходит через существующий таймер повторов. Квота сравнивается
только после получения строки SUM и успешного finalize. Реальный недостаток места
вызывает `REJECT` и выбор другого M. Исправление и регрессии:
[dm_audit_findings.txt](dm_audit_findings.txt), раздел6.
Освобождение назначения на S по EXPIRED/REJECT и сброс текущего holder выполняются
в одной транзакции. Ошибка prepare/bind/step/finalize/commit откатывает оба изменения.
Подтверждённое назначение завершается также таймером по сохранённому expires:
потерянный EXPIRED и offline B не оставляют stored/holder навсегда. После restart
таймер берёт те же durable факты. Если STORE ещё не подтверждён, S продолжает повторять
его; M возвращает STORED/EXPIRED из custody/tombstone. Квитанция B сохраняет приоритет
над TTL; старый control другого M не сбрасывает новое текущее назначение.
Текстовая история, подписи и сроки хранения M этим переходом не меняются.
Подробности и регрессии: [dm_audit_findings.txt](dm_audit_findings.txt), раздел11.
Control `ETCP_RT_ID_DM_MEDIA=0x38`: перед body идёт один байт команды.
Все многобайтные поля DM/control имеют native-endian порядок; исключение —
зашифрованный attachment_info с big-endian полями, описанный выше.
| Команда | Body |
| --- | --- |
| OFFER=1 | recipient:8, holder:8, expires:8, file-message |
| STORE=2 | recipient:8, file-message |
| STORED=3 | UUID:16, expires:8 |
| RECEIPT=4 / DELETE=5 | файловая квитанция:152 |
| DELETE_ACK=6 / EXPIRED=7 | UUID:16 |
| REJECT=8 | UUID:16, reason:1 (1 — недостаток квоты) |
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
учитываются при удалении; повторный STORE не продлевает TTL.
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
A предлагает собственный файл с holder=A и expires=0. S выбирает M с storage=yes
в общей группе A/B, исключая A/B и все прежние назначения этого UUID. Назначение
фиксируется до STORE; S/M могут быть одним узлом, local control обрабатывается синхронно.
Успешная передача control в router или локальный обработчик не означает durable успех.
EXPIRED относится к промежуточной копии. B помечает ещё не готовый файл expired,
но продолжает попытки получить оригинал у A. После проверки исходного ciphertext,
публикации и сохранения квитанции файл снова ready; поздний EXPIRED его не меняет.
Готовый файл A/B остаётся в локальной истории до явного удаления беседы.
При удалении busy-счётчик каждого пути удерживает его до callback worker/transfer;
уборщик работает по старым поколениям, использует busy=-1 для своего захвата
и синхронизирует каталог после удаления. Пользовательский исходник не входит в уборку,
если он не был явно передан как временный файл приложения.
Qt и Android получают историю через события ядра: 41 —
`[conv_len:1][conv:decimal][generation:8][JSON сообщений]`, 45 — JSON бесед.
Запрос истории с ненулевым generation читается только для текущей видимой беседы;
0 захватывает текущее поколение в uasync. UI отбрасывает снимок другого поколения.
Снимки ограничены 200 записями. Отправка вложения вызывает
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
@@ -194,6 +290,23 @@ Android использует общие `ConversationComposer`/`ConversationMess
## Проверки
Аудит контрактов DM завершён 27.10.2026: все семь заголовков `src/dm/`, этот документ,
`pm.txt` и `pm_invites.md` сверены с реализациями owners и относящимися к ним callers.
Это проверка соответствия документации; свежая интерактивная проверка клиентов и
новые платформенные сборки не выполнялись. Подробности и границы проверки —
[dm_audit_findings.txt](dm_audit_findings.txt), раздел 12.
Первоначальная проверка отказа SQLite выявила сброс счётчиков; дефект исправлен.
`test_dm_load` проверяет 79 отказных сценариев: чтение, bind/step/finalize, короткие
ключи и X25519; открытие/приём/отправку/JSON/invite до и после удаления; отдельные
отказы подготовки файла, отсутствие беседы, restart и продолжение seq. При отказе
сравниваются все поля DM-таблиц и отсутствие SQL-записей/событий успеха.
`test_dm_names`: 68 случаев с точным сравнением имени в SQL и JSON при создании,
загрузке, штатной записи, fallback из nodes и перезапуске. ASCII и 2/3/4-байтовые
символы проверяются до/на/за границей 63 байт; отдельно — overlong, surrogate,
значения выше U+10FFFF, неверные/оборванные последовательности, NUL и JSON-экранирование.
Снимки не меняют SQL; 273 полных JSON сохраняются для независимого строгого парсера.
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
неверные author/UUID/hash, обрезание и лишние байты.
@@ -210,17 +323,42 @@ ACK чужого тела, отказ mailbox без роли, точное уд
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
и restart S/M при выключенном источнике.
В Linux дополнительно проверяются prepare/step/finalize и неожиданный SQLITE_DONE
при подсчёте места, отсутствие REJECT/STORED и SQL-записей при ошибке; три сбоя
в первых трёх STORE, успешный четвёртый STORE на том же M и последующая доставка
после restart. Проверка настоящей квоты требует один REJECT до регистрации custody.
Дополнительно — 18 отказов освобождения EXPIRED/REJECT: prepare/bind/step/finalize
обоих UPDATE и commit, точный rollback назначения/job, успешный повтор, дубликат
и поздний STORED. Сетевой сценарий теряет EXPIRED, блокирует SQL-сброс, перезапускает S
и проверяет восстановление из сохранённого expires, состояние M и отсутствие ложной квитанции.
На M проверяются 11 SQL-отказов retirement: rollback сохраняет custody и не выдаёт ACK;
после restart M/S потерянный DELETE_ACK восстанавливается по tombstone и повтору DELETE.
После EXPIRED и возврата A проверяются точный оригинал на B, настоящая файловая
квитанция, очистка S и невлияние позднего EXPIRED на готовый файл.
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
Linux `test_dm_media_cleanup`: 13 отказов SQL при чтении назначений, записи ACK
и очистке body; сохранность квитанции, повтор, restart S, потерянный/повторный/чужой ACK,
несколько M и поздний STORED. Сеть в этом component-тесте перехвачена; подпись B и
реальное удаление M проверяет `test_dm_media`. Ошибка finalize после успешного
autocommit не означает rollback уже выполненной одношаговой записи.
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
Заключительный прогон 27.10.2026: штатные `test_dm`, `test_dm_history`, `test_dm_names`,
`test_dm_load`, `test_dm_e2e`, `test_dm_invite`, `test_dm_media`, `test_dm_worker`
и Linux `test_dm_media_cleanup` — 9/9 PASS.
Отдельный строгий JSON-парсер прочитал все 273 снимка имён. Заголовки проверены в C99/C++20;
в заключительном этапе менялись только документация, комментарии и форматирование деклараций.
Результаты старых сборок ниже относятся к указанной дате, а не к этому прогону.
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
+609
View File
@@ -0,0 +1,609 @@
DM — аудит документации и контрактов, 26–27.10.2026
База: /home/user/utun2, HEAD 03b050949cf36cc4868324be94a54eaddfdd494c.
Задание: /home/user/utun-doc-audit-checklist.txt, раздел src/dm/ и связанные документы.
Текущий статус на 27.10.2026: аудит контрактов DM завершён, 10/10 файлов [V].
Итог — раздел12; разделы1–11 сохраняют историю промежуточных проверок и исправлений.
1. Объём и статус
Полностью прочитаны dm_core.c/.h, dm_crypto.c/.h и dm_history.c/.h, doc/dm_arch.md,
doc/pm.txt и doc/pm_invites.md. Просмотрены заголовки media/mailbox owners, lifecycle
в utun_instance и участок media_forget_prepare/committed. Это не полный аудит этих owners.
dm_crypto.h [V]; dm_core.h/dm_history.h/doc/dm_arch.md [~].
dm_mailbox.h/dm_mailbox_media.h/dm_media.h/dm_media_priv.h сохраняют [ ].
doc/pm_invites.md сохраняет [~]; doc/pm.txt [ ]: нормативные требования прочитаны,
но соответствие всей доставки и хранения пока не проверено.
Уточнены контракты nonce, native-endian wire/длин, lifecycle, commit исходящей задачи,
JSON и локального удаления/поколений. В pm_invites исправлен формат события результата:
36 байт с generation вместо прежних 28. Платформенные реализации целиком не проверялись.
2. Исходный дефект: ошибка загрузки сбрасывает счётчики существующей беседы
dm_conv_load возвращает -1 при отсутствии строки и при ошибках prepare/step,
повреждённых ключах либо отказе X25519. dm_start проверяет только ==0, а иначе
обнуляет dm_conv, заполняет ключи и сохраняет через dm_conv_save.
ON CONFLICT обновляет last_out_seq/last_in_seq/last_ts из обнулённой структуры.
Ошибка чтения тем самым превращается в успешную запись с потерей счётчиков.
Проверка использует настоящие utun_core/chat_service, SQLite и OpenSSL, отдельные
БД в /tmp, штатные dm_start/dm_send/dm_accept_message. Исходные сообщения принимаются
обычным публичным API. SQLite authorizer однократно отказывает в чтении
dm_conversations.peer_node_id; последующие операции разрешены. Production-файлы,
архивы и объекты для воспроизведения не подменялись.
Положительный контроль: повторный dm_start без отказа сохраняет out=1,in=2.
При одном SQLITE_DENY: dm_conv_load пишет ERROR с точной причиной, затем dm_start
пишет INFO started, возвращает 0; out=1->0,in=2->0, обе строки dm_messages остаются.
Проверка ожидаемого отказа/сохранности даёт FAIL, exit2; tracked allocations0->0.
Существующей диагностики вместе с журналом инъекции и SQL-снимком достаточно
для однозначного вывода: это ошибка ветвления, не ошибка расшифровки или транспорта.
Последствия: пока исходящая строка seq=1 сохранена, новая отправка того же номера
будет конфликтовать с историей. После локального удаления история очищается,
а сброшенный счётчик сохраняется; повтор seq при неизменном ключе воспроизведёт
детерминированный nonce. Это вывод из кода, повторная отправка после сбоя в стенде
не выполнялась. Раскрытие содержимого/эксплуатация не проверялись.
В dm_accept_message и dm_invite_trampoline также смешаны отсутствие и ошибка
dm_conv_load; их отказные сценарии в этом этапе отдельно не воспроизводились.
Простой план исправления: выделить результаты FOUND/NOT_FOUND/ERROR для загрузки;
считать NOT_FOUND только завершённый SELECT без строки, ошибки логировать и
передавать без UPSERT/события успеха. Просмотреть все callers, включая dm_media;
при найденной беседе не обновлять seq из пустой структуры. Регрессии должны
проверить prepare/step/короткие ключи/crypto, сохранность полей и generation/hidden,
повторный запуск, успешное создание отсутствующей беседы и нормальное продолжение seq.
Новый формат протокола или БД для исправления не требуется.
3. Проверки и артефакты первоначального аудита, до исправления
Сборка штатных шести целей через tests/Makefile — PASS.
test_dm/test_dm_history/test_dm_e2e/test_dm_invite/test_dm_media/test_dm_worker:6/6 PASS.
Полные логи прочитаны; ERROR в штатных отрицательных проверках ожидаемы.
Worker:24 запуска,49 heartbeat,max_gap11.3мс. Media:phase9 PASS.
Эти тесты не покрывают подтверждённый отказ загрузки беседы.
/tmp/dm-audit-baseline-build.log
/tmp/dm-audit-baseline-results.log
/tmp/dm-audit-baseline-test_dm{,_history,_e2e,_invite,_media,_worker}.log
/tmp/dm-audit-load-failure.c и /tmp/dm-audit-load-failure.mk
/tmp/dm-audit-load-failure-build.log
/tmp/dm-audit-load-failure.log (контроль PASS, сохранность при отказе FAIL, exit2)
Первый запуск стенда в sandbox остановился до DM на запрете netlink-сокета.
Окончательные стенд и штатные тесты запущены с разрешением вне sandbox;
сетевые настройки хоста не менялись. Пользовательская БД не использовалась.
Рабочий алгоритм, протокол, БД и штатные тесты не менялись: только документы/комментарии.
Новые полный check.sh, ASAN/UBSan/LSan, GUI/Android/Windows/FreeBSD не запускались.
По AGENTS.md:10 аудит остановлен на подтверждённом дефекте до его исправления;
открытая задача внесена в doc/tasks.md. Коммит/push не выполнялись.
4. Исправление по запросу пользователя — 26.10.2026
dm_conv_load теперь возвращает FOUND/NOT_FOUND/ERROR. Только SQLITE_DONE без строки
и с успешным finalize означает NOT_FOUND; SQL-ошибки prepare/bind/step/finalize,
короткие ключи и отказ X25519 означают ERROR. Полная структура выдаётся только
после успешного завершения загрузки/вывода ключа. Лог содержит conv/rc/SQL-причину,
успешная загрузка с seq доступна в TRACE, отсутствие записи — DEBUG.
Проверены все5 callers: dm_start и dm_accept_message возвращают ошибку без
создания/сохранения/открытия беседы, dm_invite_trampoline завершает запрос
DM_INVITE_START_ERROR без повторного dm_start; dm_send и JSON требуют FOUND.
В dm_start также перестала игнорироваться ошибка сохранения выбранной области.
Медиа не вызывает dm_conv_load и не создаёт беседу вместо ошибочной загрузки:
dm_send_file отклоняет ошибки чтения/ключей до регистрации worker/файлов.
Протокол и формат БД не менялись.
Постоянная Linux-регрессия test_dm_load включена в check_PROGRAMS.
35 комбинаций5 callers×7 отказов для видимой беседы, ещё35 после локального удаления,
4 отказа отдельной media-подготовки и5 отказов открытия отсутствующей беседы:79 PASS.
Отказы:prepare/bind/step/finalize,короткие X25519/Ed25519 BLOB,EVP_PKEY_derive.
Проверяются SHA256 всех полей7 DM-таблиц,sqlite3_total_changes,autocommit,
отсутствие событий успеха и неизменённый буфер квитанции при отказе приёма.
Нормальные контроли:создание отсутствующей беседы,повторное открытие после restart,
продолжение исходящего seq1->2 и приём впервые доставленного seq1 после seq2.
Запрос invite проверяется на настоящей собственной CHAT-группе; общая доставка
и создание беседы при первом входящем сообщении покрываются штатными E2E/media-тестами.
Новая регрессия с прежним dm_core.c из HEAD ожидаемо FAIL на первом prepare-отказе:
ошибка -> dm_start0,дополнительные SQL-write/CONV_UPDATED;cleanup allocations0->0.
Использовались отдельные /tmp/old-core.o и бинарник; рабочий архив не подменялся.
Первоначальный стенд с настоящим SQLITE_DENY теперь PASS:dm_start=-1,out1->1,in2->2,
две строки истории сохранены,allocations0->0. Оба контрольных лога прочитаны полностью.
Сборка Linux PASS; DM-профиль test_dm_load/dm/history/e2e/invite/media/worker7/7 PASS.
ASAN/UBSan/LSan тот же профиль7/7 PASS с detect_leaks=1/halt_on_error=1.
Отдельная копия /tmp/utun-dm-load-sanitizers:новая сборка core/libuasync/Opus
с согласованными -fsanitize=address,undefined,без смешивания обычных объектов.
dm_core.c,test_dm_load.c,tests/Makefile.am побайтно совпадают с рабочими.
Проверены все sanitizer-логи:нет отчётов ASAN/UBSan/LSan;test_dm_load allocations0->0.
Первый полный check.sh:202 passed,1 failed,0 skipped (203 total).
Отказ test_chat_join_e2e:happy A!=C,J не декодировал приглашение,потом A/C не нашли J;
degraded и invalid-key PASS. Отдельный диагностический повтор3/3 PASS.
Код теста создаёт link.txt до записи/закрытия;читатель ждёт только access(F_OK).
Это возможная гонка тестовой публикации,но конкретная причина исходного отказа
не доказана без размеров/результатов чтения. Отдельная задача в doc/tasks.md;
production JOIN и критерии/код теста не менялись,первый отказ сохранён.
Окончательный повтор полного check.sh:203 passed,0 failed,0 skipped;exit0,175с.
Интеграции proxy9/9,burst1/1,load8/8 PASS,134217728 проверенных байт.
В этом прогоне новый test_dm_load PASS;код после профильных проверок не менялся.
Повторный успех не закрывает отдельную задачу диагностики исходного отказа JOIN.
Логи исправления:
/tmp/dm-load-fix-build.log (окончательная сборка PASS)
/tmp/dm-load-fix-profile-results.log и /tmp/dm-load-fix-test_dm*.log
/tmp/dm-load-fix-controls-results.log и /tmp/dm-load-fix-control-*.log
/tmp/dm-load-fix-negative-build.log и /tmp/dm-load-fix-original-build.log
/tmp/dm-load-fix-sanitizers-build.log,/tmp/dm-load-fix-sanitizers-results.log,
/tmp/dm-load-fix-sanitizers-test_dm*.log,/tmp/dm-load-fix-final-verification.log
/tmp/dm-load-fix-full-check-first.log,/tmp/dm-load-fix-join-first.log,/tmp/dm-load-fix-join-repeat.log
/tmp/dm-load-fix-full-check.log,/tmp/dm-load-fix-netns.log,/tmp/dm-load-fix-repeat-netns.log
Полный набор запускается в отдельном IPv6 network namespace без изменения сети хоста.
Начальные ошибки подготовки sanitizer-копии (недостающие generated auxiliary files/
etcpmon header и ограничение LSan в sandbox) устранены до успешной сборки/прогона.
В первом native build не хватало openssl/evp.h в новом тесте;добавлен до успешной сборки.
GUI/Android/Windows/FreeBSD не запускались;регрессия с linker wrappers — Linux.
Дефект счётчиков исправлен;исходные частичные отметки аудита остальных DM owners сохраняются.
Коммит/push не выполнялись.
5. Исходный отказ mailbox/media — 26.10.2026: transient SQL становится постоянным отказом M
Прочитаны полностью dm_mailbox.c,dm_mailbox_media.c,dm_media.c и четыре заголовка,
связанные file_transfer/media_async контракты,lifecycle instance и штатный test_dm_media.
Уточнены uasync/ownership/lifecycle,семантика принятия PUT,транзакция регистрации media,
предусловия размеров control,ограничение рассылки до64 S на группу и SQL-helper ownership.
Четыре новых заголовка отмечаются[~]:чтение завершено,но полный аудит контрактов/отказов
и документация media API не завершены. Прежние core/history/dm_arch[~],crypto[V] сохраняются.
Дефект в dm_custody_store:результат SELECT COALESCE(sum(cipher_size),0) FROM dm_custody
при любом rc!=SQLITE_ROW превращается в used=UINT64_MAX. Следующая ветка used>quota
выдаёт DM_MEDIA_REJECT reason1,то есть утверждает реальное переполнение квоты.
Оригинальная причина SQL не логируется;WARN показывает фиктивный used18446744073709551615.
dm_mailbox_media_recv на S сохраняет released=1 и holder=0. dm_job_storage исключает
все прежние назначения через NOT IN(SELECT node_id FROM dm_media_holders WHERE id=?),
включая released. Поэтому повторные STORE на этом M для UUID больше не возникают.
Доказательство:/tmp/dm-quota-audit.c/.mk,временный вариант штатного test_dm_media.
Настоящие четыре utun/chat ядра A/B/S/M,разные БД,подписанные channel/member/admin-блоки,
реальные UDP-транспорты и file_transfer. Сначала штатная прямая доставка и прежние
проверки rollback/реальной квоты;затем B останавливается,A отправляет новое вложение.
Прикладной алгоритм,архивы,production-объекты и штатный тест не подменялись.
Linker-wrapper однократно возвращает SQLITE_IOERR только для used-space SELECT в БД M.
В обеих проверках настроено1024МиБ,текущая custody пуста;ciphertext запрашивает98385байт.
Контроль без отказа:назначение M->custody reserved->файл сохранён->STORED на S.
Через16 календарных секунд тестовых часов:ready1,released0,waiting0,holders1,used98385;
phase9 PASS,exit0,allocations0->0.
Fault:SQLITE_IOERR->quota refused usedUINT64_MAX requested98385 quota1073741824,
затем storage refused reason1 на S. Через16с:ready0,released1,waiting1,holders1,used0;
quota-запрос STORE выполнен только один раз (финальный диагностический SELECT в счётчик не включён).
phase4 ожидаемо FAIL,exit1,allocations0->0. После однократного отказа SQLite работает;
возобновления назначения нет. Persistent-исключение после restart следует из таблицы/SQL,
отдельный restart в этом стенде не выполнялся.
Логи:/tmp/dm-quota-audit-build.log,/tmp/dm-quota-audit-results.log,
/tmp/dm-quota-audit-control.log и /tmp/dm-quota-audit-fault.log.
Ключевые строки всех переходов сохранены в /tmp/dm-quota-audit-diagnostics.log.
В baseline protocol_checks ожидаемы отдельный SQL-trigger rollback и настоящий
quota-отказ2МиБ при квоте1МиБ:они происходят до включения fault и не являются новым дефектом.
Работающие transport/media callbacks и SQL-снимки однозначно показывают механизм;
production-диагностика должна дополнительно различать SQL-ошибку и настоящую квоту.
Простое исправление:проверять результат чтения занятого места отдельно,логировать
ошибки SQL с rc/причиной и возвращаться без REJECT. Сохранённый holder/stored0 на S
уже обеспечивает повтор STORE через существующий таймер. Реально недостаточная
квота сохраняет нынешний REJECT,выбор другого M и учёт всех назначений.
Также проверить bind/finalize,не выдавать operational-ошибку за quota refusal.
Постоянная регрессия:однократные SQL-отказы не исключают свободный M,последующий
STORE сохраняет ciphertext;реальная квота по-прежнему отклоняет запрос до загрузки.
Новый протокол/формат БД/специальный control для retry не требуется.
По AGENTS.md:10 аудит остановлен до исправления;открытая задача в doc/tasks.md.
В этом этапе изменены только комментарии/документы/отметки;исправление dm_conv_load
и новая постоянная регрессия из предыдущего этапа сохранены. Новые fullcheck/ASAN/UBSan/LSan,
GUI/Android/Windows/FreeBSD не запускались;прежние203/203 и sanitizer7/7 относятся
к предыдущему исправлению,а не к новому отказу custody. Коммит/push не выполнялись.
6. Исправление quota-admission — 26.10.2026
По запросу пользователя dm_custody_store проверяет чтение занятого места и finalize
до сравнения с квотой. Ошибка prepare уже возвращалась без ответа;теперь ошибка step,
неожиданный SQLITE_DONE и ошибка finalize также прекращают STORE без REJECT/регистрации.
Лог step/finalize содержит rc,SQL-причину и указание на сохранённый повтор STORE.
Прежнее назначение M остаётся на S;его штатный таймер повторяет STORE. Настоящий
недостаток квоты по-прежнему выдаёт reason1. У агрегатного SELECT нет bind-параметров.
Протокол,формат БД и алгоритм назначения S не менялись.
Постоянный test_dm_media расширен fault wrappers только для Linux:
- Отдельные prepare/step/finalize/неожиданный DONE проверяют один отказ за вызов,
отсутствие REJECT/STORED,отсутствие изменений SQL в S/M,пустые custody/tombstones,
autocommit. Достаточная квота настроена1024МиБ;отказ не заменяется успешной регистрацией.
- Настоящее превышение квоты требует ровно один REJECT без STORED/регистрации custody.
- Сетевой сценарий использует прежние4 ядра,подписанные роли,настоящие UDP/router/transfer.
После отключения B первые3 STORE получают разные SQL-ошибки на M:prepare,step,finalize.
Четвёртый STORE сохраняет файл на том же M;REJECT0,STORED1,одно назначение,released0.
Последующие штатные проверки перезапуска S/M при выключенном A,точного файла у B,
квитанций,удаления промежуточной копии/тела S,поздних STORE/STORED и TTL сохранены.
Исходные4 сбоя проверяют обработчик после router,сетевой retry идёт обычным транспортом.
Нативный и sanitizer-профили7/7 PASS:test_dm_load/dm/history/e2e/invite/media/worker.
В test_dm_media phase9 PASS,allocations0->0;в native/sanitizer одинаковая последовательность
hits3,STORE4,REJECT0,STORED1,holders1. ASAN/UBSan/LSan7/7 PASS,detect_leaks=1,
halt_on_error=1;ядро,libuasync и встроенный Opus собраны с одинаковыми sanitizer-флагами.
Использована отдельная копия /tmp/utun-dm-quota-sanitizers;обычные объекты не подменялись.
Побайтно сверены dm_media.c/test_dm_media.c/tests/Makefile.am и прежние dm_core.c/test_dm_load.c.
Контроли:
- Новый test_dm_media с dm_media.c из HEAD ожидаемо FAIL,exit1:step-отказ даёт ложный
REJECT,который ловит новая проверка. allocations0->0. Старый модуль собран отдельным
объектом и включён в отдельный бинарник;рабочий libutun.a не подменялся.
- Исходный /tmp/dm-quota-audit.c с одним SQLITE_IOERR и исправленным core:phase9 PASS,exit0.
- test_dm_media без DM_QUOTA_FAULT_INJECTION:штатный сценарий phase9 PASS,exit0.
Это Linux-компиляция без wrappers;runtime других ОС не проверялся.
Все3 контроля восстанавливают tracked allocations до0. Полные логи изменённого
media-теста/native/sanitizer/контролей прочитаны;ожидаемые ERROR относятся к инъекциям.
Артефакты:
/tmp/dm-quota-fix-build.log
/tmp/dm-quota-fix-profile-results.log,/tmp/dm-quota-fix-test_dm*.log
/tmp/dm-quota-fix-sanitizers-build.log,/tmp/dm-quota-fix-sanitizers-results.log,
/tmp/dm-quota-fix-sanitizers-test_dm*.log
/tmp/dm-quota-fix-controls-build.log,/tmp/dm-quota-fix-controls-results.log,
/tmp/dm-quota-fix-control-{test_dm_media_old,dm-quota-fixed-original,test_dm_media_no_injection}.log
/tmp/dm-quota-fix-final-verification.log
/tmp/dm-quota-fix-full-check.log,/tmp/dm-quota-fix-netns.log
Полный check.sh203/203 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8 PASS,
134217728 проверенных байт,exit0,176с. Этот прогон включает новую fault/retry-проверку media.
Набор выполнен в отдельном IPv6 network namespace;сеть хоста не менялась.
Первый сборочный запуск отдельного negative-control не включал test_clock.o в линковку;
временная make-цель исправлена до контрольных прогонов. Основная сборка/код от этого не менялись.
Документация обновлена,задача quota-admission закрыта. Остальные отметки аудита DM остаются
частичными;GUI/Android/Windows/FreeBSD не запускались. Коммит/push не выполнялись.
7. Commit/rebase/push по запросу пользователя — 26.10.2026
Все накопленные изменения DM сохранены в2ae1990d. Fetch обнаружил8 новых коммитов
origin/master поверх03b05094:изменения headless PCM,документации звонков и их проверок.
Rebase на03be46c3 завершён с единственным конфликтом doc/tasks.md:сохранены обе
полные группы задач. tests/Makefile.am объединился автоматически,цели test_dm_load,
quota wrappers test_dm_media и upstream test_call_headless_queue присутствуют.
DM .c/.h и обе новые/расширенные регрессии побайтно сохранены относительно исходного коммита.
После rebase коммит стал b6588d059455ed430c902a325af7d010f15f3475.
Нативная сборка PASS;новый полный check.sh204/204 PASS,0 failed/0 skipped,
proxy9/9,burst1/1,load8/8 PASS,134217728 байт,exit0,175с. Включён новый upstream
test_call_headless_queue (11/11) и все DM-регрессии. Сеть — отдельный IPv6 namespace.
Push origin master:03be46c3..b6588d05 PASS,без force. git ls-remote подтвердил hash,
HEAD==origin/master,рабочее дерево после push было чистым.
Логи:/tmp/dm-audit-rebase-build.log,/tmp/dm-audit-rebase-full-check.log,
/tmp/dm-audit-rebase-netns.log. Санитайзеры после rebase заново не запускались;
исходные DM-файлы по сравнению с прошедшим sanitizer-профилем не менялись.
8. Продолжение: UTF-8 имя повреждается при построении беседы
Продолжены сверка core/history,передачи поколения и путей из attachment_send,
участков headless/GUI/JNI вызывающих функций. Это чтение кода,не аппаратная проверка UI.
Уточнён dm_send_file:0 означает вызов submit,который вправе синхронно вызвать callback
ошибки;результат подготовки сообщается событием. Это исправление описания возврата.
Новый подтверждённый дефект:dm_conv.peer_name[64],snprintf в dm_start/dm_conv_load
режут имя по63байтам,без проверки границы UTF-8. json_escape передаёт получившиеся
байты в JSON без проверки/исправления. Корректное входное имя тем самым повреждается
в SQLite,а dm_list_conversations_json возвращает0 с некорректным UTF-8 JSON.
dm_start_req допускает до127байт имени;отсутствует предусловие ASCII/<=63байт.
Стенд:/tmp/dm-utf8-audit.c/.mk. Настоящие core/chat_service/SQLite/X25519,
отдельные ключи для трёх собеседников,новая БД/tmp;публичные dm_start/list API.
Валидные входы:ASCII63байта;31×«Я»=62байта;32×«Я»=64байта.
Контроли ASCII63/UTF8_62 дают корректный UTF-8 JSON. UTF8_64 сохраняется как63байта,
последний байт0xd0 без продолжения;start=0,list=0. Строгий Python decode JSON
даёт invalid continuation byte на позиции537;проверка exit2. SQL-readonly-снимок
подтверждает тот же обрыв в сохранённом peer_name,tail_hex=afd0afd0afd0afd0.
Tracked allocations0->0;собственно C-стенд exit0,отказ обнаруживает независимый валидатор.
Сам runtime-лог тоже содержит этот некорректный байт:валидатор читает диагностический
лог с replacement только для поиска ASCII-путей,но JSON и SQL BLOB проверяет строго.
Причина однозначна по точным входным/сохранённым длинам и байтам;это не ошибка JSON
парсера,шифрования или вывода терминала. Повторная загрузка имени через dm_conv_load
имеет то же побайтовое усечение;этот путь в новом стенде отдельно не запускался.
Простой план:определить единое ограничение имени и копировать только целые UTF-8
code points. Проверять входы и уже сохранённые некорректные строки с диагностикой;
увеличение массива само по себе не решает обрыв на новой границе.
Постоянные проверки:ASCII и2/3/4-байтовые символы до/на/за границей,SQL round-trip,
повторная загрузка и строгая декодируемость JSON. До выбора обработки уже повреждённых
имён (безопасное усечение/отказ) рабочая реализация не менялась.
Логи:/tmp/dm-utf8-audit-build.log,/tmp/dm-utf8-audit-runtime.log,
/tmp/dm-utf8-audit-validation.log;валидатор:/tmp/dm-utf8-audit-validate.py.
По AGENTS.md:10 дальнейший аудит остановлен,задача открыта в doc/tasks.md.
В продолжении только комментарии/документы,все текущие частичные отметки сохранены.
Предыдущий check204 проверял объединённые исправления,а не обработку нового имени;
новые sanitizer/GUI/Android/Windows/FreeBSD проверки не запускались.
9. Исправление UTF-8 имени собеседника
Предел имени явно задан DM_PEER_NAME_MAX=63 байта без NUL. Единый dm_copy_peer_name
сохраняет целые UTF-8 code points при dm_start, dm_conv_load, автосоздании во входящем
dm_accept_message и построении снимка бесед. Входящее имя из nodes сначала читается
в128байт, чтобы прежнее предварительное усечение в64байта не портило символ.
JSON нормализует как SQL peer_name,так и fallback из nodes при пустом имени.
Некорректная последовательность,overlong,surrogate,значение выше U+10FFFF или NUL
внутри SQL TEXT обрывают имя перед первым недопустимым code point. WARN содержит
conv_id,смещение и исходную длину;обычное усечение длинного имени отмечается DEBUG.
Чтение/снимок не исправляют SQL автоматически. Следующая штатная запись беседы
сохраняет уже нормализованное имя;формат БД/wire и счётчики сообщений не меняются.
Постоянный test_dm_names добавлен в штатный набор tests/Makefile.am:
68 случаев с реальными core/chat_service/SQLite и отдельными ключами собеседников.
ASCII0/1/62/63/64/127байт;границы2/3/4-байтовых символов до/на/за63байтами,
повторяющиеся символы,включая U+10FFFF;12 видов некорректных последовательностей
с пустым/непустым префиксом;SQL TEXT с NUL;кавычки/слеш/newline и кириллица.
Каждый случай проверяет точные SQL/JSON имена при создании,чтении повреждённого
сохранённого имени,load через чтение сообщений,load/save через dm_send и fallback.
total_changes и исходные SQL-байты подтверждают отсутствие скрытых записей при чтении.
Отдельно проверен restart с той же БД/идентичностью;allocations0->0.
Независимый строгий Python decode UTF-8 + json.loads проверил273 полных снимка,
9452 строки бесед и предел63байта на каждое имя;PASS в native и sanitizer сборках.
Контроль с прежним core:новая регрессия FAIL,exit1,на чтении длинного SQL-имени
после первых4 случаев (прежний JSON обходил предел);allocations0->0.
Исходный стенд раздела8 повторён на исправленном core:ASCII63/UTF8_62/UTF8_64 PASS,
32×«Я» сохраняются как62байта,строгий JSON всех3 снимков корректен,allocations0->0.
Исходные доказательства дефекта сохранены в прежних /tmp/dm-utf8-audit-*.log.
Нативная сборка PASS;DM-профиль8/8 PASS: names/load/crypto/history/e2e/invite/media/worker.
ASAN/UBSan/LSan8/8 PASS,detect_leaks=1,halt_on_error=1;отчётов о памяти/UB/утечках нет.
Изолированная копия /tmp/utun-dm-utf8-sanitizers синхронизирована с текущими исходниками,
включая upstream call_headless;ASAN/UBSan-ссылки проверены в core/libuasync/Opus.
Полный check.sh205/205 PASS,0 failed/0 skipped,proxy9/9,burst1/1,load8/8,
134217728байт,exit0,172с. Сеть — отдельный IPv6 namespace,сеть хоста не менялась.
Логи:/tmp/dm-utf8-fix-{build,profile-results,test_dm_names,negative-build,negative}.log,
/tmp/dm-utf8-fix-{original-build,original,original-validation,json-validation}.log,
/tmp/dm-utf8-fix-{sanitizers-build,sanitizers-results,sanitizers-test_dm*,full-check,netns}.log.
Задача UTF-8 закрыта;общий аудит DM остаётся частичным:crypto[V],остальные6headers
и dm_arch[~]. Платформенные GUI/Android/Windows/FreeBSD проверки не выполнялись.
Финальная сверка перед отправкой:fetch принёс4 Android mute-коммита до fa76447f.
Rebase прошёл без конфликтов;DM исходники/headers,Makefile и регрессия побайтно
сохранены. Коммиты исправления/отчёта после rebase:d09ed031/87d24b72.
Повторная native сборка PASS;check.sh205/205 PASS,0 failed/0 skipped,proxy9/9,
burst1/1,load8/8 PASS,134217728байт,exit0,175с. Строгие273 JSON/9452строки снова PASS.
Логи:/tmp/dm-utf8-fix-rebase-{build,full-check,json-validation,netns}.log.
Санитайзеры после rebase не повторялись:DM runtime не менялся,общий call_audio.h
в upstream изменяет только комментарий;новые платформенные Android callers этим
DM-профилем не проверяются. Upstream задачи/отчёты Android mute сохранены целиком.
10. Продолжение mailbox: SQL-ошибка оставляет завершённое TTL-назначение активным
База продолжения:3dc5eb502db6964fc6467385cc65db9d1df83c44,рабочее дерево было чистым.
Повторно сверены dm_mailbox.c/.h,dm_mailbox_media.c/.h и относящиеся к квитанциям,
контролю и TTL функции dm_media. Приём PUT проверяет точное тело,роль S и общую
группу;квитанция текста и удаление pending объединены транзакцией. Это чтение
соответствующих путей,не полная проверка всех SQL/allocator/платформенных отказов.
Подтверждён новый дефект:dm_mailbox_media_recv в EXPIRED сначала делает
UPDATE dm_media_holders SET released=1,затем отдельным statement обновляет job:
holder=0,stored=0,expired=1,retry_at=0. При отказе второго statement первое изменение
остаётся в БД. Таймер при недоступном B видит прежние holder и stored=1,поэтому
не выбирает нового M,не повторяет STORE и не сверяет expires с текущим временем.
На M custody уже удалена;dm_custody_done хранит tombstone,но таймер эту таблицу
не сканирует и не повторяет уведомление EXPIRED сам по себе.
Стенд:/tmp/dm-expiry-audit.c/.mk — отдельный вариант штатного test_dm_media.
Настоящие4 ядра A/B/S/M с подписанными мемберскими/административными блоками,
разными SQLite,реальными UDP/BGP/file_transfer и прежними проверками доставки.
Штатные quota-fault проверки идут в обоих прогонах одинаково;новая инъекция только
на S и только на UPDATE завершения EXPIRED,однократно SQLITE_IOERR в sqlite3_step.
Рабочие C,штатные тесты,объекты и libutun не подменялись;данные только в/tmp.
Оба прогона сначала проходят прямую доставку,offline custody и восстановление
S/M с выключенным A,побайтную доставку B и удаление промежуточной копии.
Затем B снова выключен;для третьего файла настроен TTL1с на M. M успевает принять
98385байт ciphertext,подтвердить хранение,по TTL удалить custody и послать EXPIRED.
Контроль без новой инъекции:phase9 PASS,exit0,allocations0->0. Для seq3 на S
holder0,stored0,expired1;назначение released1. На M нет custody,есть expired tombstone.
Fault:UPDATE завершается rc10,hit1,ошибка логируется вместе с точным SQL. После
ещё16 календарных секунд тестовых часов и нескольких успешных повторов PUT:
на S holder723190674248656275,stored1,expired0,expires1793032708,receiptNULL,body сохранён;
для того же UUID E20AC64A2660882CE73D68D48E2AFFA0 назначение этого M released1.
На M custody0,expired tombstone1. Phase8 FAIL,exit1,allocations0->0.
В runtime released=2 считает также прежнее успешно доставленное назначение;
независимый readonly SQL JOIN отдельно подтвердил released1 для проверяемого UUID.
Остановка/освобождение owners штатные,не маскируют сохранённое расхождение.
Окно отсутствия восстановления16с проверено исполнением;отсутствие пути восстановления
при продолжающемся offline B следует также из ветки таймера. Restart после этой
инъекции,возврат B и дополнительный storage не запускались;потеря до TTL не заявляется.
Это нарушение согласованности durable состояния после SQL-отказа,а не ошибка самого
удаления по TTL:копия M удалена в предусмотренный срок.
Направление исправления:идемпотентный восстанавливаемый переход EXPIRED с согласованным
состоянием job/holder;подтверждённое назначение проверять таймером по сохранённому expires.
Одного BEGIN/COMMIT вокруг двух UPDATE недостаточно:при rollback событие уже потреблено,
а M не повторит EXPIRED после удаления custody. Нужны отказные проверки обоих statement,
commit и восстановления после restart;поздний STORED не должен отменить завершение.
Сходная последовательность REJECT/DELETE_ACK замечена при чтении,но отдельный runtime
её отказов в этом продолжении не выполнен. Конкретная реализация пока не менялась.
Сборка отдельного стенда PASS. Логи:/tmp/dm-expiry-audit-{build,control,fault,validation}.log;
полные журналы сохранены,DM-выборки без транспортного шума — *-control-dm.log/*-fault-dm.log.
Прежний check205/санитайзеры относятся к исправлению UTF-8 и не покрывали этот отказ.
Новые fullcheck/sanitizer/GUI/Android/Windows/FreeBSD проверки не выполнялись.
По AGENTS.md:10 дальнейший аудит остановлен;задача открыта в doc/tasks.md.
В продолжении только комментарий контракта и отчёт/задача;частичные отметки сохранены.
11. Исправление восстанавливаемого завершения TTL
На S добавлен единый dm_job_release для EXPIRED/REJECT:BEGIN IMMEDIATE,освобождение
назначения и сброс текущего holder/stored,COMMIT. Prepare/bind/step/finalize обоих
statement и COMMIT проверяются;при ошибке ROLLBACK,без сообщения об успешном переходе.
Лог ошибки содержит M,rc/finalize,точный SQL и причину;успех — M,причину expired/rejected
и отдельные счётчики изменения назначения/job. Идемпотентный повтор не пишет released
заново;старый M не сбрасывает нового holder. WHERE receipt IS NULL сохраняет приоритет
подписанной квитанции B. DELETE_ACK остаётся отдельным одношаговым освобождением.
Таймер S независимо проверяет сохранённый expires подтверждённого stored-назначения.
При истечении выполняет тот же переход и перечитывает job после commit;ошибка остаётся
на следующем retry_at. Это работает при offline B,потерянном EXPIRED и после restart.
Для ещё не подтверждённого хранения STORE продолжает повторяться;M отвечает из
custody/tombstone. Поэтому для отсутствующего STORED новый механизм не требует
выдуманного срока хранения. Формат таблиц/wire,подписи и TTL на M не менялись.
Лог удаления тела S уточнён:освобождены все назначения. Завершение TTL не утверждает,
что worker M уже закончил физическое удаление в тот же момент;его владелец и повторы
сохраняются на M. Неистёкшие назначения продолжают удаляться до DELETE_ACK.
Штатный Linux test_dm_media расширен,без отдельной подмены production:
18 component-сценариев на настоящем job S:EXPIRED и REJECT × prepare/bind/step/finalize
обоих UPDATE и COMMIT. После каждого отказа сравниваются все6 полей назначения/job
(holder,stored,expired,expires,retry_at,released),проверяется autocommit;нормальный повтор
завершает переход,дубликат и поздний STORED не меняют результат и не создают receipt.
Фикстура возвращается синхронно до следующего uasync callback;это component-проверка
SQL/control,не утверждение о реальном удалении M на каждом из18 искусственных событий.
Сетевой TTL-сценарий использует настоящие4 ядра A/B/S/M и прежний полный путь
offline-файла с restart S/M при выключенном A,точным файлом на B и удалением копий.
Затем B отключён,на M TTL1с. EXPIRED от M намеренно теряется,SQL-сброс job на S
блокируется до удаления custody M. Проверены сохранённый holder/stored и released0
после rollback;S перезапускается. Без получения EXPIRED таймер S восстанавливает
holder0,stored0,expired1,released1. Поздний STORED с будущим expires и STORE не
возрождают назначение/файл и не выдают ложную квитанцию доставки.
После restart поздний component-control ждёт реального восстановления ETCP-линка.
Native и ASAN:18 checks,один заблокированный сброс,два потерянных EXPIRED (включая
ответ на поздний STORE),restart1,phase9 PASS,allocations0->0.
Независимое readonly чтение SQLite по точному UUID подтвердило на M custody0,
expired tombstone1;на S holder0/stored0/expired1,receiptNULL,body сохранён и released1.
Прежний job seq2 сохранил квитанцию и удалённое тело. Проверено и в sanitizer-копии.
Прежний координатор в отдельном /tmp/dm-expiry-old-coordinator.o/бинарнике ожидаемо FAIL
новой регрессии на bind-отказе первого UPDATE:частичный переход,phase4,exit1,
allocations0->0. Рабочий libutun не подменялся. Исходный one-shot IOERR-стенд раздела10
повторён на исправлении:matching SQL дополнен receipt IS NULL,hit1,затем успешный
переход,phase9 PASS,exit0,allocations0->0;исходные доказательства дефекта сохранены.
Нативная сборка PASS;DM-профиль names/load/crypto/history/e2e/invite/media/worker8/8 PASS.
ASAN/UBSan/LSan8/8 PASS,detect_leaks=1,halt_on_error=1;отчётов памяти/UB/утечек нет.
Изолированная /tmp/utun-dm-expiry-sanitizers побайтно совпадает с проверяемыми
dm_mailbox_media.c/.h,tests/Makefile.am/test_dm_media.c;core/libuasync/Opus содержат
ASAN/UBSan-инструментацию. В media-логе при restart один packet too small for init
size68;прогон восстановил линк и завершился,это не отчёт санитайзера.
Первую сборку запустил до завершения изменения linker flags,она отказала на
__real_sqlite3_bind_blob. После окончания правок повторная полная сборка и отдельная
сборка media PASS;проверки выполнялись на согласованных окончательных объектах.
Полный check.sh205/205 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8 PASS,
134217728байт,exit0,173с. Сеть — отдельный IPv6 namespace,настройки хоста не менялись.
Логи:/tmp/dm-expiry-fix-{final-build,media-build,profile-results,test_dm*,validation}.log,
/tmp/dm-expiry-fix-{negative-build,negative,original-build,original}.log,
/tmp/dm-expiry-fix-{sanitizers-build,sanitizers-results,sanitizers-test_dm*,full-check,netns}.log.
Задача завершения TTL закрыта. Общий аудит DM частичный;прежние отметки сохраняются.
GUI/Android/Windows/FreeBSD не проверялись;DELETE_ACK fault matrix отдельно не добавлялась.
12. Завершение аудита контрактов DM — 27.10.2026
База заключительного этапа: /home/user/utun4, master, исходный HEAD07f63c3047b4a9bef6c568ca590d3fefde95816f.
Полностью сверены все семь заголовков src/dm/ с реализациями core, crypto, history,
mailbox, mailbox_media и media; doc/dm_arch.md, doc/pm.txt и doc/pm_invites.md.
Это завершение проверки описаний/API, не утверждение об отсутствии всех возможных
ошибок программы. Новых подтверждённых дефектов выполнения в этом этапе нет.
В заголовках уточнены:
- Заимствование аргументов, один освобождаемый блок запросов UI, жизнь instance,
поток uasync и запрет доступа workers к SQLite/сети.
- Проверка тела/подписи отдельно от области router/ролей, точные повторы и конфликты,
границы транзакций при send/accept, подпись квитанции после commit входящего сообщения.
- Независимые квитанции сообщения и файла; STORED не удаляет исходящий outbox.
- Форматы DM/mailbox/media control, native-endian чисел и big-endian attachment_info,
лимиты send data с descriptor и размеры выходных буферов квитанций.
- Поколения/hidden, сохранение ключей и seq, отпечатки удалённых тел, отмена старых
invite/transfer и отбрасывание завершения worker старого поколения.
- Роли S/M, выбор нового storage без прежних назначений, фиксация назначения до STORE,
local S/M control и отличие передачи пакета от durable приёма.
- File holds, манифест путей, busy=-1 уборщика, повтор удаления и fsync каталога;
пользовательский исходник не передаётся уборке без явного remove_source.
- Запуск mailbox/core/media и остановка quiesce -> media_async drain -> destroy
с живыми SQLite/группами, сохранение данных между stop/restart.
dm_arch согласован с порциями таймеров32/8, обычным retry5с и отдельными retry3600с/30с,
сохранённым retry_at текстовых очередей и сбросом медиа при restart. Добавлены wire
таблица, порядок владельцев и восстановление оригинала после EXPIRED: срок копии M
не запрещает B получить исходный файл A; поздний EXPIRED не портит готовый файл.
pm.txt дополнен привязкой требований к текущим owners/ролям/настройкам, dm_arch
больше не называет этот аудит частичным. Исторические результаты сборок помечены датой.
Сверены относящиеся к контрактам callers: chat_service_start/stop, attachment_send,
file_transfer lookup/cancel, Qt DmInviteDialog/MainWindow/MessageList и bridge,
Android JNI/ViewModel/PmInviteDialog, headless DM handlers, регистрация JOIN-ключей
и планирование member_sync после JOIN_READY/принятия участника.
Клиентский код просмотрен для описанных DM-событий/выбора адресата/подготовки файла;
полный аудит Qt/Android и остальных общих модулей этим не заявляется.
pm_invites описывает владение запросом, поколение до регистрации и отсутствие
отката открытой беседы при последующей ошибке; отмена PM не отзывает JOIN-ключ.
Проверки текущего этапа:
- make -C src -j4 и сборка всех девяти существующих DM-целей: PASS.
- Native test_dm/history/names/load/e2e/invite/media/worker/media_cleanup:9/9 PASS.
test_dm_load:79 проверок; names:68 случаев и273 снимка, независимый Python JSON
parser прочитал каждый полный снимок. Worker:25 запусков,50 heartbeat,max_gap11.3мс.
Media:phase12,6266 heartbeat,allocations0->0; проверены11 retirement SQL faults,
18 release faults, потерянные control/restart и возврат точного оригинала после TTL.
- Linux cleanup component:13 SQL faults для scan/ACK/body, повтор после restart,
несколько M, потерянный/повторный/чужой ACK и поздний STORED; exit0.
Сеть перехвачена, receipt задан фикстурой; криптографию и физическое удаление M
проверяет отдельный test_dm_media. Одношаговый autocommit может уже завершиться
до искусственного finalize failure; тест проверяет это состояние и идемпотентный повтор.
- Каждый из семи заголовков: C99 и C++20 с -Wall -Wextra -Werror,14/14 PASS.
Для private/media C API C++ использует extern "C", как существующие клиенты;
include path lib/ соответствует проекту и предоставляет sqlite3.h.
- Все C-файлы DM побайтно совпадают с начальным снимком; C-токены всех заголовков
совпадают после исключения комментариев/пробелов. Алгоритмы, API/ABI, wire и SQL
этим этапом не менялись. git diff --check PASS; новые тесты не добавлялись.
Логи и снимки: /tmp/dm-contract-finish-before/sha256.json,
/tmp/dm-contract-finish-{build,final-core-build,final-tests-build,headers,validation}.log,
/tmp/dm-contract-finish-native-{results,test_dm*}.log; исходные тела/JSON — в test artifacts.
Сетевой профиль выполнен вне sandbox. Первый sandbox-запуск cleanup вернул exit0,
но uasync сообщил Operation not permitted при создании служебного fd; окончательный
повтор вне sandbox прошёл без этих сообщений. Сетевые настройки хоста не менялись.
Новые full check.sh, ASAN/UBSan/LSan, сборки/интерактив Qt/Android/Windows/FreeBSD
не выполнялись; прежние результаты не приписываются заключительному прогону.
Изменения radio и doc/tasks.md сохранены отдельно от DM-коммитов; push не выполнялся.
13. Rebase перед push — 27.10.2026
После отказа push (origin содержал новые коммиты) пользователь запросил rebase.
Получен origin/master492f6adc; все шесть DM-коммитов перенесены на эту базу.
Единственный конфликт — dm_messages_req в dm_core.h: сохранены generation из origin
и проверенные комментарии. Остальные пять патчей перенесены без изменения по range-diff.
Старые хеши и результаты раздела12 относятся к состоянию до переноса.
В origin появился generation в запросе истории и CHAT_EVT_DM_MESSAGES:
[conv_len:1][conv][generation:8][JSON]. Сверены полный trampoline, проверка current,
новая регрессия test_dm_history и получение снимка Qt/Android. dm_arch уточнён под
этот формат. Все C-файлы DM побайтно совпадают с новым origin; токены семи заголовков
совпадают после исключения комментариев/пробелов. C99/C++20:14/14 PASS.
Перед rebase сохранены21 незакоммиченных radio-файл, включая untracked, в stash
и отдельной резервной копии с SHA256/режимами. После переноса восстановлены все21:
18 файлов побайтно, doc/tasks.md/tests/Makefile.am/tools/chatgui/CMakeLists.txt
слиты с origin; списки добавленных/удалённых строк radio точно совпали с сохранёнными.
Повторная проверка после сборки также PASS; radio не включается в DM-коммиты.
Свежий ./build.sh --no-gui -j4:PASS. После пересборки native DM9/9 PASS,
включая скрытые/устаревшие запросы истории и точное поколение опубликованного снимка.
Дополнительно radio/radio_mesh/radio_headless/radio_headless_queue4/4 PASS.
Это профиль13 бинарников, не полный check.sh; новые GUI/Android/sanitizer прогоны
не выполнялись. Радио EOS C++-профиль не повторялся, его исходники сохранены побайтно.
Логи:/tmp/utun4-dm-rebase-{build,results,radio-preservation,test_*}.log.
Резервная копия указана в /tmp/utun4-dm-rebase-backup-path.txt.
+190 -5
View File
@@ -14,11 +14,33 @@ Android показывает зашифрованное вложение отд
Список чатов сохраняет общее название зашифрованного вложения вместо manifest.
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
подготовку. Ключ, UUID, автор, пути и метаданные записываются в локальную SQLite
собственной короткой транзакцией до передачи подготовленного файла worker.
Проверяются prepare, все bind, step, ровно одна добавленная строка, finalize и commit.
Внешняя транзакция отклоняется без изменений. До принятия ошибка возвращает `-1`:
файл остаётся у вызывающего, worker и событие отправки не создаются.
После commit возвращается `0`, файл принадлежит задаче и удаляется также при её отказе;
ненулевой request получает один `MSG_SUBMITTED`, в том числе синхронно при отказе запуска worker.
Исходный пользовательский файл сохраняется: общий frontend передаёт сюда собственную подготовленную копию.
Неудачный rollback с оставшейся открытой транзакцией отключает модуль вложений и
запрашивает остановку ядра. Desktop и Android учитывают запрос в своих poll-циклах;
владелец освобождает ядро и закрывает SQLite после возврата из poll. Текущий poll может
доставить другие callbacks до выхода. Политика общей БД остаётся WAL/synchronous=NORMAL:
завершённый commit до worker не является гарантией сохранности после отключения питания.
История дефекта и проверок — в [chat_audit_findings.txt](chat_audit_findings.txt).
Готовый контейнер передаётся через обычные media_index/media_delivery. UUID транспортного медиа
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
Связь авторского ключа с `message_id` и SHA256 контейнера фиксируется собственной
транзакцией до публикации сообщения. Проверяются все SQL-этапы и одна строка;
точный повтор допустим, замена сообщения или hash запрещена. Внешняя транзакция
остаётся нетронутой, SQL-отказ сохраняет прежнюю пару. UUID-префикс принимает только
канонический base64url с правильными padding/завершающими битами и lowercase hex;
при отказе выходной UUID не изменяется.
## Подписанное сообщение и контейнер
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
@@ -68,6 +90,18 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
какие артефакты задача действительно создала или опубликовала.
Перед расшифровкой собственная SQL-транзакция фиксирует `decrypting=1` ровно для
одного подходящего файла с выбранным ключом. Worker запускается только после commit.
Завершение также владеет транзакцией: отличает удалённую строку от ошибки чтения,
проверяет все bind/step/changes/finalize и сохраняет метаданные/ready либо ошибку.
До commit оно остаётся в списке задач instance; SQL-отказ повторяется таймером
с готовым результатом, без повторного worker. Внешняя транзакция остаётся нетронутой.
Лимит16 включает workers и такие завершения; отказ новой отправки сохраняет source
у вызывающего. Событие/публикация сообщения следуют после commit результата.
Quiesce останавливает повторы; после завершения workers destroy освобождает остаток
списка и принадлежащие ему результаты. При restart незавершённая расшифровка может
выполниться заново по сохранённому ключу и контейнеру.
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
@@ -90,11 +124,61 @@ instance того же процесса остаются защищены общ
Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном
открытии удалённая открытая копия восстанавливается с сохранённым ключом.
Производная запись контейнера создаётся из проверенной истории собственной
транзакцией: новая строка проверяется целиком, точный дубликат сохраняет ключ,
метаданные и флаги. SQL/OOM-отказ (`-1`) допускает повтор; неверный manifest или
коллизия (`-2`) его не запускают. Ошибка этого индекса не отменяет событие уже
сохранённого сообщения. Общая очередь backfill удерживает до16 каналов, через2с
повторяет чтение истории; переполнение ставит полный scan. Обрабатывается отделённый
batch, поэтому SQL-отказ во время прохода может безопасно поставить следующий.
Startup и ручная загрузка также восстанавливают индекс, включая сохранённый ciphertext.
Секретный ключ из истории не восстанавливается.
`wanted` и сброс `ready/error` при ручной загрузке фиксируются вместе. Состояние
`downloaded` и автоматический запрос обычного участника также сохраняются отдельной
проверенной транзакцией, событие следует после commit; неизменный повтор молчит.
Новая проверенная передача сбрасывает прежний error для повторной расшифровки,
простое обнаружение старого cache сохраняет error. Внешняя транзакция запрещена.
JSON истории вместе с manifest ограничен `DB_SYNC_MESSAGE_MAX` (4000байт), как
для остальных сообщений. Ручной обработчик использует тот же предел; отдельного
достижимого усечения на4096байтах не было.
При локальном удалении канала SQLite-trigger удаляет ключи, запросы и outbox в той
же транзакции, что `channels`; незавершённый каскад, включая `RAISE(IGNORE)`, отменяет
удаление. Также проверяется отсутствие канала и Merkle-хешей после DELETE.
SQL-ошибка сохраняет все записи и файлы; сетевые ресурсы к этому моменту уже могут
быть остановлены, поэтому требуется повтор команды. Внешняя транзакция отклоняется
до отмены ресурсов. Открытая после отказа rollback транзакция требует stop владельцем.
Только после commit удаляются ciphertext через media_index и собственные канонические
`decrypted/<channel>/<UUID>.file`. Посторонние имена, подкаталоги и temp этот проход
сохраняет. I/O-ошибка не возвращает удалённый канал: она логируется, очистку можно
повторить. Завершение активного worker отдельно удалит свой результат, увидев отсутствие строки.
После полной проверенной загрузки обычный клиент автоматически запрашивает
ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
Сетевая классификация `peers.storage` для этого не используется.
Таймер создаёт исходящий REQUEST собственной BEGIN IMMEDIATE/COMMIT: повторно
проверяет, что файл скачан и нужен, ключ отсутствует и прежнего запроса нет;
затем сохраняет ровно один исходящий запрос и команду outbox. Все SQL-этапы,
число строк и commit проверяются. Уведомление и отправка следуют после commit.
Отказ сохраняет прежние строки, следующий проход повторяет подготовку.
При внешней транзакции весь проход файлов и отправки откладывается, не меняя её.
Если rollback не закрыл собственную транзакцию, таймер не запускается повторно:
модуль снимается и владелец останавливает ядро после poll.
Перед каждой сетевой попыткой очередь собственной транзакцией проверяет команду и
фиксирует `retry_at=now+5с`. Только после commit выполняются проверка допуска и сеть;
SQL-отказ сохраняет команду/дату и не отправляет пакет. За проход обрабатываются до16
команд, периодический tick —10с. Пустая очередь освобождает свою транзакцию без
вызова commit hook. Отсутствие маршрута, backpressure и отказ отправки сохраняют очередь.
Очистка неподтверждённых подготовок выполняется до workers и после их завершения.
Если SQLite-транзакция открыта, весь recovery откладывается, файлы сохраняются:
незакоммиченное удаление истории не доказывает отсутствие публикации. Инициализация
модуля также отклоняет внешнюю транзакцию до изменения таблиц/таймера/сети.
В шапке desktop-клиента кнопка «Запросы расшифровки» видна при наличии запросов без решения
и показывает их число.
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
@@ -121,9 +205,43 @@ REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5
ответ только от автора, по своему существующему запросу и точной идентичности
контейнера. Конфликтующие ключи и результаты отклоняются.
REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются
в outbox только после сохранения решения. Полученный ключ сохраняется до
RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не
Приём REQUEST использует собственную BEGIN IMMEDIATE/COMMIT. Внешняя транзакция
отклоняется без изменений до чтения идентичности/прав. Под блокировкой проверяются
UUID/hash контейнера, допуск участника и авторство, затем читается существующий запрос.
Отсутствующий запрос вставляется со state=0 и проверкой одной строки; дубликат сохраняет
решение и created_at. Только incoming=1 и state=0/2/3/4 допустимы:0 ожидает автора,
2/4 повторяют GRANT в outbox,3 повторяет DENY. Все SQL-этапы проверяются; ACK/событие
отправляются после commit. При отказе прежние запрос/очередь сохраняются, ACK не отправляется.
Если после commit ACK не доставлен, клиент повторяет REQUEST; сохранённый запрос
позволяет повторно подтвердить приём и прежнее решение. Неудачный rollback с открытой
транзакцией требует quiesce/stop и закрытия ядра владельцем после poll.
История дефекта и исправления — [chat_audit_findings.txt](chat_audit_findings.txt).
GRANT/DENY появляются
в outbox в одной собственной BEGIN IMMEDIATE/COMMIT с решением автора. Проверка прав
также выполняется внутри неё. Изменяется ровно один входящий запрос в state=0;
уже принятое решение повторной командой не заменяется. Общий помощник записи outbox
проверяет prepare, все bind, step, ровно одну строку и finalize, транзакцией управляет вызывающий.
Внешняя транзакция отклоняется до проверки прав и остаётся нетронутой. SQL-отказ,
включая пропуск записи через RAISE(IGNORE), сохраняет прежние состояние и очередь.
Возврат0 и единственное уведомление UI следуют после проверенного commit; доставка
выполняется позже. Trampoline при ошибке отправляет событие с success=0, позволяя повторить команду.
Неудачный rollback с открытой транзакцией отключает модуль и запрашивает stop ядра после poll.
История дефекта и исправления — в [chat_audit_findings.txt](chat_audit_findings.txt).
Ответы REQUEST_ACK/GRANT/DENY/RESULT_ACK
применяются собственной короткой транзакцией: чтение запроса и очереди, запись ключа
при GRANT, изменение состояния и удаление ожидаемой команды. Все prepare/bind/step,
числа строк, finalize и commit проверяются. Удаляемая команда может уже отсутствовать
после REQUEST_ACK либо предыдущего результата; если она существовала, пропуск DELETE
считается ошибкой. Внешняя транзакция отклоняется без изменений.
RESULT_ACK для GRANT/DENY и уведомление интерфейса отправляются после commit.
При SQL-отказе прежние ключ/состояние/outbox сохраняются, подтверждение не отправляется,
автор повторяет выдачу. Если rollback оставил открытую транзакцию, модуль отключается
и запрашивает остановку ядра владельцем после poll. Найденный ранее bind-дефект и
его исправление описаны в [chat_audit_findings.txt](chat_audit_findings.txt).
Повторы возвращают прежнее решение и не
создают новых строк. Таймер обрабатывает ограниченную порцию очереди с
backpressure и повтором через 5 секунд; restart продолжает очереди.
@@ -146,9 +264,76 @@ backpressure и повтором через 5 секунд; restart продол
`test_attachment_send` проверяет восемь отказов MIME до ключей/worker/файлов,
затем останавливает сервис после настоящего crypto rename до completion.
`test_chat_attachment` проверяет 13 SQL-отказов с сохранением source/прежних строк/файлов,
отсутствием worker и события до принятия, затем настоящую публикацию при каждом повторе.
Отдельное readonly-соединение проверяет точные ключ, автора, UUID, метаданные и пути до worker.
Дополнительно проверяются настоящий commit veto, INSERT ABORT/IGNORE, внешняя транзакция,
синхронный отказ запуска, restart с опубликованным ключом и отказ rollback с закрытием БД владельцем.
Дополнительно:9 SQL-отказов захвата расшифровки,15 записи успешного результата и8
записи ошибки настоящего worker. Проверяются ABORT/IGNORE, commit veto, внешняя
транзакция, точные ключ/метаданные/флаги через reader перед worker и в событии,
один worker при повторе completion, отмена ожидающего результата при delete/stop,
restart сервиса и rollback fault без повторного таймера. 16 готовых авторских задач
удерживаются при отказе чтения; семнадцатая сохраняет source, снятие отказа публикует
все16 ровно по одному разу. Баланс выделений восстанавливается.
Удаление проверяется14 отказами: ABORT/IGNORE каналов/трёх локальных таблиц/Merkle,
prepare/bind/step/finalize удаления метаданных; дополнительно настоящий commit veto,
внешняя транзакция и rollback fault с остановкой владельцем. На отказе reader видит
прежние точные ключ/метаданные/request/state/created/outbox/retry, файлы сохранены;
в единственном CHANNEL_DELETED — канал/история/локальные записи/Merkle уже отсутствуют.
Дополнительные проверки:13 SQL-отказов привязки авторской пары,25 наблюдения новой/существующей
идентичности и60 операций wanted/downloaded. Сверяются все поля через независимый reader,
в том числе в событии; старые ключи/метаданные/соседняя запись сохраняются.
Открытое удаление истории при shutdown сохраняет опубликованные файлы; rollback и
restart восстанавливают прежний снимок. Внешняя транзакция также запрещает повторный init.
Проверяются канонический UUID,коллизия/внешняя транзакция/commit veto/ABORT/IGNORE,
повторная ошибка в отделённом batch,восстановление при startup без сети и17 каналов
с fallback после заполнения очереди16. Ручной обработчик передаёт все23 подписанных
блока manifest длиной3845байт в порт загрузки; отказ порта не удерживает контекст.
`test_attachment_requests` проверяет247 SQL-отказов/повторов новой записи и дубликатов
0/2/3/4, с пустой/существующей очередью. Подписанный мембер и сообщение создаются
штатными API, локальная запись вложения — фикстурой. Readonly reader в точке ACK
и события проверяет committed key/error/request/state/date/command/retry и соседний ключ.
Дополнительно: ABORT/IGNORE при INSERT запроса и INSERT/UPDATE outbox, настоящий commit veto,
внешняя транзакция, неверные состояние/направление/идентичность/флаги/мембер,
потеря ACK из-за маршрута/backpressure/send failure и повтор с прежней датой,
отказ rollback с остановкой и закрытием БД владельцем.
Тот же тест проверяет52 SQL-отказа создания исходящего REQUEST с пустой/существующей
outbox, ABORT/IGNORE, commit veto, внешнюю транзакцию до создания и до отправки,
отсутствие повторного события/запроса, rollback fault без повторного таймера.
Readonly reader видит обе строки до события и перед REQUEST; при отсутствии маршрута,
backpressure и ошибке send настоящий restart сохраняет очередь и исходную дату запроса.
`test_attachment_decisions` проверяет116 SQL-отказов/повторов выдачи и отказа, с пустой
и существующей очередью: чтение подписанного мембера, допуск автора, запись состояния,
outbox, BEGIN/COMMIT. Участник имеет настоящие join/invitation подписи, сообщение
сохранено штатным chat_core; запись локального вложения создаётся фикстурой.
Readonly-соединение в событии проверяет committed ключ/состояние/команду/retry_at/created_at
и сохранность соседнего ключа. Дополнительно: настоящий commit veto, ABORT/IGNORE для
UPDATE состояния и INSERT/UPDATE очереди, внешняя транзакция, удалённый мембер,
неверное направление и повторное решение, один GUI event, остановка при отказе rollback.
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
восстановление удалённого plaintext без нового согласия и удаление канала.
На Linux первый настоящий REQUEST от B обрабатывается внутри внешней транзакции:
проверяются её сохранность, отсутствие ACK/события/авторской записи и сохранение очереди B.
После rollback владельцем и restart автора настоящий повтор REQUEST принимается;
readonly reader видит сохранённый pending-запрос до отправки реального REQUEST_ACK.
Первый bind hash при создании индекса на B возвращает NOMEM. В событии сообщения
reader видит committed историю и отсутствие индекса,очередь восстановления уже создана;
перед настоящим REQUEST reader подтверждает восстановленные UUID/hash/downloaded/wanted.
На Linux пропуск GRANT outbox и внешняя транзакция автора отвергаются без изменения
ожидающего запроса и без события. После restart автора повтор решения сохраняет
state+outbox; отдельное readonly-соединение подтверждает их в момент единственного события UI.
На Linux первый bind полученного GRANT намеренно возвращает SQLITE_NOMEM: проверяются
полный откат и отсутствие ACK, сохранение GRANT у автора, затем реальная повторная выдача
и расшифровка. Перед каждым RESULT_ACK получателя отдельное readonly-соединение видит ключ.
`test_attachment_results` отдельно проверяет 105 SQL-отказов/повторов четырёх ответов,
commit veto, ABORT/IGNORE для key/state/outbox, внешнюю транзакцию, backpressure,
дубликаты, конфликт ключей/решений и остановку при отказе rollback. В точке ACK/события
readonly-соединение проверяет committed ключ/состояние/очередь и сохранность соседней записи.
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
+21
View File
@@ -3,6 +3,20 @@ ETCP: независимая обратная связь каналов и до
Статус
24.10.2026, объединение с актуальным master: удаление чужого member_files temp
уже исправлено0f42f92f, ранняя отмена —20b4975d; повторный фикс не требуется.
Описание ниже185/186 относится к прежней базе a198cfb9, до этих исправлений.
Актуальный аудит и проверки: doc/member_files_audit_findings.txt.
Небольшой недобор до условных85% — диагностическая метрика; он сам по себе
не оправдывает усложнения BBR. Дальше приоритет STCP backpressure, смена RTT/BW
нескольких путей и повторный отказ с проверкой доставки, пауз и роста очередей.
Объединённая версия897cc007: ./build.sh --no-gui -j4 PASS; полный check.sh186/186,
proxy9/9, burst1/1, load8/8 PASS;134217728 проверенных байт,162с проверки.
test_member_files_network PASS phase21: EXISTS/max_bytes/ранний cancel сохраняют
прежнее назначение и чужой temp, во всех трёх случаях target=1, foreign_temp=1.
Логи: /tmp/etcp-master-integration-build.log, /tmp/etcp-master-integration-check.log;
подробные результаты: /tmp/utun-etcp-solo-20261024/tests/logs/.
24.10.2026, продолжение после push: отдельно исследован одиночный дуплекс2Мбит/с.
Все прежние15 локальных коммитов вошли в origin/master после rebase; патчи сохранены.
Из-за параллельных изменений основного каталога продолжение изолировано в ветке
@@ -105,6 +119,13 @@ DATA EWMA остался25.353мс, RTO34.135мс, хотя timestamp-RTT пок
Новых очередей/таймеров/полей нет. Ранняя уже назначенная проверка применяет текущий RTO.
Контрольный временный бинарник восстановил DATA-измерения примерно за200мс и дал5 переходных повторов.
29.10.2026: BLE обнаружил отдельную ошибку верхней границы: при измеренном KA RTT2..6с
RTO оставался1с и удалял физическую попытку до ACK. Потолок1с снят, формула и минимум10мс
сохранены; без RTT начальное значение1с. uint64-расчёт ограничивается UINT32_MAX с WARN
при насыщении. test_etcp_windows проверяет RTT2с/ACK1.5с без повтора и с обновлением BBR,
таймер7.8672с, обе оценки RTT, jitter и предельные значения.63 сценария PASS.
Очередь BLE ниже локального completion этим изменением не ограничивается; см.doc/ble_etcp.txt.
Пересчёт прежнего таймера attempts вызывается также при изменении timestamp-RTT,
включая сокращение без новых DATA/ACK. Точная регрессия123->27мс до этого оставляла
старый срок, опаздывающий на96мс. Теперь срок сокращается сразу; раннюю проверку при росте RTO можно сохранить.
+23 -2
View File
@@ -18,6 +18,18 @@ SHA256 считается по картинке; при одинаковой в
Публикация одинаковой картинки не создаёт новую версию. Удаление — подписанная
запись с size=0 и новой версией; подпись/версия сохраняются, старый пир не может
восстановить удалённую картинку. Чтение проверяет сохранённый хеш и подпись.
Отказ SQL/проверки оставляет выход group_avatar_get неизменным. Приватный ключ
читается целиком:ровно32 байта BLOB,с проверкой bind/step/finalize;временные копии
очищаются на всех выходах. Сломанный/отсутствующий ключ не даёт права публикации.
set/apply владеют BEGIN IMMEDIATE:допуск,выбор/сравнение версии и UPDATE выполняются
под одной блокировкой. Все bind/step/changes=1/finalize и COMMIT проверяются.
Чужая транзакция отклоняется до действия,включая no-op;без изменения выполняется
ROLLBACK без commit hook. Ошибка сохраняет запись и не отправляет CHANNEL_UPDATED/
STATE. Если сам rollback оставляет транзакцию открытой,запрашивается stop;
владелец завершает остановку/закрытие БД вне callback. Миграция схемы использует
savepoint и допускает внешнюю транзакцию:ошибка ROLLBACK TO не сопровождается
RELEASE частичных ALTER;откат/закрытие остаются владельцу.
Транспорт
---------
@@ -38,8 +50,10 @@ DATA header=112 байт. Картинка разбивается на блок
Порядок блоков гарантирует router. Сборка проверяет offset, постоянные version,
total_size/hash/signature и допустимые размеры. До полной сборки прежняя картинка
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним UPDATE, затем
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним committed UPDATE, затем
CHANNEL_UPDATED доставляет изменение GUI и STATE распространяется соседям.
Приём завершает работу со сборкой/peer и только затем уведомляет интерфейс:
обработчик вправе отменить задачи группы,не оставляя обращения к освобождённой сборке.
Промежуточные блоки не публикуются, ACK/retransmit каждого блока — только router.
Отправка заполняет router до нижнего порога send_q count<=0 и ждёт send_ready;
@@ -59,7 +73,8 @@ ChatGUI
Настройки группы: Upload group avatar/Remove group avatar, предпросмотр и статус.
Кнопки доступны только при локальном приватном ключе. Применение сразу, отдельно
от Save локальных настроек. Worker обрезает центральный квадрат, создаёт JPEG
128×128, снижает качество/размер до лимита 16 КиБ. Вход ограничен 16 мегапикселями.
128×128,при необходимости96×96/64×64,снижает JPEG quality85/70/50/35 до лимита16КиБ.
Вход ограничен16*1024*1024 пикселями;SVG отклоняется.
GUI отправляет байты в uasync; ядро повторно проверяет размер/членство/ключ.
SQLite — постоянный кеш. GroupAvatarManager хранит QImage до 32 МиБ, ключ кеша:
@@ -76,6 +91,12 @@ test_group_avatar: настоящие два ядра/UDP/router, 16 КиБ че
перезапуск канала, stale/conflict, редактор с ключом вне creator UID, подписанное
удаление, повторное описание группы и restart БД. Schema failure откатывает
частичные ALTER, сохраняет прежние данные/ключи, повторное добавление идемпотентно.
Приём также проверен с отменой peer-задач прямо из CHANNEL_UPDATED.
test_group_avatar_sql:27 SQL-отказов публикации и16 чтения аватарки/ключей,
9 отказов схемы;каждый с успешным повтором. Проверены bind подписи/хеша,signing,
ABORT/IGNORE/commit veto,чужая транзакция/no-op,предельная версия,неизменность
выходов/readonly-записи,ошибка rollback и закрытие владельцем. Ключи NULL/31/33/
TEXT отклоняются;неудачный ROLLBACK TO не фиксирует частичную схему.
test_group_avatars (Qt): нормализация, лимит/отображение, кеш, права кнопок и отказ
ядра без ключа, удаление. chat=debug показывает состояния и каждый 4КиБ блок;
chat=info — публикации и жизненный цикл. Ошибки содержат group/peer/version/offset.
+69
View File
@@ -0,0 +1,69 @@
Аудит документации Local_transport — 26.10.2026
Объём и статус
Полностью прочитаны src/local_transport/local_transport.c и platform.c,
сверены все объявления и комментарии трёх заголовков local_transport.h,
platform.h и protocol.h. Все три заголовка готовы к отметке [V] в чек-листе.
Для проверки интеграции прочитан chat_local.c; прочитаны три профильных теста
и local_transport_ipv6.sh. Полный аудит src/chat этой записью не заявляется.
LAN-раздел doc/node_snapshot.md уточнён; общая отметка документа остаётся [~].
Исправленные описания
1. Доставка и дедупликация. Убрано безусловное обещание отсутствия дублей:
кеш хранит последние 128 успешно принятых (peer, epoch, request) до 30с,
без продления повтором. Отказы received не кешируются. Request уникален
среди pending, а повторное использование ID также ограничено кешем получателя.
FAILED и отсутствие ACK не доказывают, что приложение не получило данные.
Описаны 8 pending, немедленная отправка и до двух повторов по секундному tick.
2. Активность и callbacks. Уточнены синхронные уведомления из API, порядок
activity_changed перед открытием/закрытием сокетов, завершение pending
через FAILED и сохранение эпохи/дедупликации при паузе того же handle.
Отказы открытия сокетов не означают отказ set_active: повтор при сверке;
невозможность создать tick-таймер выключает транспорт. peers() очищает TTL
и может вызвать routes_changed; route() callbacks не вызывает.
3. Broadcast и TTL. Изменение ника рассылается только при active+broadcasts;
тот же ник не вызывает отправку. Отключение broadcasts сохраняет unicast,
приём, сверку интерфейсов и TTL. Только обычный ANNOUNCE продлевает endpoint.
Жизнь кандидата дополнительно ограничена его собственным TTL, интерфейсом
и discovery-endpoint. Offline-gate применяется к подписанному входящему
обмену, но не повторяется при route() и не нужен для ответа на CHALLENGE.
4. Wire. Описаны все смещения 60-байтного заголовка, NUL в 16-байтном magic,
длина тела, BE, типы сообщений, неподписанные MESSAGE/ACK и ограничения
discovery-порта. Уточнены подпись/nonce, scope, допустимые protocol,
уникальные socket_id, ненулевые port/generation и пустой PROOF.
5. Платформа. Зафиксированы различия отбора POSIX/Windows, дедупликация
адресов, усечение снимка, ответственность за ifindex/scope, допустимый
источник и результаты local_receive (включая пустой UDP/would-block/-2).
Добавлены предусловия встроенных BE/UTF-8 helpers и категория discovery.
Проверки
make -C src -j4 libutun.a и сборка трёх профильных тестов — PASS.
Логи /tmp/local-transport-audit-build.log и
/tmp/local-transport-audit-tests-build.log. Существующие предупреждения
db_sync/etcp_connections/dm_media не относятся к изменённым комментариям.
Пять сценариев PASS, последовательно вне sandbox из-за запрета сокетов:
- test_local_transport: UDP discovery, ACK retry/dedup, TTL, настройки,
остановка/повторная активация; /tmp/local-transport-audit-discovery.log.
- test_local_transport_packets: миллион некорректных пакетов, ограничения
flood/памяти/events и восстановление; /tmp/local-transport-audit-packets.log.
- test_local_routes: подписи, nonce/replay, BGP gate, поколения сокетов,
TTL, NCD UDP/STCP и сохранность snapshot; /tmp/local-transport-audit-routes.log.
- sh tests/local_transport_ipv6.sh: изолированная IPv6-only LAN;
/tmp/local-transport-audit-ipv6-discovery.log.
- sh tests/local_transport_ipv6.sh test_local_routes: IPv6 scope и LAN/NCD;
/tmp/local-transport-audit-ipv6-routes.log.
В заголовках изменены только комментарии; логика, ABI и wire не менялись.
Новых подтверждённых дефектов реализации в этом объёме не обнаружено.
Прежняя задача о нестабильном ACK в test_local_transport остаётся открытой:
успешный отдельный прогон не подтверждает устранение флака под общей нагрузкой.
Полный check.sh, санитайзеры, native Windows/FreeBSD и GUI/Android runtime
не запускались. [V] означает завершённую сверку документации по исходникам.
+36 -8
View File
@@ -25,7 +25,8 @@
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
enabled проверяется на каждом серверном запросе; клиент доступа к другим storage остаётся доступен.
Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
@@ -86,7 +87,7 @@ Wire, версия 4
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
компоненты с ведущей '.', завершающей точкой/пробелом, ASCII C0/DEL,
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
@@ -168,11 +169,19 @@ Windows удерживает родительские каталоги без SH
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
DIRECTORY_NOT_EMPTY.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ до истечения 120 секунд от начала обработки
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
мутация возвращает BUSY до истечения старых. В таком ответе cursor содержит
остаток до ближайшего освобождения квитанции в timebase 0.1мс (1..1200000).
Нулевой cursor означает BUSY другой причины с обычной задержкой 5 секунд.
Клиент COMMIT/MKDIR/RENAME/REMOVE сохраняет pending и прежний request_id/параметры,
ставит отменяемый таймер на этот срок и повторяет команду; промежуточный BUSY не завершает callback.
При потере ответа обычный повтор идёт через 5 секунд. Общий срок от создания
запроса остаётся 120 секунд и не продлевается даже при BUSY; pump не отправляет
команду после этого срока. Поэтому повторы остаются внутри серверного окна
квитанции, не требуя её досрочного вытеснения. Остальные команды возвращают BUSY caller.
Повторы после этого срока
не дают гарантии того же результата; перед новой мутацией требуется STAT.
Потоки, таймауты и остановка
@@ -221,9 +230,15 @@ continuations; только затем worker освобождает storage. О
Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает.
quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy
вне callbacks дожидается только своих работ/completions и закрывает дисковый
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
в потоке uasync вне callbacks самого модуля дожидается только своих работ/completions
и закрывает дисковый поток до уничтожения CHAT-групп. Внешняя команда stop допустима.
Drain ждёт завершения публикации completion под mutex worker и отменяет её до освобождения работы.
uasync_cancel_post удаляет и освобождает ожидающий task из общей очереди либо активного batch.
Drain доставляет done ровно один раз, включая cleanup continuations; отдельного обезвреживания
callback/arg не требуется. Контексты не освобождаются под работающим I/O.
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
Немедленная отмена после start снимает starting: поздний CM UP не запускает STAT/подготовку заново.
После перехода к финализации публикации повторный cancel не меняет уже выбранный результат.
Desktop
-------
@@ -262,7 +277,14 @@ Stop сервиса/группы завершает все принадлежа
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Два download с одной строкой local одновременно не допускаются; физические aliases
и подмену локальных путей исключает вызывающий. Upload читает стабильный regular source;
изменение снимка возвращает STALE. На POSIX O_NONBLOCK позволяет отвергнуть FIFO
до блокирующего open/read единственного дискового worker.
Download создаёт соседний temp эксклюзивно и получает право cleanup только после успеха open.
Отказ из-за существующего temp, max_bytes, STALE или ранняя отмена не удаляют чужой путь.
После проверки SHA256 выполняются sync/close и заменяющий rename; до публикации прежний local сохраняется.
POSIX-публикация локального download не делает fsync родительского каталога, в отличие от серверного COMMIT.
Аватарки пользователей
---------------------
@@ -350,12 +372,18 @@ tests/test_member_files: wire/UTF-8, произвольные блоки, пов
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
На POSIX отдельный процесс с границей 2с проверяет неблокирующее открытие FIFO
и последующий отказ проверки типа; fork выполняется до создания pthread.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
Проверены сохранность чужого temp/прежнего назначения при EXISTS, max_bytes и немедленном cancel,
поздний CM UP после отмены, 76 созданий и 76 удалений с исчерпанием квитанций.
Один успешный ответ отброшен перед клиентским handler: повтор с прежним ID возвращает квитанцию,
счётчики остаются точными (76 каталогов после создания, 0 после удаления).
test_services — stop/start общего чат-сервиса.
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
Настройка для полного трасса этих модулей:
+128
View File
@@ -0,0 +1,128 @@
Аудит C-модуля member_files — 2026-10-24
Статус: полностью прочитаны шесть реализаций и шесть заголовков src/member_files/.
Заголовки сверены; четыре подтверждённых дефекта исправлены и проверены.
Серверная/клиентская части doc/member_files.txt согласованы с кодом. Общий документ
остаётся [~]: его desktop/AvatarManager/Android-подсистемы целиком в этот аудит не входили.
Ниже результаты относятся к C-сервису, а не к полному аудиту всех файлов GUI/аватарок.
1. Удаление чужого локального temp
До исправления TRANSFER_FINISH безусловно удалял вычисленный путь temp при ошибке,
включая отказ эксклюзивного open из-за существующего файла и отказ до открытия.
Настоящий test_member_files_network: preexisting target/temp по37 проверенных байт,
скачивание с подписанного storage, MF_EXISTS; target=1, foreign_temp=0, FAIL phase23.
Корректное воспроизведение: /tmp/member-files-temp-repro5.log, строки391..395.
Исправление: один temp_owned у handle, устанавливается только после успешного
эксклюзивного создания. Finish закрывает fd, публикует/удаляет только свой temp.
После успешного rename владение именем temp снято; source/target не подменяются caller.
Новый FILE-слой или второй файловый владелец не добавлены: disk API работает с native fd.
Обычный и sanitizer-прогоны: EXISTS, max_bytes, немедленная отмена сохраняют
прежние37 байт target и чужого temp, finished ровно один раз.
2. Ранняя отмена возобновляла передачу и приводила к UAF
После cancel сразу вслед за start поздний CM UP вызывал wake. Starting оставался1,
поэтому pump после начала cleanup снова посылал STAT. Finish освобождал handle,
а ответ STAT вызывал transfer_response с уже освобождённым arg.
Лог: /tmp/member-files-temp-fixed.log; GDB: /tmp/member-files-cancel-gdb.log,
SIGSEGV в transfer_response → mf_pending_finish → mf_dispatch.
Переход stopping теперь снимает starting; wake без готового финального ответа
ничего не планирует. Обязательная cleanup-задача остаётся единственным продолжением,
затем отправляется финальный CANCEL/END и освобождается handle до callback.
Не добавлены новые ссылки/таймеры или зависимость от скорости ответа CM.
Регрессия early cancel прошла вместе с обычной отменой, параллельными потоками,
router restart и destroy при активном server I/O.
3. Заполнение64 квитанций обрывало массовые мутации
Прежнее воспроизведение в doc/tasks.md: из76 удалений выполнены64, затем MF_BUSY.
Серверный предел/окно нужны для идемпотентности и остаются: до64 успешных
COMMIT/MKDIR/RENAME/REMOVE, MF_REPLAY_TB=1200000 (120с) от начала обработки.
Квитанции не вытесняются досрочно. Полный кеш возвращает BUSY с cursor=остаток
до ближайшего освобождения в timebase0.1мс; это значение кодируется обычным LE-полем.
Нулевой/недопустимый hint использует обычную задержку5с; недопустимый логируется.
Клиент этих четырёх команд сохраняет тот же pending/ID/параметры и ожидает
отменяемый retry timer. Остальные BUSY возвращаются callback как раньше.
Общий срок запроса остаётся120с от создания; ни BUSY, ни потеря ответа его не продлевают.
Перед повторной отправкой pump также проверяет срок. Сервер начинает обработку
не раньше создания клиентского запроса, поэтому допустимые повторы не выходят
за окно первой успешной квитанции. Ошибка/timeout не обещают отката серверной мутации.
При постоянной конкуренции запрос может завершиться TIMEOUT; unbounded waiting нет.
Вариант с продлением срока клиента отклонён при архитектурной сверке до итогового
прогона: поздний BUSY и потеря результата могли вывести повтор за жизнь квитанции.
Один секундный tick также недостаточен для точного освобождения; введён отдельный
retry timer, который снимается до free/cancel/quiesce, с tick fallback при OOM.
Формат125-байтного заголовка не менялся; смысл mutation BUSY.cursor описан в protocol.h.
Новая сетевая регрессия проходит настоящий клиентский API/router/storage:
-76 MKDIR, включая исчерпание кеша; QUOTA подтверждает ровно76 каталогов.
-Первый успешный ответ намеренно отброшен до клиентского handler; повтор с прежним
ID возвращает успех, не создавая второй объект и не выдавая EXISTS.
-76 STAT/REMOVE, включая новое исчерпание квитанций; QUOTA подтверждает0 объектов/байт/резерва.
Qt recursiveDeletion(server-busy) вызывает storage напрямую через stub bridge:
его сырой MF_BUSY остаётся правильным серверным ответом. Поведение полного
клиентского ожидания и массового удаления проверено C-сетевой регрессией, не этим stub.
4. FIFO блокировал общий disk worker до проверки типа
POSIX mf_disk_local_open(source,0) использовал блокирующий open. FIFO без writer
зависал ещё до mf_disk_info, который должен отвергнуть не-regular источник.
Регрессия запускает отдельный процесс до создания pthread, alarm2с ограничивает
только диагностический child. До фикса он завершился SIGALRM (status14), failures=1:
/tmp/member-files-fifo-repro.log. Добавлен O_NONBLOCK, как у серверного open:
FIFO открывается без ожидания, проверка типа даёт INVALID_PATH, fd закрывается.
Обычный и sanitizer-прогоны: child status0, failures0; invalid mode/links имеют WARN.
Произвольный уже выполняющийся системный I/O/хеширование всё ещё не прерывается cancel.
Сверка архитектуры и описаний
-Uasync владеет запросами, streams, local FIFO, ACL, CM handles, waiter/pump/timers.
Единственный worker выполняет файловое I/O последовательно; work FIFO, порядок
done при синхронном drain не обещается. Данные живут до completion.
-Обычных работ до64; заранее зарезервированные cleanup jobs обходят лимит.
Quiesce запрещает producers; drain завершает I/O и cleanup, затем storage destroy
выполняется в worker, после него join/free, пока ядро/группы ещё живы.
-Storage принимает только уже проверенные членство/роли. Сам проверяет UID мутации
и токены temp. Квота общая для UID во всех группах; обычные операции не делают rescan.
-Server COMMIT и local download имеют разные гарантии: локальный POSIX rename
не синхронизирует каталог. Ошибка server sync после rename не откатывает публикацию.
-Пути disk API доверенные и уже проверены protocol/storage. Callback LIST получает
заимствованные name/info; порядок файловой системы, не сортировка. Source/target
aliases локальных download исключает caller; одинаковая строка local запрещена.
-Codec проверяет формат и размеры, не ACL и не UTF-8 policy пути. Ошибка decode
может изменить output; encode не проверяет семантику команды. Эти границы описаны.
-Все поля/функции шести .h получили соответствующие контракты и комментарии.
Проверки и границы
./build.sh --no-gui -j4 PASS: /tmp/member-files-audit-final-build.log.
Профиль7/7 PASS: member_files, member_files_network, services, encrypted_attachment,
dm_media, attachment_send, member_adapter. Результат: /tmp/member-files-audit-profile-results.log,
подробности: /tmp/member-files-audit-profile-logs/.
ASAN/UBSan/LSan4/4 PASS, detect_leaks=1, halt_on_error=1: member_files,
member_files_network, services, encrypted_attachment. Отдельная копия:
/home/user/utun4/.cache/member-files-asan-b87xwg24, все встроенные зависимости
инструментированы через CPPFLAGS; системные OpenSSL/FFmpeg не пересобирались.
Логи: /tmp/member-files-sanitizer-build.log, /tmp/member-files-sanitizer-tests-build.log,
/tmp/member-files-sanitizer-results.log, /tmp/member-files-sanitizer-test_*.log.
После этого прогона добавлены только уточнения описаний и WARN без изменения логики.
В sanitizer-сборке остаётся прежнее предупреждение snprintf в disk_parent_open:
strlen(component)<=MF_NAME_MAX проверяется до записи в leaf[MF_NAME_MAX+1];
воспроизведённого усечения здесь нет. Остальные предупреждения сборки относятся
к существующему коду других модулей/зависимостей, не скрывались фильтрами компилятора.
Первые диагностические прогоны до repro5 недействительны для temp: тест был
собран с новым utun_instance.h и старой libutun.a, в startup читались0xaaaaaaaa.
Общая сборка устранила несовпадение; ненужное изменение маршрутного условия отменено.
Один промежуточный старт остановился из-за полного /tmp; очищены только
воспроизводимые объекты предыдущей sanitizer-копии, исходники/журналы сохранены.
Новый полный make check, Windows/FreeBSD runtime и GUI/Android проверки не выполнялись.
Четыре подтверждённых дефекта закрыты; новых подтверждённых незакрытых в этом объёме нет.
+940
View File
@@ -0,0 +1,940 @@
member_sync / merkle_sync — аудит контрактов, 26.10.2026
Текущий статус: согласованный перенос истории на MESSAGES Merkle реализован и проверен (раздел13).
После rebase на b0bf27de:check.sh206/206 PASS,0 failed/0 skipped;
proxy9/9,burst1/1,load8/8,exit0,197с; STUN и stream PASS. Подробности — раздел15.
ASAN/UBSan/LSan10/10 PASS с detect_leaks=1. Крупная история:5002 записи на каждом узле,
полные корни одинаковы; изменение между CHECK/DONE проверено отдельно нативно и с sanitizers.
История исключённого автора сохраняется в MESSAGES; обратная политика отображения FEEDBACK отдельно.
Полный аудит остальных контрактов чата остаётся частичным; GUI/Android/Windows/FreeBSD не запускались.
Сверка документов этого блока закрыта: member_sync.h/_doc.md, merkle_sync.h/_doc.md,
db_sync.h/_arch.md и исторического merkle_tz.md отмечены [V] в общем чек-листе.
Предыдущие [~] в исторических разделах ниже описывают состояние соответствующего этапа.
Разделы1–12 сохраняют исходные обнаружения, планы и результаты прежних этапов;
их старые счётчики тестов не являются проверкой нового переноса.
1. Статус и объём
Базовый HEAD: 962a2df3. Рабочее дерево перед проверкой было чистым.
Полностью прочитаны member_sync.c (1262 строки), merkle_sync.c (687), merkle_tree.c (122),
их три заголовка, member_sync_doc.md, merkle_sync_doc.md и историческое merkle_tz.md.
Прочитаны test_member_adapter.c, test_merkle_protocol.c, test_member_sync.c;
для test_merkle_sync.c и test_chat_feedback.c проверены относящиеся к контрактам части.
Сверены связанные DB-функции, граница внешнего commit JOIN и интерфейс ll_queue.h.
Полный аудит chat_sync, chat_feedback, db_sync, дерева авторизации и всех callers не заявляется.
На первом этапе найден подтверждённый дефект очистки; по AGENTS.md:10 аудит был остановлен.
На том этапе реализация .c, протокол и штатные тесты не менялись; стенд находится в /tmp.
member_sync.h/_doc.md и merkle_sync.h/_doc.md остаются [~]; merkle_tree.h сверён [V].
merkle_tz.md остаётся [~]: исторический статус исправлен, db_sync отдельно не сверялся.
Остальные отметки общего чек-листа сохранены.
2. Подтверждённый дефект: очистка неатомарна и теряет ошибки
src/chat/member_sync.c:844–908, member_sync_verify_and_purge.
На строках 888 и 890 не проверяются результаты topo_node_sqlite_member_del и
merkle_sync_recompute_path. Между удалением строки и пересчётом индекса нет общего savepoint.
Затем безусловно очищается mute, вызываются props callbacks, обрабатывается удаление
прямого соединения из CHAT-группы и увеличивается purged.
Ошибки prepare/step исходной выборки также не имеют отдельного результата ошибки.
verify_and_purge_all возвращает void, а member_sync_start игнорирует результат очистки.
Диагностика использует настоящие member_sync, merkle_sync, merkle_tree и SQLite :memory:.
Локальный доверенный put создаёт синтетическую повреждённую запись node_id/X25519;
такая запись выбрана для проверки штатной ветви очистки, сетевые пакеты не используются.
Корень до очистки ненулевой. SQL-триггеры адресно моделируют отказ DELETE записи
либо DELETE строки производного индекса. Остальные SQL-запросы остаются рабочими.
Подписчик props фиксирует число реально оставшихся строк и autocommit в момент события.
Случай Возврат Строки после Старый корень Props
Без отказа 1 0 нет, нулевой 1 PASS
Отказ DELETE участника 1 1 да 1 FAIL
Отказ пересчёта индекса 1 0 да 1 FAIL
Во втором случае лог DB содержит rc=19/audit delete failure, но затем очистка пишет
«purged 1» и уведомляет подписчика с tags=NULL, хотя запись всё ещё существует.
В третьем случае лог Merkle показывает audit index failure, но строка уже удалена,
корень остаётся ненулевым и очистка снова пишет «purged 1».
Общее число tracked allocations после трёх случаев восстановлено; exit=1, 2/3 FAIL.
Удаление реального соединения, радио и полный сетевой результат этим стендом не проверялись.
Стенд: /tmp/member-merkle-audit-purge.c
Дополнительная цель штатного Makefile: /tmp/member-merkle-audit.mk
Сборка: make -C tests -f Makefile -f /tmp/member-merkle-audit.mk /tmp/member-merkle-audit-purge
Сборочный лог: /tmp/member-merkle-audit-purge-build.log
Полный диагностический лог: /tmp/member-merkle-audit-purge.log
3. Уточнённый план до реализации
Предыдущий вариант был неполон: не выбран контракт внешней транзакции и не прослежен
результат crypto-проверки до нижнего уровня. После перепроверки выбран следующий вариант.
А. Проверка данных до удаления
Весь путь join/update/owner/tree должен различать недействительную подпись и отказ
самой проверки. member_sync_verify_local_record:791–801 сейчас возвращает 0 также
при отказе EVP_PKEY_new_raw_public_key/EVP_MD_CTX_new/EVP_DigestVerifyInit.
sc_ed25519_verify в secure_channel.c тоже объединяет отказ VerifyInit и неверную подпись
в SC_ERR_AUTH_FAILED; одного нового результата в верхнем member_sync недостаточно.
Нужно сохранить SC_ERR_CRYPTO для allocation/init/internal failures и AUTH_FAILED
для отрицательного результата законченной проверки подписи; unknown/internal outcome
никогда не должен становиться основанием DELETE. Аналогично SQL_ERROR нельзя выдавать
за отсутствие родительского ключа. Отсутствие ожидаемой зависимости оставляет запись
до последующей проверки, операционный отказ прерывает текущую очистку.
Публичный результат локальной проверки может остаться простым: 1=сохранить,
0=доказанная порча, <0=ошибка; callers должны явно сравнивать с 0, а не использовать !result.
Формат записи, подписи, wire и смысл отсутствующих подтверждений не меняются.
Это статически установленные ветви; runtime crypto/OOM fault-injection ещё не выполнялся.
Б. Одна транзакция принадлежит очистке одного канала
verify_and_purge требует sqlite3_get_autocommit(db)==1; чужая транзакция отклоняется
до чтения и изменений. Рабочий вызов start идёт из cs_sync_timer_cb без DB-транзакции;
init также не требует вложенного вызова. Сложный механизм отложенной внешней публикации не нужен.
BEGIN IMMEDIATE выполняется до SELECT, чтение/проверка должны завершиться SQLITE_DONE.
Размер каждого используемого BLOB проверяется до чтения; список повреждённых node_id
принадлежит операции, не содержит заимствованных SQLite указателей. После отбора SELECT закрывается.
В той же транзакции удаляются найденные строки и пересчитываются затронутые листья/предки.
На этом этапе recompute_path не делает changed, поскольку autocommit выключен.
Все bind/step/finalize/COMMIT проверяются. На ошибке statements закрываются и выполняется ROLLBACK;
учитывается автоматический rollback SQLite. Не выполнять RELEASE/COMMIT после неудачного отката.
Если восстановить состояние транзакции не удалось, продолжать работу синхронизации с БД нельзя.
В. Публикация только после успешного COMMIT
Уведомить Merkle об изменённом namespace и выполнить именно существующие последствия удаления:
снять mute, удалить членство конкретной CHAT-группы через её API, уведомить props-потребителей.
Не закрывать общий ETCP/NCD транспорт, принадлежащий другим группам/сервисам.
Не заменять удаление member_sync_record_committed: тот предназначен для обновления
и пытается присоединить существующий UP-транспорт к группе.
Не держать SQLite cursor и заимствованные group/conn указатели через callbacks;
повторный вход и время жизни подписчиков проверяются отдельно.
Ошибка публикации после commit не означает откат уже сохранённой очистки.
Г. Ошибки и граница атомарности
verify_and_purge возвращает <0 при отказе до commit, иначе число подтверждённых удалений.
verify_and_purge_all возвращает ошибку и останавливается на первом неудачном канале;
уже успешно очищенные каналы остаются согласованными и не откатываются.
init освобождает созданные ресурсы при отказе; start не создаёт Merkle-запрос после отказа очистки.
Результат <0 из start не сопровождается done callback. Эти правила фиксируются в .h.
Д. Критерии проверки
Нормальная очистка; read/bind/delete/hash/commit/allocation/crypto failures; отказ на второй записи;
сохранность корректных записей, source placeholders и отложенных зависимостей;
неизменность данных/индекса и отсутствие props/mute/group-изменений при rollback;
освобождение statements/памяти/транзакции, отказ при чужой транзакции, init/start и повтор после отказа.
После commit проверить реальные stream/radio/group потребители и чужого владельца транспорта.
Затем профильные наборы, полный check.sh и согласованная sanitizer-сборка.
До реализации и этих тестов утверждать, что исправление доказанно корректно, было нельзя.
Семантика ошибок сверена с официальными документами:
https://docs.openssl.org/3.0/man3/EVP_DigestVerifyInit/#return-values
https://www.sqlite.org/lang_transaction.html
На момент перепроверки до реализации HEAD оставался 962a2df3; исправления .c ещё не было.
4. Исправленные описания
member_sync.h/_doc.md: область атомарности put/apply, предупреждение об очистке,
различие проверки наличия строки и допуска CHAT, точный verified_ed25519 без обещания
рекурсивного обхода предков, эпоха join_ts/update_ts, независимый owner ver,
дополнение сертификата без смены профиля, проверка подписи owner и для локального put.
Уточнены игнорируемый вход storage, COUNT(*) с placeholders/deleted, уведомления после
внешнего commit через record_committed (одного merkle_sync_changed недостаточно).
merkle_sync.h/_doc.md: необязательные четыре staged-pull callbacks, ненулевой контекст,
commit в конце своего прохода и abort после commit/restart/отказа, сброс общего индекса
при init, NULL done callback, REINIT, 30-секундный progress timeout и отсутствие WAL-таймера.
Полная пересверка дополнительных разделов feedback/опросов не завершена.
merkle_tree.h: владение общей таблицей, namespace dataset:channel, обязательные аргументы,
нормализованные prefix, возможное изменение выходов при ошибке, domain хеша,
транзакция caller, удаление пустых узлов и остановка на неизменившемся предке.
merkle_tz.md: прежний out_q/push/WAL описан как исторический этап, а не текущая реализация.
Требования и старые ссылки оставлены с явной пометкой; аудит db_sync здесь не заявлен.
5. Выполненные проверки
make -C src -j4 libutun.a — PASS: /tmp/member-merkle-audit-core-build.log.
Сборка четырёх штатных целей — PASS: /tmp/member-merkle-audit-tests-build.log.
test_member_adapter — PASS: /tmp/member-merkle-audit-adapter.log.
test_merkle_protocol — PASS: /tmp/member-merkle-audit-protocol.log.
test_chat_feedback — PASS: /tmp/member-merkle-audit-feedback.log.
test_merkle_sync — 28/28 PASS: /tmp/member-merkle-audit-sync-unrestricted.log.
В этих четырёх наборах нет проверки потерянных ошибок verify_and_purge.
Первый test_merkle_sync в sandbox остановился на создании UDP-сокетов (EPERM),
после чего повторён вне sandbox с результатом PASS. Первый лог:
/tmp/member-merkle-audit-sync.log. Это ограничение среды, не ошибка Merkle-протокола.
Лог успешного mock-набора содержит ошибки отсутствующих peers_1002/1003 от планировщика
тестовых CHAT-групп; совпадение mock-данных и 28 PASS не подтверждают работу admission.
В первом этапе полный check.sh, sanitizers, native Windows/FreeBSD, GUI и Android не запускались.
После тех документационных правок git diff --check и неизменность C-токенов трёх .h — PASS.
Проверка документов не равнозначна доказательству отсутствия дефектов реализации.
Коммит и push в первом этапе не выполнялись.
6. Согласованное исправление и окончательные проверки
По подтверждённому пользователем уточнённому плану исправлены member_sync.c/.h,
secure_channel.c/.h и SQL-getters каналов/Ed25519 в topo_node_sqlite.c/.h.
Изменений wire, подписываемых тел и схемы БД нет.
Одна очистка канала владеет BEGIN IMMEDIATE от SELECT до общего COMMIT удаления и индекса.
Чужая транзакция отклоняется до изменений. До любых мутаций собираются собственные node_id,
проверяются размеры BLOB/строк и SQLITE_DONE, закрывается cursor. Все отказы приводят к rollback
без props/mute/group-изменений. При автоматическом rollback SQLite лишний ROLLBACK не требуется.
Если откат не удался и транзакция осталась открытой, логируется требование restart, inst.running=0;
восстановление после реального постоянного I/O-отказа хранилища этим набором не проверялось.
После commit изменяется Merkle revision, снимается mute, вызывается API удаления
членства конкретной CHAT-группы и props. Общий ETCP/NCD другого владельца не закрывается.
Удаление не подменено record_committed, который обрабатывает обновление/подключение.
Props-подписка снимается с отложенным free при вложенном dispatch; callback может
снять себя и повторно вызвать очистку. Stop/destroy по-прежнему выполняются вне callbacks.
verify_and_purge_all больше не держит schema cursor через публикацию и возвращает ошибку;
атомарность по каналу, ранее завершённые каналы остаются согласованными.
init освобождает созданный Merkle-движок при отказе очистки, start не создаёт запрос и не вызывает done.
Верификация возвращает сохранить/доказанно повреждена/ошибка операции.
Отсутствующий ключ/сертификат/ожидаемая зависимость не вызывает удаления.
SQL-getters различают LOOKUP_MISSING=-1 и LOOKUP_ERROR=-2; прежние callers по !=0/<0 сохраняют смысл.
sc_ed25519_verify различает VerifyInit/internal failure и завершённую проверку неверной подписи.
SHA256 node_id проверяется на отказ; неопределённый hash не читается.
Эта классификация используется также в приёме записей, без расширения доверия к входящим данным.
test_member_adapter включает новый test_member_purge.c и linker-wrap faults настоящих SQLite/OpenSSL API.
Проверены11 отказов read/finalize/bind/begin/commit/allocate/delete/index/автоматического rollback,
отказ второй аллокации списка,24 crypto faults в join/update/owner/tree, отказ identity hash,
ошибки SQL-getters, сохранение placeholders и отложенного подписанта, внешняя транзакция,
init cleanup/retry, отсутствие запроса/callback при отказе start и успешное повторное подключение.
Настоящие неверные join/update/owner/tree и короткий ключ удаляются с пустым корнем.
Логи: /tmp/member-purge-fix-test_member_adapter.log и ...-asan-final-test_member_adapter.log.
В test_stream_service/test_stream_member добавлены настоящие DELETE/index faults:
rollback сохраняет reader/mute/READY и корень; успешный commit удаляет членство,
сохраняя другой канал и внешний NCD handle/UP-транспорт.
Лог отдельного повтора: /tmp/member-purge-fix-member-ownership.log.
Первая проверка tracked allocations в новом helper сработала до освобождения call_soon/пула uasync.
Контроль приведён к контракту владельца: immediate queue обрабатывается, число активных timers
восстанавливается, полный счётчик аллокаций проверяется после uasync_destroy штатным main.
Повторный набор и sanitizers проходят без утечек. Попытка использовать private topo_group_peer
в интеграционном тесте заменена публичным topo_group_peer_ready до окончательных прогонов.
Обычная сборка: PASS. Профиль11/11 PASS; после дополнения ownership-теста stream_member ещё раз PASS.
Логи: /tmp/member-purge-fix-{build,tests-build,core-final-build,profile-build,profile-results}.log.
Первый check.sh вне sandbox:197/200, три отказа bind(::1) при disable_ipv6=1 на внешнем lo.
Полные первичные логи сохранены /tmp/member-purge-fix-first-test_{ncd_faults,ipv6_sockets,route_connectivity}.log.
Повтор общего набора в user/network namespace с IPv6:200/200 PASS,0 failed/0 skipped.
Лог: /tmp/member-purge-fix-full-check-ipv6.log. Следующий шаг check.sh (proxy) в этом user namespace
остановился на fopen pidfile: Permission denied в /run, поэтому единый check.sh не дал exit0.
Три штатные интеграции отдельно выполнены в root network/mount namespace с собственным /run:
proxy9/9 PASS,burst1/1 PASS (8388608 байт),load8/8 PASS (134217728 байт).
Лог: /tmp/member-purge-fix-proxy-integrations.log. Таким образом все составляющие check.sh проверены;
настройки IPv6, маршруты, iptables и /run хоста этими повторными namespace-запусками не менялись.
ASAN/UBSan/LSan:11/11 PASS, ASAN_OPTIONS=detect_leaks=1:halt_on_error=1,
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1. Изолированная копия:
/tmp/utun-member-purge-sanitizers. Сначала configure потребовал запуск вне sandbox из-за LSan/ptrace;
затем проверка символов выявила отключённый sanitizer в локальном Makefile Opus.
Окончательная сборка через build.sh с MAKEFLAGS=-e инструментирует lib/src/Opus согласованно;
проверены __asan_ и __ubsan_ во всех трёх архивах.
Логи /tmp/member-purge-fix-asan-{consistent-build,consistent-tests-build,consistency,final-results}.log;
полные результаты каждого теста /tmp/member-purge-fix-asan-final-test_*.log.
Предварительные5 PASS с неинструментированным Opus не считаются окончательным sanitizer-набором.
Ключевые кодовые коммиты:55050a9e,deda3c58,a1ef68da,4e0d6174;
регрессии0c28503e,1cacf693,8442ddc2. Коммиты локальные, push не выполнялся.
Задача очистки закрыта; частичные отметки member/Merkle-документов сохранены,
поскольку исправление дефекта не заменяет оставшийся аудит всех контрактов и callers.
Native Windows/FreeBSD, GUI и Android в этом исправлении не запускались.
7. Продолжение аудита: история исключённого автора блокирует запуск
База продолжения5616be8c; рабочее дерево в начале было чистым.
Полностью прочитаны chat_feedback.c/.h, повторно сверены части Merkle-движка,
относящиеся к data_ops/сессиям/транзакциям и границы member/chat_sync startup.
Остальные интеграционные контракты, разделы poll и db_sync ещё не закрыты.
Согласно AGENTS.md:10 аудит вновь остановлен; реализация .c, wire и БД не менялись.
В chat_feedback.h добавлено предупреждение, merkle_sync_doc.md уточнён.
chat_feedback.h — [~]; прежние member/Merkle [~], merkle_tree.h[V] сохраняются.
Причина:
chat_feedback_init (chat_feedback.c:384) для каждой сохранённой записи вызывает
member_sync_verified_ed25519. SELECT этой функции (member_sync.c:1322) требует
source=0 AND deleted=0. Подписанное решение владельца deleted=yes сохраняет строку,
ключи, join и сертификат, но исключает её из SELECT. Проверка возвращает -1,
chat_feedback_init также -1. chat_sync_init (chat_sync.c:865) прерывает запуск;
chat_service_start (utun_instance.c:843) передаёт этот отказ выше.
В runtime-стенде непосредственно проверены feedback_init и chat_sync_init;
полный внешний chat_service_start/GUI runtime этим стендом не запускались.
Сценарий использует неизменённую fixture штатного tests/test_chat_feedback.c,
настоящие публичные member/feedback API и общий Makefile для линковки.
Два независимых узла, два канала, SQLite :memory:, никаких пользовательских данных.
Автор отличается от локального узла. Его join и приглашение подписаны штатно;
реакция подписана автором и успешно принята вторым узлом до исключения.
Затем владелец подписывает adm_tags ver=1/deleted=yes для этого автора только в1001.
SQL-проверка подтверждает сохранённую строку source=0/deleted=1, join/tree по64 байта.
Собственный участник канала1001 и автор в другом канале1002 успешно проходят проверку.
Результаты:
— До исключения: member_sync_init=0, chat_feedback_init=0, records=1.
— После исключения: member_sync_init=0, chat_feedback_init=-1.
— Отдельный вызов chat_sync_init=-1, chat_sync/msync освобождены.
— Feedback после отказа читается, подпись и ver не изменились: физической потери записи нет.
— Подписанное ver=2/deleted=no восстанавливает успешный member/feedback init с records=1.
— Итог2/2 FAIL ожидаемого запуска при исключённом авторе,exit1; tracked allocations восстановлены.
Полный повтор с дополнительной проверкой строки дал тот же результат.
Стенд: /tmp/member-merkle-feedback-restart.c
Цель: /tmp/member-merkle-feedback-restart.mk
Сборка: make -C tests -f Makefile -f /tmp/member-merkle-feedback-restart.mk /tmp/member-merkle-feedback-restart
Лог сборки: /tmp/member-merkle-feedback-restart-build.log (PASS)
Полный диагностический лог: /tmp/member-merkle-feedback-restart.log
Категории member_sync/chat_sync/debug включены наINFO; причина отказа явно видна в логе.
Архитектурная граница, требующая выбора:
Текущий допуск действий узла и проверка ранее сохранённого подписанного состояния
используют одну функцию, поэтому изменение членства ломает восстановление истории.
fb_apply_row также проверяет текущий допуск автора, а fb_hash/fb_page перечисляют
сохранённые записи без фильтра deleted. Поэтому исправление только init не задаёт
согласованную модель репликации/корня при разном порядке получения исключения и feedback.
Это статически установленное расхождение путей; отдельный сетевой replay/round не запускался.
Вариант сохранения истории требует отдельного контракта исторической подписи/сертификата
и правил учёта состояний исключённого автора; актуальный допуск peer/локальных действий
при этом остаётся отдельной проверкой. Следует явно определить отношение новых ver
и исключения: feedback не содержит подписанного времени или отсечки версии при отзыве.
Вариант исключения таких состояний из активного набора требует одинаковой фильтрации
init/apply/hash/page и атомарного изменения производного индекса при смене членства.
Политику хранения/подсчёта нельзя заменять молчаливым пропуском ошибки старта или
глобальным снятием deleted-фильтра с общего verifier, используемого также radio/attachments.
Это направления решения, не подтверждённый реализацией план исправления.
Для регрессии нужны исключённый другой автор, реакция/ответ/сохранённая отмена,
два канала, restart и поздний догон нового пира, удаление повреждённого участника,
восстановление членства, сохранность отмен и одинаковые корни при разном порядке событий.
Прежний purge-дефект закрыт; прежние200/200 и sanitizer11/11 не включают этот сценарий.
Новый полный check/sanitizers/native Windows/FreeBSD/GUI/Android не запускались,
поскольку runtime-код не менялся и аудит остановлен на подтверждённом дефекте.
8. Проверка архитектуры плана: source=0 не равен подтверждённому допуску
Проверка выполнена на f6e54478, до неё рабочее дерево было чистым.
Пользователь выбрал: после исключения реакции/голоса скрываются из результатов,
сохранённые записи, версии и отмены остаются в БД. Runtime-реализации нового плана пока нет.
Ни его чистота, ни корректность готового diff этим исследованием не подтверждаются.
Прежний план предполагал общий активный набор по source=0 AND deleted=0 и пересчёт
feedback при изменении соответствующего участника. Проверка нашла два пробела:
1) member_apply_record (member_sync.c:579) разрешает отложенную проверку приглашения,
когда нет доверенного ключа signed_by. При корректном join/идентичности запись может
сохраняться как source=0/deleted=0. member_finish проверяет зависимость в конце раунда.
member_sync_verified_ed25519 (member_sync.c:1320) в этом состоянии возвращает -1;
тот же -1 означает неактивность, неверную подпись и операционную ошибку.
Значит, SQL-фильтр не является доказательством полного допуска. Если немедленную
проверку feedback сделать обязательной для commit каждой записи MEMBERS,
корректное обновление может быть отклонено до поступления его зависимости.
Отложенную зависимость нельзя приравнивать к повреждению или отзыву членства.
2) Ключ пригласившего ищется в общей nodes, а не только в peers данного канала.
Появление ключа при обработке другого участника/канала меняет результат проверки
уже сохранённого автора. Уведомление и пересчёт только для изменённого node_id
в текущем namespace не охватывают такую зависимость. Если активный набор зависит
от полного допуска, его индекс, выдача и результаты должны меняться согласованно
также при разрешении зависимости. Это касается автора и его signed_by,
а не только полей deleted/source непосредственно изменённой строки.
Runtime-контрпример использует неизменённую fixture tests/test_chat_feedback.c,
три изолированных экземпляра SQLite :memory:, подлинные Ed25519-подписи и публичные API.
Сначала feedback автора сохранён с корректным корневым сертификатом.
Затем удалённый member_sync_apply_record принимает корректный join_ts=2 того же автора
с приглашением от ещё неизвестного signer. SQL_active=1, apply=MS_APPLY_CHANGED,
но verified_ed25519=-1 с причиной invalid or unresolved invitation signature.
После member_sync_destroy/init: member_init=0, feedback_init=-1, сохранённый feedback не менялся.
Затем в1002 добавляется только пригласивший; автор и feedback в1001 не изменяются.
verified_ed25519 для автора1001 становится0; следующий restart member/feedback даёт0/0.
Tracked allocations восстановлены. Проектные C-файлы, wire и схема БД не менялись.
Стенд: /tmp/member-merkle-feedback-plan-review.c
Цель: /tmp/member-merkle-feedback-plan-review.mk
Сборка: make -C tests -f Makefile -f /tmp/member-merkle-feedback-plan-review.mk /tmp/member-merkle-feedback-plan-review
Логи: /tmp/member-merkle-feedback-plan-review-build.log и /tmp/member-merkle-feedback-plan-review.log.
Exit0 означает подтверждённый контрпример с выполненными assertions, а не прохождение
исправленной реализации: план на исходный runtime не накладывался.
Обязательные уточнения перед реализацией:
— Явный результат проверки автора: допущен, исключён/отсутствует, ожидает зависимость,
недействительные данные, операционная ошибка. Один -1 для всех причин недостаточен.
— Единая модель допустимости для init/hash/page/результатов, с отдельной обработкой
ожидания зависимости; неподтверждённые состояния не выдаются за проверенные.
— Проверка зависимостей учитывает границу всей пачки/раунда MEMBERS, а также ключи,
появившиеся через другой канал. Ожидание feedback не блокирует корректную запись участника.
— При выборе зависимого от допуска Merkle-набора нужны явные пути пересмотра всех
затронутых авторов/namespace. Их индекс обновляется до публикации изменения,
changed/UI-события — после внешнего commit, а не из props callbacks.
— Неактивные сохранённые версии не удаляются; после восстановления выбираются
по прежнему правилу ver/data. Формат не доказывает создание до исключения:
обещать запрет любых подписанных в период исключения версий после восстановления нельзя.
— Регрессии дополняются неизвестным signer, разным порядком/страницами доставки,
поступлением ключа через другой канал и восстановлением без изменения самого автора.
Для чистоты реализации проверки удостоверения принадлежат member_sync, состояние и индекс
feedback — chat_feedback, представление опросов — chat_poll. Независимые копии признака
допуска, снятие общей проверки radio/attachments и запись индекса из post-commit callbacks
не являются согласованным решением. Полная реализация и её проверка остаются отдельным этапом.
9. Реализация feedback: активный набор, зависимости и восстановление
Реализован выбранный пользователем контракт: неактивные реакции/ответы исчезают
из результатов, выдачи и Merkle-набора, но сохраняются в feedback_<channel>
вместе с ver, подписью и нулевым состоянием отмены. При восстановлении допуска
сохранённое состояние заново проверяется и может распространяться новым пирам.
Wire v3/feedback v1 и формат подписанной записи не менялись; добавлен индекс (node_id,bucket).
Проверки удостоверения сосредоточены в member_sync_author_key: ACTIVE, INACTIVE,
PENDING, INVALID, ERROR. Только ACTIVE записывает выходной ключ. Недостающий сертификат
или доверенный ключ отличается от недействительной подписи и отказа SQL/OpenSSL.
Старый verified_ed25519 по-прежнему допускает radio/attachments только при ACTIVE.
source=0/deleted=0 — необходимый фильтр, но не доказательство полного допуска.
chat_feedback использует один fb_read_active для восстановления, хеша и страницы:
проверяются текущий допуск, подпись состояния, канонические record_key/bucket.
Неактивная/ожидающая история не включается в индекс; ошибка проверки не маскируется пустым листом.
Init строит активный индекс в одной транзакции после начального purge MEMBERS.
Входящие INACTIVE/PENDING не сохраняются и не ломают остальные корректные записи страницы.
Локальный set проверяет допуск до сравнения со старым значением, включая no-op.
chat_poll использует тот же author_key до декодирования и учёта ответа/реакции.
При изменении удостоверения, общего Ed25519-ключа или deleted пересчитываются
feedback-листья автора и зависимых от его ключа signed_by во всех каналах.
Пересчёт входит в транзакцию изменения MEMBERS/общего ключа; ошибка откатывает оба индекса.
PENDING не блокирует корректную запись участника, даже если signer приходит позднее.
До регистрации FEEDBACK hook ничего не делает; startup rebuild затем учитывает результат purge.
Сохранённая активность автора отдельно в БД не дублируется.
MS_APPLY_FEEDBACK переносит факт изменения удостоверения/ключа/допуска через внешнюю транзакцию.
Страница и JOIN сохраняют полную маску apply до вызова record_committed после внешнего commit.
Обычные profile/name/stream options меняют MEMBERS, но не пересчитывают feedback и не будят его сессии.
Post-commit уведомления читают адреса сообщений без живого cursor через callback.
После физического purge сохраняются feedback и барьеры ver у отмен; уведомления идут после commit.
Сетевой сценарий выявил также зависший старый раунд после исключения пира:
пакет уже отброшен по допуску, а восстановление членства само по себе не завершает ожидание.
merkle_sync_revalidate_peers после commit/changed завершает ожидания MT_ERR_DATA,
освобождает tx/waiter/deadline и оставляет подписку FAILED. При возврате changed начинает
новый раунд BEGIN/WAKE через прежний UP-транспорт. Callback вправе отменить сессию;
обход после callback не использует старый указатель. MEMBERS/другие datasets не затрагиваются.
Штатные регрессии:
— test_chat_feedback: исключённый другой автор, реакция/ответ/отмена, сохранность подписей/ver,
два канала, restart, запрет локального no-op/отмены, смешанная страница, физический purge,
возвращение без возрождения старой версии, неизвестный signer и его поступление только в1002.
Проверены SQL lookup/index failure, SQLite commit veto и внешние rollback/commit без ранних событий.
Подписанное обновление профиля оставляет feedback-корень/события неизменными.
— test_member_adapter/test_member_purge: дополнительная проверка ERROR и неизменности выходного
ключа при отказах двух стадий Ed25519, SHA256 и prepare/bind/step/finalize.
— test_merkle_protocol: отзыв допуска при заблокированных QUERY/ERROR, освобождение ожидания,
сохранение подписки, новый раунд после возврата, callback с удалением своей сессии.
— test_chat_poll_network: четыре настоящих ядра/UDP, A↔B↔C, исключение A, полный stop/start C,
поздний D без feedback исключённого A, возврат A, догон D и новая отмена от A по прежнему транспорту.
Проверены JSON-результаты, сохранённые версии/подписи/отмены и совпадение Merkle-корней.
Первый расширенный сетевой запуск остановился в стенде: D открыл группу до входящего UP.
Ожидание транспорта добавлено; затем тест обнаружил и помог исправить ожидание отозванного раунда.
Первый sanitizer-прогон дал12 PASS, затем test_stream_service остановился на временном
assertion line1630 (99–150 мс); отчёта ASAN/UBSan не было. По логу устранены лишние пустые
feedback-раунды при профилях/stream options. Временные границы stream-теста не ослаблялись.
Повторный полный профиль без параллельной нагрузки проходит15/15, включая stream_service.
Сборка обычного ядра/тестов PASS. До оптимизации отдельный check.sh дал200/200 и все
сетевые интеграции,exit0,193с: /tmp/feedback-author-fix-full-check.log.
Окончательный check.sh текущего кода:200/200 PASS,0 failed/0 skipped, proxy9/9,
burst1/1,8388608 байт, load8/8,134217728 байт,exit0,186с.
Лог /tmp/feedback-author-fix-full-final-check.log. Запущен через sudo -n unshare -nm
с собственным /run и новым lo; сеть, firewall и /run хоста не менялись.
Профильные логи /tmp/feedback-author-fix-native-final-{feedback,member,protocol,network}.log.
ASAN/UBSan/LSan:15/15 PASS, detect_leaks=1:halt_on_error=1:abort_on_error=1,
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1, отдельный user/network namespace.
Изолированная сборка /tmp/utun-member-purge-sanitizers; подтверждены __asan_/__ubsan_
в core/libuasync/Opus,12 изменённых C/header-файлов побайтно совпадают с рабочим кодом.
Логи /tmp/feedback-author-fix-asan-final-results.log, -final-run.log и -final-test_*.log;
согласованность /tmp/feedback-author-fix-{asan-consistency,source-consistency}.log.
Предварительные отказы сохранены в /tmp/feedback-author-fix-network*.log и -asan-test_stream_service.log.
Ключевые коммиты:606c32f3,5e125755,0877930e,c0095c54,23329a44;
штатные регрессии c4a6a128,9750fd22,f6a72c16,1720945d. Коммиты локальные, push не выполнялся.
Исправление закрыто. Это не завершает исходный аудит member/Merkle, db_sync и всех callers:
частичные отметки чеклиста сохранены. Native Windows/FreeBSD, GUI и Android не запускались.
10. Продолжение db_sync: отказ проверки удаляет корректную сохранённую запись
База aea8ca72 после rebase/push; рабочее дерево перед аудитом было чистым.
STUN и поиск первого падающего коммита исключены пользователем из этой работы.
Продолжено чтение db_sync.h, частей db_sync.c и db_sync_arch.md, связанных с хранением,
верификацией, удалением, пересчётом цепочки и исходящей пачкой. Аудит всего db_sync
и всех обработчиков/коллбэков ещё не завершён; db_sync.h/arch.md помечаются [~].
Runtime-код, wire, схема БД и штатные тесты проекта не менялись.
Уточнённый пользователем контракт:
DELETE разрешён, если проверка выполнена и подпись проверенных данных не совпала,
либо доказан повреждённый формат самих исходных данных. Отказ OpenSSL/памяти/SQL
и отсутствие необходимого ключа не доказывают порчу и не должны удалять данные.
Граница входящей копии и БД обязательна: неверная копия с тем же ключом не является
доказательством неверности ранее сохранённых байтов. Перед удалением сохранённой
строки проверять надо именно эту строку; корректную копию необходимо сохранять.
Причина первого дефекта:
db_verify_author_sig (db_sync.c:622) объединяет отсутствие ключа, битый формат,
SC_ERR_AUTH_FAILED и SC_ERR_CRYPTO в -1. db_record_insert (db_sync.c:666–676)
при любом таком результате выполняет DELETE по (timestamp,author_signature).
Это происходит до проверки дубликата. Поэтому операционный отказ на корректном
повторе стирает уже сохранённую запись и возвращает -2, а верхний лог называет её forgery.
Та же классификация управляет удалением при выдаче SEND_DATA в si_send_data_batch
(db_sync.c:1067–1077); отдельный сетевой runtime этой ветви не выполнялся.
Причина второго дефекта:
Входящие байты проверяются, но удаляются байты из БД по совпавшему ключу.
Если входящая копия изменена, а timestamp/подпись совпали с корректной сохранённой,
SC_ERR_AUTH_FAILED относится к входящей копии. Текущий DELETE всё равно стирает корректную.
Просто сохранить различие AUTH_FAILED/CRYPTO недостаточно: надо различать проверяемую копию.
Изолированное воспроизведение:
Один настоящий core/chat_service, два временных канала SQLite :memory:, свои подписи.
Сообщения публикуются через chat_core_submit_message; байты/подпись читаются публичным
db_sync_select и повторно передаются публичному db_sync_insert_signed.
Однократный EVP_DigestVerifyInit=0 вводится только в стенде через штатную линковку --wrap;
все операции вне этой точки используют настоящие OpenSSL/SQLite/libutun/uasync.
Нет пользовательской БД, третьих узлов или отправки повреждённых сетевых пакетов.
Случай ret Строк ожидалось/осталось Итог
Корректный дубликат 1 1/1 PASS
Разовый операционный VerifyInit=0 -2 1/0 FAIL
Испорченная входящая копия -2 1/0 FAIL
Неверная подпись самих сохранённых байтов -2 0/0 PASS
Битое сохранённое тело размером4001 -2 0/0 PASS
После операционного отказа оригинальная подпись отдельно успешно проверяется SC_OK.
После снятия отказа повторная вставка восстанавливает запись; без ошибки обычный
дубликат не удаляет её. Во втором отрицательном случае до вызова отдельно подтверждено,
что сохранённые байты корректны и отличаются от повреждённой входящей копии.
Положительные контроли порчи меняют только собственную временную строку стенда.
Другой канал сохраняет свою запись во всех случаях. После teardown tracked allocations
восстановлены. Exit2 означает2/2 FAIL ожидаемого сохранения, а не ошибку сборки/стенда.
Полный диагностический лог прочитан; причина VerifyInit failure и ошибочный forgery видны явно.
Дополнительный найденный дефект учёта доставки:
si_delivery_update (db_sync.c:851) формирует UPDATE с WHERE timestamp=? AND author=?,
но схема si_table_create содержит node_id и не содержит author. На реальной таблице
стенда подготовка EXPLAIN UPDATE возвращает SQLITE_ERROR/no such column: author,
без исполнения UPDATE. Поэтому эта ветвь не обновляет delivered_peers/delivery_chain;
ошибка подготовки самим si_delivery_update не логируется. Полный сетевой PUSH/ACK
для этой дополнительной находки не запускался; оба вызова используют тот же helper.
Стенд: /tmp/db-sync-audit-verify-failure.c
Цель: /tmp/db-sync-audit-verify-failure.mk
Сборка: make -C tests -f Makefile -f /tmp/db-sync-audit-verify-failure.mk /tmp/db-sync-audit-verify-failure
Логи: /tmp/db-sync-audit-verify-failure-build.log (PASS), /tmp/db-sync-audit-verify-failure.log (exit2).
Категории chat_sync=debug, crypto=info, остальное warn; запуск в отдельном user/network namespace.
Для исправления требуется единая классификация проверки (VALID, BAD_SIGNATURE,
BAD_FORMAT, DEFERRED, ERROR), сохранение корректной существующей копии и отдельная
проверка удаляемой строки. Условия нужны во вставке, исходящей пачке и обработчиках
отказа; удаление доказанной порчи должно согласовываться с пересчётом цепочки и
позициями/подтверждениями синхронизации. Неполный пакет без достоверного ключа записи
не является основанием удалить произвольные данные БД. Операционные отказы требуют
правильной диагностики и сохранения данных, включая путь неизвестного ключа.
Контроль4001 подтверждает одну ошибку формата, но не проверяет все варианты повреждения.
Полная проверка recalc, доставки и жизненного цикла callbacks остаётся дальнейшим этапом.
По AGENTS.md:10 аудит остановлен на подтверждённых дефектах; исправление runtime не выполнялось.
Предупреждение и уточнение контракта добавлены в db_sync.h/arch.md, задачи — в doc/tasks.md.
Новые полный check/sanitizers/GUI/Android/Windows/FreeBSD не запускались; STUN пропущен.
Результаты прошлых исправлений member/feedback сохраняются и не объявляются покрытием новых случаев.
11. Исправление db_sync после rebase — 26.10.2026
По запросу пользователя выполнен fetch/rebase на origin/master=03b05094.
Три локальных коммита аудита перенесены без конфликтов; range-diff:3/3 идентичны.
Резервная ветка backup/db-sync-pre-rebase-61f5ab4f сохраняет исходный HEAD.
Прежний запрет запуска STUN отменён новым запросом пользователя проверить его после rebase.
Реализация:
- Проверка конкретных байтов возвращает VALID, BAD_SIGNATURE, BAD_FORMAT, DEFERRED или ERROR.
Только SC_ERR_AUTH_FAILED завершённой операции означает BAD_SIGNATURE; CRYPTO и SQL lookup error — ERROR.
Неизвестный ключ — DEFERRED. SQLITE_NOMEM при чтении BLOB не превращается в BAD_FORMAT.
Формат здесь означает непустое тело до4000 байт и подпись ровно64 байта, не проверку JSON-схемы приложения.
- Перед удалением дубликата внутри BEGIN IMMEDIATE независимо проверяется сохранённая строка.
Корректная строка переживает неверную входящую копию. Доказанная сохранённая порча удаляется;
при корректном входе вместо неё атомарно сохраняется исправная копия.
Любой отказ чтения, DELETE, INSERT или COMMIT приводит к ROLLBACK без продвижения next_id/позиций.
- Выдача пачки проверяет длины и подпись самой сохранённой строки. SELECT завершается до DELETE по rowid.
Одна доказанно битая строка удаляется в собственной транзакции, затем пачка старого раунда не отправляется.
После COMMIT запускается пересчёт с0 и сбрасываются позиции/состояния пиров; другой канал не затрагивается.
- Recalc имеет отдельный pending, не зависящий от наличия таймера. Батч50 атомарен;
только успешный COMMIT продвигает позицию. Отказ SQL/хеша/таймера сохраняет работу.
Flush возвращает ошибку; обработчики не публикуют заведомо неготовые хеши как успешный результат.
Битая длина сохранённой подписи, обнаруженная recalc, удаляется с проверкой SQL и COMMIT.
- Парсер не передаёт короткую подпись дальше к чтению64 байт. Ошибка вставки/проверки/формата
прерывает SEND_DATA без подтверждения позиций и SYNC_DONE; успешно вставленная часть остаётся для повтора.
Старый peer-empty цикл теперь продвигается на фактически отправленное число записей при ограничении буфера.
- ACK_PUSH изменён с(timestamp,author) на message_id[32], общий размер33 с типом; совместимость не поддерживается.
SQL больше не использует отсутствующую author. Один UPDATE обновляет только нужную запись,
повтор ACK от того же пира не увеличивает счётчик; WAS_SENT выставляется только для локального автора.
Постановка PUSH в очередь доставкой не считается. Корректный дубликат PUSH тоже получает ACK.
Подготовка/bind/step/finalize SQL и неизвестный message_id диагностируются.
Проверки и доказательства:
- Прежний /tmp/db-sync-audit-verify-failure.c теперь exit0, сохранение2/2 PASS;
положительные контроли удаления неверной подписи и тела4001 PASS, другой канал и allocations сохранены.
Лог /tmp/db-sync-preservation-fixed.log. Исторический EXPLAIN старого неверного SQL
в этом стенде ожидаемо остаётся SQLITE_ERROR; новую доставку проверяют штатные тесты ниже.
- Новый tests/test_db_sync_integrity.c использует собственную SQLite, настоящие Ed25519 подписи,
uasync и точечные --wrap отказы.10 введённых отказов: VerifyInit входа/сохранённой копии/выдачи,
BLOB NOMEM во вставке/выдаче/recalc, COMMIT удаления/recalc/вставки, создание recalc-таймера.
Дополнительно реальные SQL-триггеры запрещают DELETE, обновление хеша и ACK; проверены откат и повтор.
Неизвестный ключ и SQL lookup error сохраняют историю. Проверены две таблицы, reset позиций,
замена порчи корректной копией, длины подписи0/1/63/65/255, тело4001 и55 записей (несколько батчей).
ACK для двух подписей одного автора при одном timestamp обновляет только конкретную запись и идемпотентен.
После teardown tracked allocations восстановлены. Лог /tmp/db-sync-integrity-final.log,exit0.
- tests/test_db_sync.c дополнен двумя сообщениями одного автора с одинаковым timestamp:
до poll доставки нет, затем реальные PUSH/ACK подтверждают оба сообщения независимо.
Все прежние сценарии2/3 узлов, merge, mid/tail PUSH, reply и randomized reconnect также PASS.
Лог /tmp/db-sync-network-final.log; схема — /tmp/db-sync-schema-final.log, оба exit0.
- Полный ./check.sh в отдельном root network/mount namespace:203/203 PASS,0 failed/0 skipped;
proxy9/9,burst1/1 (8388608 байт),load8/8 (134217728 байт),exit0,195с.
Лог /tmp/db-sync-full-final-check.log. Финальная целевая сборка без предупреждений.
- STUN: отдельный запуск и общий набор PASS. Прежнее падение assert(bind) после rebase не воспроизведено;
причина прежнего отказа и первый падающий коммит не установлены, исправление STUN не заявляется.
В общем STUN-логе есть штатно пропущенный подслучай bind-to-device (нет разрешения);
основные RFC5780/fallback/cache/lifecycle/isolation проверки выполнены.
Логи /tmp/stun-after-rebase.log и tests/logs/test_stun_nat.log.
- Отдельная копия HEAD9131789c: ASAN+UBSan и detect_leaks=1,halt_on_error=1;
core/libuasync/Opus согласованно инструментированы.8/8 PASS: db_sync_integrity,db_sync,
db_sync_schema,member_adapter,chat_feedback,chat_poll,chat_poll_network,merkle_protocol.
/tmp/db-sync-asan-results.log и /tmp/db-sync-asan-test_*.log; признаков sanitizer errors нет.
Границы:
Полный аудит db_sync не закрыт. В существующем db_handle_sync_done ветка одинакового count
с разными chain_hash всё ещё явно принимает состояние как converged; это отдельный дефект
финального критерия, не проверявшийся новым runtime-стендом. Wire раундов не имеет generation ID;
поведение поздних пакетов прежнего раунда требует отдельного анализа. Также остаются TTL
(единицы времени/пересчёт после удаления) и жизненный цикл callback-списка.
Эти места не включены в доказанное покрытие текущего исправления. GUI/Android/Windows/FreeBSD не запускались.
Следующий этап аудита требует отдельного исправления финального критерия; по AGENTS.md
дальнейшее расширение текущей реализации остановлено на этом замечании.
12. План устранения ложной сходимости истории и проверка предпосылок — 26.10.2026
Запрос пользователя: продумать исправление и его корректность. Runtime-код в этом этапе не менялся.
Повторно прочитаны относящиеся к обмену структуры/функции db_sync, общий merkle_sync,
контракт data_ops, запуск/остановка chat_sync, создание канала, callback докачки медиа и sleep policy.
Почему заменить одну ветку на restart недостаточно:
1. db_handle_send_data объявляет sync_state=2/done сразу после отправки собственного SYNC_DONE,
ещё до получения подтверждённого равенства. db_handle_init_resp имеет отдельный путь завершения.
2. db_handle_init_resp исследует только последние51 позицию через16 интервалов.
Самая ранняя несовпавшая проба не является первым отличием или доказанным общим префиксом.
3. chain_hash включает id, который не входит в ключ записи или подпись автора.
Разные id одного подписанного сообщения сохраняются при duplicate и не исправляются пересылкой.
4. OFFSET относится к изменяемой таблице; поздний PUSH/DELETE сдвигает позиции.
В старом wire нет идентификатора раунда, запросы/ответы не проверяются по фазе.
5. Сравнение8 байт не заменяет проверку полного SHA256 при объявлении успеха.
Дополнительный изолированный стенд /tmp/db-sync-completion-design.c/.mk, сборка через tests/Makefile:
использованы настоящие SQLite/OpenSSL/uasync и private-функции из штатного integrity test.
Только собственные временные записи, вызовы локальных обработчиков, без сетевых пакетов.
- Две таблицы одного group_id содержат одинаковые timestamp/author/data/signature, но id1 и900.
Обе db_sync_chain_verify возвращают0, последние хеши различны. Передача локальному обработчику
count1/чужого хеша вызывает done1 и state2: ложное завершение подтверждено runtime.
- По200 записей, различие только в первой; остальные199 записей и id совпадают.
Фактические хеши по текущим sparse-пробам дают fm148. Весь хвост148..199 — дубликаты;
его повторная вставка в обе таблицы сохраняет различие корней. Простое повторение старого обмена
не имеет условия прогресса. Симулировался выбор sparse-проб и повтор хвоста, не полный сетевой раунд.
- Exit0, tracked allocations восстановлены. /tmp/db-sync-completion-design-build.log и
/tmp/db-sync-completion-design.log. Это диагностика существующего кода, не тест новой реализации.
Повторная проверка самого плана выявила обязательную предпосылку общего движка:
при merkle_sync_cancel сессия и её round освобождаются. Новый follower начинает с round0,
поэтому прежний BEGIN7 принимается как новый и получает READY. Локальный стенд
/tmp/merkle-reattach-design.c/.mk это подтвердил на штатной protocol-фикстуре после cancel/start:
round7,stateSERVE,READY в очереди. Применение старых PAGE или ложный MT_OK этим стендом
не проверялись и не заявляются. Логи /tmp/merkle-reattach-design-build.log и
/tmp/merkle-reattach-design.log,exit0,allocations восстановлены.
Значит, при переносе нельзя безусловно считать поле round достаточной защитой после пересоздания сессии.
Варианты:
А. Сохранить собственный bulk-протокол: потребуются канонический хеш без id, поиск настоящего общего
префикса либо полный fallback с0, устойчивый курсор, два явных прохода, round/request/state,
проверка revision и подтверждённого корня, backpressure и корректная отмена.
Это второй самостоятельный автомат с теми же обязанностями, что уже выполняет merkle_sync.
Б. Рекомендуемый: история как MT_DATA_MESSAGES в общем merkle_sync. db_sync остаётся моделью хранения,
публичным API истории и PUSH/ACK. Старые INIT/RESP/SEND_DATA/SYNC_DONE и цепной recalc удаляются
после подключения новой модели. Два bulk-протокола одновременно не поддерживаются.
Это архитектурный перенос, а не локальная замена ветки SYNC_DONE; отдельное одобрение реализации
не подразумевается самим запросом продумать план.
Конкретный план вариантаБ:
0. До подключения истории закрепить изоляцию пересозданных сессий общего Merkle.
В новой версии wire использовать двустороннее подтверждение попытки:
BEGIN несёт свежий round_id инициатора; READY добавляет свежий accept_id ответчика;
последующие QUERY/PAGE/TURN/CHECK/DONE/ERROR проверяют пару идентификаторов, request и фазу.
Идентификаторы — случайные128 бит с проверкой отказа генерации; каждый новый раунд/handshake
получает новые значения, отмена уничтожает действующий accept_id. Поля namespace/dataset/соединение
остаются частью области сессии. READY принимается только для ожидаемого текущего BEGIN.
Поздний BEGIN после cancel может получить свежий challenge; старые PAGE/DONE содержат прежний accept_id
и не могут примениться или завершить новый запрос. Дубликат BEGIN активной попытки не перезапускает
её применение; повтор WAKE может вызвать новый handshake, но не является успехом или применением данных.
Так не требуется хранить неограниченный архив отменённых сессий или надеяться на порядок доставки.
Проверить это для MEMBERS/FEEDBACK прежде, чем добавлять MESSAGES. Старая версия wire не поддерживается.
1. Канонический набор истории.
Namespace — числовой channel_id; dataset отдельный от MEMBERS/FEEDBACK.
Ключ — существующий message_id[32], проверяемый вычислением из group_id/timestamp/signature.
Лист и страница используют один и тот же отбор записей и порядок message_id по полным32 байтам.
Канонические байты записи: timestamp(BE64),author(BE64),data_len(BE32),точные data,signature64;
message_id связывает запись с каналом. Хеш листа включает ключ и эти однозначно кодируемые байты.
id,flags,local_attrs,delivery_chain/delivered_peers и прочие локальные поля в корень не входят.
Существующие подписи и идентичности сообщений не меняются; переподписывать историю не нужно.
Авторство старой записи не фильтруется текущим ACTIVE-правилом feedback. Допуск передающего пира
и проверка исторической подписи — разные проверки; действующие права не расширяются.
2. Индекс и границы SQL-транзакций.
Добавить индекс для выбора листа и keyset-страниц; bucket — старшие25 бит ключа.
Первая активация/restart восстанавливает производное дерево из сохранённых записей до снятия gate.
Индекс строится ограниченными порциями, без долгой SQLite-транзакции между сетевыми событиями;
ошибка оставляет канал неготовым, не публикует неполный корень как пустой.
Ожидание неизвестного ключа автора — отдельное от операционного отказа состояние: строки сохраняются,
готовность истории откладывается, но запуск чата не падает. После поступления общего ключа, в том числе
через другой канал, индексация/попытка возобновляется без перезапуска сервиса.
Каждый путь изменения содержимого (локальная вставка, PUSH, bulk page, доказанная порча, TTL)
сохраняет строки и пересчитывает затронутые Merkle-пути в одной транзакции.
Только после COMMIT меняются runtime-состояние/next_id и вызываются changed/on_insert.
flags/local_attrs/ACK не меняют набор и не запускают Merkle.
Перестройка формата должна сохранять подписанные строки и локальные атрибуты; DROP истории недопустим.
3. Adapter data_ops.
get_page — WHERE bucket=? AND message_id>after ORDER BY message_id LIMIT..., с учётом размера пакета.
Применение проверяет весь формат страницы, ключи/курсоры, подписи и достоверность identity до COMMIT.
Одна страница атомарна: частичная запись/индекс/события при SQL или OpenSSL failure запрещены.
apply_items не вызывает каскад PUSH и не отправляет пакеты из callback. Для локальной вставки/PUSH
после COMMIT вызывается changed; bulk тоже уведомляет о реальном изменении, чтобы оно дошло другим пирам.
Неизвестный ключ/ошибка операции прерывают попытку без удаления и ложной пустой страницы.
BAD_SIGNATURE/BAD_FORMAT относится к проверенным сохранённым байтам и допускает согласованную очистку.
Хеширование/чтение страницы сами не удаляют строки из открытого курсора и не запускают рекурсивный update.
Если там найдена сохранённая порча, чтение завершается ошибкой; очистка выполняется отдельно
после закрытия курсора в проверяемой транзакции, обновляет индекс и запускает повтор.
4. Управление раундом и успехом.
Используются общий request/state и усиленная пара идентификаторов из п.0, два pull-прохода,
CHECK/DONE, полный32-байтовый корень,
revision, deadline и очередь с waiter. Ошибка или повтор неизменной пары разных корней — не успех.
done для db_sync/media поступает только от MT_OK; постановка пакета в очередь и конец страницы
не подтверждают завершение истории. Последующие изменения вновь делают сессию dirty.
Успех означает подтверждённую контрольную точку данного раунда, не вечную глобальную синхронность
при продолжающейся записи. PAGE/DONE/ERROR прежней пары идентификаторов не изменяет данные/позиции,
не продлевает deadline и не завершает новый запрос. BEGIN/WAKE рассматриваются отдельно как handshake,
а не как доказательство результата. Эти свойства проверяются и для подключённой модели истории.
5. Интеграция, права и владение.
Регистрировать MESSAGES после создания общего движка при chat startup; data_ctx — стабильный inst,
канал ищется по namespace, указатель на DB_SYNC_INSTANCE не удерживается движком после remove.
Операционный отказ регистрации/индексации диагностируется и откатывает незавершённое подключение;
ожидание ключа не приравнивается к ошибке. При restart заново восстановить дерево и подписки,
даже если ETCP уже UP и повторного события UP не будет. До MEMBERS и READY история остаётся gated.
validate_peer проверяет активную таблицу/готовность/членство. Изменение допуска после COMMIT
переоценивает и MESSAGES, отменяя недопущенный раунд без потери чужих datasets.
Нужна узкая отмена по(dataset,namespace), а для sleep — по(dataset,peer): cancel_ns/cancel_peer сейчас
затрагивают сразу MEMBERS/FEEDBACK. Remove канала отменяет запросы истории до освобождения состояния.
Порядок stop учитывает, что member_sync_destroy уничтожает общий движок раньше удаления db_sync instances.
Публичные проверки цепочки заменяются явными проверкой истории/чтением полного Merkle-корня;
тесты обновляются, старые chain API с неожиданно изменённой семантикой не сохраняются.
Мост старых done callbacks требует безопасного удаления подписок во время dispatch; кешировать
next на освобождаемом элементе нельзя. Проверить удаление текущего/следующего callback и канала.
6. Sleep/sound и PUSH/ACK — обязательная часть, не скрытое изменение поведения.
Сейчас общий движок запрещает start при peer_sleep_phase, а chat_sync при sleep отменяет все datasets.
db_sync допускает важный чат при sound_on=1. При переносе это нельзя молча потерять.
Ввести предикат разрешённой работы модели: default для MEMBERS/FEEDBACK сохраняет нынешний запрет сна;
для MESSAGES использует существующий db_sync_peer_throttled. Проверять его при старте, автоповторе
после changed и обработке события sleep/sound, отменяя только запрещённые сессии.
Awake и включение sound_on повторно подключают допустимые READY-сессии, даже без нового UP;
одного changed недостаточно, если предыдущая отмена освободила подписку.
PUSH и ACK(message_id) продолжают текущую адресную доставку; собственный round не требуется ACK,
потому что он подтверждает конкретную неизменяемую запись. Очередь не считается доставкой.
Отказ PUSH не теряет dirty-изменение Merkle. Bulk-совпадение корня само по себе не увеличивает
delivered_peers: этот счётчик сохраняет текущую семантику индивидуальных ACK, без подтверждения
более поздних записей старым результатом раунда.
7. TTL и очистка.
До удаления старой цепочки исправить сравнение timestamp(ms) с cutoff сейчас в микросекундах;
использовать то же определение времени, что при создании timestamp, и исключить unsigned underflow.
Сохранить отдельную политику удаления локальных неподтверждённых записей, без глобальных tombstones.
TTL удаляет строки и обновляет MESSAGES-пути атомарно; изменившаяся история уведомляет движок после COMMIT.
Удаление хорошей копии из-за ошибки проверки запрещено прежним контрактом.
Почему этот план логически сходится:
- Равенство набора записей даёт одинаковые корни независимо от локальных id, порядка получения,
delivery counters или local_attrs. Изменение подписанного содержимого меняет канонические байты.
- В пределах листа after — полный ключ неизменяемой записи, а не OFFSET; вставка до курсора не сдвигает
уже читаемый хвост. Если её пропустил текущий проход, различие финальных корней требует нового раунда.
- При конечном наборе допустимых записей, прекращении внешних изменений, доступных ключах/ресурсах,
доставке пакетов и отсутствии криптографических коллизий первый pull добавляет записи второго узла
первому, обратный — объединение второму. Пропущенные из-за изменения дерева листья дочитываются
следующим раундом; принимать разные корни для окончания не требуется.
- Ошибки формата/операций и неизвестные зависимости не кодируются совпадением; repeated-root conflict
завершает попытку ошибкой, сохраняя данные. Поэтому нет бесконечного успешного restart без прогресса.
- Сохранённые данные и индекс меняются атомарно. Предположение доказательства — все изменения модели
проходят этот путь и производный индекс согласован; произвольная порча SQLite вне модели не становится
автоматически обнаружимой без чтения/проверки соответствующих записей. Startup и читаемые страницы
проверяются явно; непрерывная проверка всех байтов хранилища этим планом не обещается.
- Контрольная точка допускает последующую запись/отзыв/TTL: они помечают дерево dirty или отменяют
доступ, а не используют старый MT_OK для подтверждения новых данных.
Проверки при реализации (требования, сейчас не запускались для новой модели):
- Равный count и разные наборы: отличие в начале/середине/конце истории, >200 и несколько тысяч записей;
исходные ложные завершения воспроизводятся до исправления и исчезают после него.
- Полностью одинаковые подписанные записи с разными id/local_attrs/доставкой: одинаковый полный корень.
- Пустые таблицы; по одному сообщению; два автора с одним timestamp; несколько страниц одного листа,
пакет заполнен раньше лимита записей, дубликаты и повтор последней страницы.
- PUSH/вставка до и после курсора; третий узел; удаление/TTL; изменения между CHECK и отправкой DONE.
- Старые round/request, неожиданные фазы, отмена/повтор, DOWN/REINIT/restart, старый UP без нового события.
- Cancel/start follower, затем прежний BEGIN и PAGE/DONE старого accept_id; новая попытка того же namespace
на том же UP-соединении; повторы BEGIN/READY/WAKE и старый ERROR. Никакой успех/запись от старой пары.
- SQL prepare/bind/step/commit, BLOB NOMEM, хеш/VerifyInit, очередь/таймер/waiter: нет частичных данных,
ложного MT_OK, потери корректных строк или callback после cancel/free.
- ACTIVE-допуск пира, отзыв/возврат, сохранение старых сообщений исключённого автора; различие sound_on/off
при сне, изменение sound в ходе раунда, восстановление после пробуждения.
- Недостающий исторический ключ не роняет startup и не стирает записи; его получение через другой
канал возобновляет индексацию и обмен без restart. Операционный SQL/crypto error имеет отдельный исход.
- Сохранность прежней истории/подписей/локальных атрибутов при смене схемы, удаление только derived-chain
формата, startup index failure, остановка и повторный запуск чата с работающим UTUN.
- Корректные индивидуальные ACK без двойного счётчика; media backfill вызывается только при MT_OK;
callback отписывает себя/следующий callback и удаляет канал без UAF.
- Общий check.sh со STUN и целевые ASAN/UBSan/LSan после реализации.
Статус: план вариантаБ сформирован и уточнён после отдельной проверки reattach;
логическое обоснование требует п.0 и согласованного индекса, а не опирается на неизменённый старый движок.
новая модель и её тесты ещё не реализованы. Перенос нельзя объявлять проверенным текущими203/203
и8/8: эти результаты относятся к уже выполненному исправлению раздела11.
13. Реализация MESSAGES, свежий handshake и ограниченные транзакции
Пользователь одобрил вариантБ раздела12, уточнив два обязательных условия:
- Не ограничивать исторические сообщения по deleted автора: ограничение входа в группу отдельно.
- Не фиксировать всю новую историю одной транзакцией: короткая локальная вставка/страница,
сеть вне транзакции, первичная проверка и построение индекса ограниченными порциями.
Реализовано:
- db_sync оставляет общую SQLite, подписанные записи, публичное API и PUSH/ACK(message_id).
Собственный bulk-автомат, chain_hash, sparse-поиск и сетевой OFFSET удалены.
MT_DATA_MESSAGES=2 использует общий merkle_sync; namespace=group_id, ключ=message_id[32].
Канонический лист включает ключ/timestamp/author/длину/точное тело/signature и исключает local id,
attrs/flags/доставку. Одинаковая подписанная история не зависит от локального порядка получения.
- Wire v4:55-байтовый заголовок с двумя случайными128-битными round_id/accept_id.
Старые PAGE/DONE/ERROR после cancel/start не проходят новый challenge; дубликаты BEGIN/READY
не сбрасывают применение. Ошибка RNG обеих сторон даёт MT_ERR_IO до установки нового раунда.
Полные корни и revision подтверждаются общими CHECK/DONE; равный count не означает успех.
- Страница MESSAGES проверяет полный32-байтовый курсор, лист, строгий порядок и подписи до BEGIN.
До16 записей и ограничение размера wire; данные/индекс/next_id фиксируются атомарно.
Обычная вставка меняет один путь дерева в своей короткой транзакции.
Preflight проверяет до16 строк вне write-lock, затем сверяет собственные байты в коротком commit.
Второй этап строит до16 листьев/путей за commit; timer1 tick возвращает управление I/O между шагами.
Число строк ограничено, но жёсткий временной предел commit не обещается: диск/наполнение листа влияют.
- VALID/BAD_FORMAT/BAD_SIGNATURE/DEFERRED/ERROR сохранены. Удаляются только доказанно неверные
сохранённые байты; SQL/OpenSSL/NOMEM/неизвестный ключ не удаляют историю и не публикуют пустой корень.
Ошибка оставляет pending; поздний общий ключ, включая событие другого канала, возобновляет preflight.
Ключ автора читается независимо от deleted; допуск пересылающего READY-пира проверяется отдельно.
- Миграция схемы сохраняет подписанные строки и local_attrs/flags/счётчики доставки.
Производные message_id/bucket вычисляются заново; ошибка DDL/copy/COMMIT откатывает перестройку.
Подпись неправильной длины — доказанный неверный формат, корректной длины проверяется preflight.
- Новые data_ops apply_page/can_run/on_result; узкая отмена dataset/ns и dataset/peer.
MESSAGES сохраняет sound_on-разрешение спящего пира; остальные модели используют default sleep policy.
Отзыв/возврат допуска и sleep/sound переоценивают сессии на прежнем UP, не затрагивая чужие datasets.
Android retry сохраняет отменяемое ожидание активной фазы, inline callback отказа памяти не рекурсивен.
- on_insert bulk отложен за пределы data_ops и видит committed страницу; rollback отменяет уведомления.
Done получает только MT_OK, включая автоматические раунды; самоотписка, удаление следующей подписки
и канала безопасны. Remove сохраняет таблицу, отменяя только её MESSAGES/таймеры/уведомления.
- TTL исправлен на миллисекунды Unix и удаляет до16 собственных неподтверждённых строк за commit.
Удаление и очистка MESSAGES-индекса атомарны; далее корень gated/pending до восстановления.
Это уточнение реализации к плану: массовая очистка сбрасывает производный индекс вместо обновления
каждого удалённого пути. Не создаёт tombstone; другая сохранённая копия может вернуться при обмене.
Проверки новой реализации:
- test_db_sync:реальный ETCP/READY,2/3 узла, PUSH/ACK и случайные слияния;200 одинаковых записей,
затем отличие первой при равном count и сдвиг local id на900. Итог201 на обоих, полный корень одинаков.
- test_chat_sync_stress UTUN_TEST_HISTORY_LARGE=1:20 чередующихся пачек по250 записей двух авторов;
вставлено5000, вместе с исходными2 итог5002/5002 и одинаковые полные корни,exit0.
insert_avg453мкс,финальная сходимость2180мс,весь сценарий4485мс; обычный случай после добавления режима PASS.
- test_db_sync_integrity:11 введённых crypto/SQL/COMMIT/BLOB/timer отказов, реальные подписи/триггеры;
сохранность хорошей копии, доказанная порча/формат0/1/63/65/255/4001, два канала, индекс/next_id rollback.
Плотный лист40 записей, несколько страниц, вставка до курсора требует следующей сверки, replay без дублей.
Отказ второй записи и COMMIT страницы откатывает все строки/корень/уведомления; on_insert только после commit.
Подписи preflight проверяются при sqlite3_get_autocommit=1. Разные id/attrs/flags/доставка не меняют корень.
История deleted автора выдаётся; поздний ключ через событие другого канала сохраняет строку и запускает индекс.
TTL дренирует19 expired в двух коротких commit и оставляет fresh/ACKed. Tracked allocations восстановлены.
- test_merkle_protocol:старые токены/фазы/курсоры, reattach, дубликаты BEGIN/READY, RNG обеих сторон,
backpressure, timeout, apply/SQLite faults, модельный sleep-допуск и восстановление на том же соединении.
Последняя регрессия меняет revision после получения CHECK до передачи DONE:старый checkpoint
не завершает запрос; контроль без изменения MT_OK. Нативный и ASAN/UBSan/LSan прогоны PASS.
- test_db_sync_schema:сохранность полей, повторный restart, rollback DDL после copy, очистка только доказанной порчи.
- test_dm_invite:настоящий JOIN и UTUN_TEST_HISTORY_TIMEOUT=1; принудительно потерянный обмен30с
завершается timeout, повтор восстанавливает историю, тест exit0.
- Общий первый прогон199/203:две изолированные db_sync-фикстуры не создавали общий движок,
purge-фикстура использовала недопущенного peer2; исправлены setup/teardown и реальный допущенный peer.
Повтор202/203, STUN PASS:stream-сценарий зависел от standby предыдущих сценариев.
DEBUG и SQL-диагностика показали новые версии только у автора и sleep=1 на центральном пиру;
wake-only PASS. Тест явно восстанавливает activity центрального узла и пробуждает lifecycle-получателя
после сохранённой проверки перехода forced-active → standby. Production stream не менялся.
- ASAN/UBSan/LSan10/10 PASS,detect_leaks=1:db_sync,integrity,schema,merkle_protocol,merkle_sync,
member_adapter,chat_feedback,chat_poll,chat_poll_network,dm_invite. Core/libuasync/Opus инструментированы
согласованно в /tmp/utun-member-purge-sanitizers. Нативные/ASAN-объекты не смешивались.
Отдельной Makefile-цели test_member_sync нет: исключена ошибочная попытка implicit build;
соответствующие проверки входят в member_adapter. Новая sanitizer-сборка/запуск10 целей exit0.
- Временная цель через tests/Makefile: /tmp/history-standby-check.c/.mk, макрос UTUN_HAVE_STANDBY,
настоящий history-код/SQLite/uasync и управляемый standby callback. Inline OOM даёт один retry без
рекурсии, wake освобождает ожидание, allocations восстановлены;exit0. Это не запуск Android-приложения.
Документы db_sync.h/db_sync_arch.md и merkle_sync.h/merkle_sync_doc.md приведены к реализации.
Ограничения:индекс согласован при записи через модель; произвольная внешняя порча SQLite
выявляется при соответствующем чтении/перепроверке, непрерывный scrub всех байтов не заявляется.
Платформенные GUI/Android/Windows/FreeBSD прогоны в этом этапе не выполнялись. Общий аудит чата частичный.
Логи: /tmp/history-merkle-asan-results.log, /tmp/history-merkle-asan-test*.log,
/tmp/history-merkle-final-test_db_sync_integrity.log, /tmp/history-merkle-final-test_merkle_protocol.log,
/tmp/history-merkle-dm-timeout.log, /tmp/history-standby-check.log.
Окончательный общий прогон /tmp/history-merkle-verified-check.log:203/203,0 failed/0 skipped,
proxy9/9,burst1/1,load8/8 (134217728 байт),STUN/stream PASS,exit0,179с.
После него добавлены только два тестовых случая:5000 записей и CHECK/DONE revision;
оба нативно PASS, CHECK/DONE также PASS с sanitizer. Production-код не менялся.
Дополнительные логи: /tmp/history-merkle-large.log, /tmp/history-merkle-final2-test_merkle_protocol.log,
/tmp/history-merkle-final2-test_chat_sync_stress.log, /tmp/history-merkle-asan-checkpoint.log.
Задача переноса закрыта. Изменения сохранены локально; push этого этапа не выполнялся.
14. Согласование отметок общего чек-листа
После завершения раздела13 итог был добавлен в конец /home/user/utun-doc-audit-checklist.txt,
но семь строк основного списка ошибочно остались [~]. Исправлено по замечанию пользователя:
member_sync.h/member_sync_doc.md, merkle_sync.h/merkle_sync_doc.md, db_sync.h/db_sync_arch.md
и историческое merkle_tz.md отмечены [V]. merkle_tree.h уже имел [V].
В документах сняты устаревшие формулировки о незавершённости именно этого блока;
добавлено уведомление MESSAGES после commit MEMBERS, историческое ТЗ ссылается на wire v4.
Остальные модули чата и записи других аудитов сохраняют прежние статусы.
Это закрытие сверки описаний с кодом; непроведённые платформенные прогоны отдельно перечислены выше.
Изменены только комментарии/документы/учёт статусов; runtime и тесты не менялись,
прежние результаты раздела13 действуют, повторный запуск не требовался.
15. Rebase на актуальный master и повторная проверка
По запросу пользователя выполнен fetch origin/master:03b05094 → b0bf27de,23 upstream-коммита.
Перед переносом40 локальных коммитов сохранена backup/messages-merkle-pre-rebase-6b8dd2ec.
Конфликты только в doc/tasks.md:задачи upstream и актуальные локальные закрытия сохранены.
Range-diff40/40 сопоставлен; три отличия касаются только контекста/объединения doc/tasks.md.
Все22 локальных уникальных файла и28 upstream-уникальных файлов побайтно сохранены;
общие файлы doc/tasks.md и tests/Makefile.am объединены,новые upstream-цели и wrappers присутствуют.
Код и регрессии member/Merkle/history не изменились; DM/call/Android/desktop соответствуют upstream.
Native build PASS; check.sh206/206 PASS,0 failed/0 skipped,STUN/stream PASS,
proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,197с.
Санитайзеры после rebase заново не запускались; прежний профиль10/10 относится к разделу13.
Логи /tmp/member-merkle-rebase-{build,check,range-diff,preservation}.log.
Перед отправкой ls-remote подтвердил master=b0bf27de6188746e53ff436f64a24ec69789051a.
+25 -7
View File
@@ -1,7 +1,8 @@
# Ссылки в сообщениях
Android и Qt распознают `http://`, `https://` и `utun://` в обычном тексте
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы.
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы;
`utun://` распознаётся только в нижнем регистре. Утилиты разбора не открывают ссылки.
Ссылки выделяются цветом и подчёркиванием; длинные веб-адреса переносятся по ширине.
Сокращённая строка структурированного PM-приглашения сохраняет полный `utun://` адрес.
@@ -11,26 +12,43 @@ Android и Qt распознают `http://`, `https://` и `utun://` в обы
Адреса без схемы не распознаются. Некорректные веб-адреса остаются обычным текстом.
Исходное сообщение в БД и протоколе не меняется.
Разборщики возвращают диапазоны UTF-16 и полные адреса:
У `utun://` распознаётся кандидат из обычного base64-алфавита с до двух конечных `=`.
На этом этапе не проверяются версия, поля и содержимое приглашения: `utun://abc+/==`
может получить подсветку, но быть отклонённым при открытии. Предпросмотр Qt и Android
использует общий `src/chat/invite_link.c` через Qt-обёртку/JNI; принимается версия 0x03.
Сам декодер не проверяет регистрацию/срок join_key и не даёт членства.
Разборщики возвращают непересекающиеся диапазоны исходного текста в порядке появления
и полные адреса в исходном написании: Qt — `start/length`, Android — `start/end`,
где end исключён. Единицы обоих API — UTF-16:
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
Удаление внешней пунктуации занимает линейное время: баланс трёх видов скобок
считается один раз, хвост сокращается по индексу без повторного копирования адреса.
Порт веб-адреса пустой либо десятичный от 0 до 65535; знак перед числом недопустим.
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
Перетаскивание выделяет текст без открытия ссылки.
HTTP/HTTPS открываются из GUI системным обработчиком: Android `ACTION_VIEW`,
Qt `QDesktopServices::openUrl`. `utun://` запускает существующий JOIN.
Qt `QDesktopServices::openUrl`. На нажатии веб-адрес снова проверяется.
В Qt `utun://` сначала декодируется: уже существующая группа, где узел является
участником, открывается без JOIN; иначе показывается JoinDialog с автоматической попыткой входа.
На Android адрес передаётся в существующий invite/JOIN flow.
Ошибки открытия показываются через перевод интерфейса и логируются.
Диагностика открытия содержит схему и host, без полного пути/query.
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
Android `MessageLinksTest` проверяет тот же TSV через Gradle, некорректные authority
и длинный хвост пунктуации. Аналогичный тест длинного хвоста есть в Qt.
Оба разбора не открывают ссылки; некорректный пользовательский текст не создаёт логов.
```bash
cd tools/chatgui-android
./build.sh noinstall
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
./gradlew :app:testDebugUnitTest --tests com.utun.chat.data.MessageLinksTest
```
`tests/test_message_links.kt` остаётся отдельным CLI-тестом того же TSV;
ему передаётся путь к файлу, а в classpath нужны собранные классы `MessageLinks`.
+51
View File
@@ -88,10 +88,61 @@ READY сессии. Принятая подписанная запись и пу
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
обслуживает соединение, и использует её при восстановлении.
Для обхода адресов NCD делает отдельную копию: callbacks транспорта могут заменить
списки в общем реестре до завершения обхода. Удержание ссылки на запись сохраняет
сам объект, но не старые списки. Обновление, открытие и добавление нового сокета
используют общий подбор разрешённых пар: family, UDP/TCP, локальная политика,
NAT-профиль и обязательные метаданные REALITY. IPv6 link-local требует netif_index.
Новый сокет обслуживает NCD-транспорты вне FIN_WAIT; случайный выбор одного адреса не используется.
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
свежая подписанная запись принимается целиком, старая уступает уже известной.
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
ID узла проверяется по public_key. Ошибка запуска возвращает NULL handle и не снимает
прежний FIN_WAIT. Отдельно принятый snapshot и уже добавленные линки при частичной
ошибке могут сохраниться; новый handle публикуется после подготовки ресурсов.
Явный specific_sock задаёт локальную политику, сохраняя проверки family/протокола/цели/REALITY;
автоматическое появление сокета использует обычную политику подбора.
LAN discovery предоставляет отдельные адреса подключения и не меняет подписанный снимок.
Открытое объявление рабочих сокетов и ответ на проверку имеют Ed25519-подпись;
публичные ключи в этих пакетах отсутствуют. Получатель использует известный ключ
из registry/SQLite, а неизвестное устройство остаётся только в списке discovery.
Обычные объявления имени, сообщения и ACK не подписаны и не подтверждают личность.
Общий UDP-порт 45731 принимает только объявления; unicast-обмен использует отдельные
ephemeral-порты. Формат и API описаны в `src/local_transport/protocol.h` и `local_transport.h`.
Подписываются весь заголовок local_transport (magic/version/type, длина, IDs/эпохи,
request=0) и тело: nonce(16), family(4/6), IP(16), количество сокетов,
затем socket_id/protocol/port(BE)/generation(BE) каждого сокета. IPv4 дополнен
нулевыми байтами. Подписанный IP обязан совпасть с источником UDP; рабочий порт
отдельный от discovery-порта. Публикуются принимающие IP/interface обычные сокеты,
без PRIVATE, probe и SOCKS UDP; до 32 endpoint на один IP, без фрагментации.
Объявление само не разрешает NCD использовать адрес. Только для узла без прямого UP
и живого BGP-пути во всех группах получатель отправляет случайный nonce;
подписанный ответ связывает его с обоими node_id и эпохами discovery. Ответ принимается
однократно в течение 3 секунд. Это проверка свежести без часов/NTP: старые подписи,
эпохи или случайные поколения сокетов сами по себе свежесть не доказывают.
После проверки адрес действует 30 секунд по монотонным часам и только на интерфейсе
приёма (включая IPv6 scope). Повтор объявления/ответа не продлевает срок без нового nonce.
Дополнительно должен оставаться живым discovery-endpoint: только обычный ANNOUNCE
продлевает его собственный TTL 30 секунд; MESSAGE/ACK и подписанный обмен этого не делают.
Смена discovery-эпохи очищает endpoints и кандидаты. Проверка offline выполняется
при обработке подписанного объявления/ответа; чтение кандидатов её не повторяет
и само по себе не удаляет ранее проверенные адреса при появлении рабочего пути.
Сверка интерфейсов и периодические объявления выполняются каждые 10 секунд,
очистка TTL — по секундному tick или при запросе списка устройств/отправке сообщения.
Отключение broadcasts сохраняет unicast-объявления и ответы, приём и сроки жизни;
полная остановка discovery закрывает сокеты, завершает pending-доставки и очищает кандидаты.
NCD использует эти кандидаты при проверке совместимости, открытии/обновлении
и появлении локального сокета. Если сохранены только ключи, идентичность загружается
из nodes без требования старого адреса. LAN не создаёт NCD handle автоматически.
Неподтверждённые ETCP-пробы очищаются после истечения кандидата/остановки discovery;
после успешного handshake линк живёт по обычным правилам транспорта. REALITY требует
метаданных из известного снимка; прямые LAN TCP-пробы при глобальном SOCKS выключены.
Диагностика: discovery для подписи/nonce/адресов, etcp для NCD-проб и их удаления.
Конфигурационные подключения UTUN используют один адаптер
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
+108
View File
@@ -0,0 +1,108 @@
Динамический пакетный драйвер ETCP
================================
Потребитель: lib/packet_link.h. Экземпляр API драйвера: lib/packet_link_backend.h.
Привязка к ETCP: src/transport_layer/etcp_link_io.h/c, etcp_connections.h.
Все операции выполняются в одном uasync-потоке. Платформенные callbacks драйвер
сам переводит в этот поток. Тип транспорта не определяет интерфейс доставки.
Использование
-------------
conn = etcp_connection_create(instance, "peer"); // пока без линков
etcp_connection_set_peer(conn, peer_x25519_key); // проверить результат
link = etcp_link_add(conn, &driver); // проверить результат
Драйвер создаёт вызывающий модуль. В packet_link_driver передаются ctx, ua,
диагностическое name и функции bind/get_state/send/set_rx_enabled/stop/destroy.
direction задаёт физическое направление (UNKNOWN/OUTGOING/INCOMING), копируется
при bind и доступно через packet_link_get_direction. Оно не задаёт роль handshake
верхнего протокола. name служит также подписью транспорта в диагностике;
по его содержимому не выбираются кодирование или способ доставки.
Один ctx обслуживает один двусторонний канал к пиру. Общий UDP-сокет или BLE-радио
может принадлежать отдельному владельцу и обслуживать несколько экземпляров.
Несколько разных драйверов добавляются в один conn отдельными вызовами link_add.
Если conn уже настроен NCD, повторно set_peer вызывать не нужно.
Наличие ключа ещё не доказывает владение им: ETCP проверяет защищённый handshake.
Обнаружение ключа неизвестного пира не является частью пакетного драйвера.
Владение и события
-----------------
packet_link_bind копирует API, name, callbacks и снимок состояния. bind драйвера
копирует таблицу обратных вызовов, заимствует arg и не выдаёт события синхронно.
Успех передаёт ctx оболочке; отказ сохраняет его у вызывающего, без новой подписки.
Один экземпляр допускает только одну привязку; повторный bind возвращает ошибку.
Все потенциально неуспешные выделения оболочки делаются до bind.
При отказе etcp_link_add соединение сохраняется, драйвер остаётся у вызывающего.
Первое уведомление состояния отложено. RX первоначально выключен; ETCP включает
его после возврата link_add, при OPEN. OPEN драйвера означает готовность канала,
а UP ETCP — результат своего handshake и подтверждения эпох сессии.
send принимает целый ll_entry вне очереди. OK передаёт владение entry и dgram.
Любой отказ оставляет их вызывающему. Частичного принятия нет. Успех означает
принятие ответственности, а не доставку удалённому приложению. Ошибка после
принятия сообщается состоянием TERMINATED. Подтверждения ETCP сохраняются.
max_packet — максимум байтов целого пакета на границе драйвера, неизменный на время
его жизни; в него входят защита и заголовки ETCP. ATT MTU сюда не подставляется:
драйвер GATT сам фрагментирует принятый пакет. ETCP ограничивает свой wire-пакет
1500 байтами и вычисляет полезную часть с учётом заголовков выбранного кодирования.
tx_ready сообщает о собственных ресурсах драйвера. Если собственной очереди нет,
используется публичная готовность следующего модуля. Чужие буферы не инспектируются.
WOULD_BLOCK не требует опроса: восстановление готовности даёт state_changed.
Проверка готовности не заменяет результат send.
Драйвер может синхронно сообщить изменение состояния из send; оболочка сразу
обновляет снимок, но откладывает/объединяет верхний state_changed. RX из send
запрещён. received передаёт владение пакетом получателю; при отключённом RX драйвер
сам ограничивает накопление и не выдаёт callback. Поле memlen содержит размер
доступного dgram, len — размер данных. Оболочка очередей данных не создаёт.
close сразу отменяет уведомления и вызывает stop. stop запрещает дальнейшие
события, но не освобождает ctx. Если платформенный API не позволяет закрытие
из callback, драйвер откладывает физическое отключение после отмены подписки.
destroy выполняется через call_soon после выхода обработчиков. До уничтожения
uasync закрыть линки и выполнить drain. После close handle недействителен.
TERMINATED необратим; CONNECTING→OPEN запускает handshake. Возврат внешнего канала
в CONNECTING снимает готовность ETCP; последующий OPEN возобновляет handshake.
Доставка и кодирование
----------------------
ETCP_LINK владеет привязкой io. packet_link из get заимствован и отдельно не закрывается.
Общий путь отправки/приёма вызывает кодирование через etcp_link_codec и доставку
через API экземпляра драйвера. Кодирование не выбирается по имени драйвера.
etcp_link_get_snapshot копирует описание линка в uasync без callbacks и ссылок
на ресурсы драйвера: подпись, физическое направление, состояние ETCP, IP-адреса
и имя сокета, если они есть. Внешний пакетный канал имеет has_ip=0, имя/направление
берутся из packet_link. IP-путь сохраняет подписи UDP/TCP/TCP+REALITY и свои адреса.
Desktop MEMBER_DETAIL и Android JNI JSON сериализуют один и тот же снимок.
etcp_link_add использует защищённые датаграммы ETCP без IP-заголовков. Ключ пира
уже задан в conn. Приём привязан к каналу, поэтому адресный bootstrap, NAT и поиск
сокета не нужны. INIT и согласование эпох используют существующие форматы ETCP.
Существующий etcp_link_new — фабрика IP-путей: подготавливает UDP/STCP-драйвер
и подключает его через тот же packet_link_bind. Адреса, listener, reconnect,
SOCKS и REALITY остаются в IP-привязке. Прежние wire-форматы сохранены:
UDP получает зашифрованную датаграмму, STCP — plaintext для собственного framing.
Неизвестный UDP endpoint проходит прежний bootstrap; известный — callback драйвера.
UDP сначала пробует sendto; EAGAIN сохраняет одну датаграмму. writable сокета
возобновляет передачу; SOCKS ожидает relay ready без EPOLLOUT-цикла.
STCP разрешает один ожидающий пакет в TX адаптера. Нижний STCP извлекает следующий
после освобождения собственной досылки. RX сохраняет прежние лимиты/паузы TCP.
DATA, ACK и измерительные серии проверяют готовность. ACK и служебные признаки
фиксируются после принятого send; при отказе остаются для повторной отправки.
Проверки и диагностика
---------------------
test_packet_link: владение, TX blocked/resume, объединённые события, RX pause,
закрытие из callbacks, отмена событий и отсутствие утечек.
test_etcp_packet_link: UDP EAGAIN, два пути общего сокета, ACK rollback/commit.
test_etcp_driver: пустые conn, отказ bind без потери владения, копирование API,
два ядра без сокетов, защищённый INIT/DATA, сообщения 1500 байт, backpressure,
независимые драйверы и освобождение ресурсов.
Существующие проверки UDP/STCP/session сохраняют прежние ожидания.
sys=debug показывает состояние packet_link, etcp=debug — привязку/протокол.
+23 -2
View File
@@ -1,4 +1,4 @@
Работа лички
Работа лички — требования и реализация
1. отдели логику лички от логики групповых чатов. не смешивай.
@@ -9,4 +9,25 @@
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
для доставки медиа поднимаем линк (conn_mgr).
и посмотри реализацию медиа для групповых чатов.
Контракты реализации проверены 27.10.2026: src/dm/ и doc/dm_arch.md.
«Напрямую» означает доставку endpoint через живой маршрут общей CHAT-группы,
в том числе с BGP-транзитом; физический линк между A и B не обязателен.
Беседа имеет собственные историю/seq/outbox и не создаёт отдельную TOPO_GROUP.
Общие с групповыми вложениями примитивы: media_async, шифрование потоков,
attachment_info, voice_file; DM не использует групповой HAVE_BLOCK и его хранилище.
S выбирается по подписанному adm_tags supernode=yes, M — storage=yes в общей группе A/B.
Роли независимы и могут совмещаться на одном узле. A сохраняет исходящую задачу
до подписанной квитанции B; STORED от S/M не означает доставку получателю.
Текстовая квитанция подтверждает commit сообщения, файловая — проверку ciphertext,
публикацию файла и сохранение результата. S повторяет удаление у M до DELETE_ACK
либо завершения подтверждённого срока назначения; сохраняются служебные tombstones.
На M настройки чата dm_media_ttl_sec=604800 и dm_media_storage_mb=1024 задают TTL
в секундах и квоту в МиБ. Повтор STORE не продлевает TTL; при нехватке квоты новое
размещение отклоняется, подтверждённая копия не вытесняется до срока.
Текстовые pending не имеют TTL. Истечение копии M не удаляет оригинал A и не мешает
B получить его после возвращения A; локальное удаление истории не отзывает копии S/M.
Подробные wire-форматы, жизненный цикл и проверки: doc/dm_arch.md и src/dm/*.h.
+19 -2
View File
@@ -40,9 +40,21 @@ ID получателя, выбранную группу и исходную о
Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только
соответствующий `request_id`; уже отправленное сообщение не отзывается.
Отмена и остановка DM завершают принятый запрос ровно один раз.
Отмена уже завершённого request_id ничего не отправляет и не создаёт повторный результат.
Отмена касается ожидания PM, уже зарегистрированный JOIN-ключ живёт по своему TTL.
`CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт:
`[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра.
Запрос — один обнулённый блок `u_calloc(dm_invite_req)` с NUL-строками;
после передачи в uasync им владеет обработчик до финального callback.
Instance, chat_sync и SQLite должны оставаться живыми до завершения отмены.
`dm_core_destroy` отменяет ожидающий invite до освобождения owner.
Поколение беседы захватывается перед регистрацией и проверяется перед `dm_send`:
удалённая и повторно открытая PM не принимает старый результат.
Создание/открытие беседы происходит до регистрации; ошибка регистрации или отправки
не откатывает это открытие, но не создаёт invite-сообщение.
`CHAT_EVT_DM_INVITE_RESULT` содержит 36 байт:
`[request_id:u64][result:i32][peer:u64][conv_id:u64][generation:u64]` в порядке байтов ядра.
Поколение читается при завершении запроса и позволяет сопоставить результат с состоянием беседы.
Desktop и Android сопоставляют результат с запросом и получателем.
Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки.
@@ -52,6 +64,11 @@ JOIN во вторую группу может использовать уже
## Проверка и диагностика
Контракт сверён 27.10.2026 с DM, регистрацией JOIN-ключей и обработчиками результата
Qt/Android. Свежий `test_dm_invite` прошёл вместе с остальными восемью DM-тестами.
Приведённые ниже GUI-тесты описывают существующее покрытие; в этом этапе новые
сборки Qt/Android и интерактивная проверка интерфейса не выполнялись.
- `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN,
репликация участника и отправка группового сообщения после вступления;
отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM;
+214 -13
View File
@@ -22,17 +22,47 @@ utun_instance_destroy(inst);
```
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
в конфигурации. Desktop и Android GUI по умолчанию запускают ядро и чат.
`[gui] utun_enabled=yes` явно добавляет UTUN-сервис; `[global] tun_enabled=yes`
управляет только созданием TUN внутри включённого UTUN. Отсутствующий GUI-флаг означает no.
Без TUN и NAT запуск чата не требует прав администратора.
Desktop подтверждает start после готовности всех сервисов и медиапривязок, перед входом в uasync poll;
ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
UTUN/чата откатывает созданные им ресурсы; ошибка core start оставляет подготовленные
ресурсы для retry/destroy. `utun_instance_init()` сохраняет ранее запущенные сервисы
при отказе последующего. `utun_instance_stop()` снимает `running` и будит цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
Новый старт чата требует SQLite без внешней транзакции до подготовки db_sync/listeners.
chat_core всегда заимствует DB ядра. Схема,локальная идентичность и media_base
сохраняются под BEGIN IMMEDIATE;обе подписки member properties/routes устанавливаются
до commit и снимаются при отказе. Ошибка сохраняет DB/чужих подписчиков;no-ready events.
Далее готовятся сохранённые каналы. Отказ активации оставляет committed метаданные,
а chat_service_stop убирает частичный runtime;повтор start повторяет активацию.
MY_NODE_ID/DB_READY публикуются после обеих фаз chat_core,без активного SQL cursor.
Это готовность chat_core,а не подтверждение запуска всех следующих частей чат-сервиса.
Тестовые Alice/Bob/etc в production accounts больше не создаются;существующие записи сохраняются.
Desktop и Android проверяют `instance->running` вместе со своим флагом остановки,
включая Android-цикл после restart. Запрос из callback завершает цикл после возврата
из текущего poll; уже начатая обработка остальных callbacks этого poll не прерывается.
Владелец затем снимает media-привязки, уничтожает ядро и освобождает uasync.
Это используется также при отказе rollback подготовки вложения, приёма REQUEST, решения автора или ответа о ключе, если SQL-транзакция
осталась открыта: модуль вложений предварительно прекращает приём и снимает свой таймер/router.
Create уже подготавливает listeners, топологию/SQLite, router/proxy и мониторинг;
core/UTUN/chat запускаются отдельно. `create_from_str` использует временный файл
в `/tmp`, updater и parse. `create_from_config` передаёт владение config только
при успехе; при отказе config остаётся caller, но node_id мог быть исправлен.
Logger общий для процесса, его изменения не откатываются при отказе создания.
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
```ini
[gui]
utun_enabled=yes
[global]
tun_enabled=yes
tun_ifname=vibechat
@@ -63,14 +93,48 @@ ConnectivityManager до запроса запуска и передаёт ко
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
по успешному созданию потока; после события передаётся свежий снимок адресов.
Android `instance_lite` владеет joinable worker с момента `pthread_create`, включая фазу до появления
`UTUN_INSTANCE`. Все выходы worker проходят через общую очистку; завершившийся поток собирается при stop
или перед следующим start. Stop снимает готовность, будит живой uasync и ждёт cleanup/join без detach;
ожидание дольше двух секунд даёт предупреждение, ресурсы сохраняются до завершения. Controller API
из callback worker отклоняется до захвата блокировки. Только успешный join разрешает освобождение
recorder, C callbacks и JNI global refs; ошибка возвращается frontend без освобождения этих ресурсов.
JNI init регистрирует callbacks/recorder; SQLite открывает только ядро. `ChatRepository` получает данные
через JNI, предварительное создание `chats.db` в init не требуется.
`NativeLib.stop/destroy` снимает media-владельцев на main и ждёт native worker на IO. `HeadlessService`
передаёт остановку в общий `CoreShutdown`, который переживает onDestroy; новый Service ждёт его до init.
Отказ stop повторяется автоматически с backoff 250 мс..30 с, без нового Service/request.
Отмена одного ожидающего Service не отменяет очистку. AudioRecorderManager запрашивает cleanup вне main,
удерживает AudioRecord/AudioTrack и decoder handles до подтверждённого release, включая ViewModel.onCleared.
NativeLib не разрушает recorder до завершения этих владельцев; во время teardown новый capture запрещён.
Подготовка звонка также ждёт voice cleanup, а отмена звонка не отменяет это ожидание.
Диагностика worker не считает живые ресурсы
frontend утечками: recorder освобождается bridge после остановки ядра.
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, сокеты, NCD и постоянные транспорты `[client]`, маршрутизатор, репликация, время, мониторинг |
| UTUN | Группа UTUN, независимые запросы участия к настроенным пирам, DATA binding, TUN, системные маршруты, NAT transport |
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
`[client]` открывает транспорт без вступления в UTUN. CHAT согласуется только при наличии
членства в соответствующем канале. Входящий JOIN отсутствующей группы отклоняется.
UTUN stop закрывает свои групповые запросы, но сохраняет постоянные транспорты ядра.
Запись настроенного пира владеет копией параметров и адресов; DOWN/TIMEOUT сохраняет NCD,
окончательный CLOSED запускает повтор через секунду. Удаление записи отменяет её повтор
и освобождает только её handles. Ошибка подготовки нового списка сохраняет прежний список.
Изменение автосокетов переприменяет сохранённые endpoints также для bootstrap без подписанного NODEINFO.
Полный reload сохраняет набор запущенных сервисов и running; частичный обновляет `[client]` также без UTUN.
Restart нужен при смене негорячих global-настроек (в том числе db_path, control,
TUN/NAT/proxy), подсетей либо server-конфига в automatic режиме. В ручном режиме
сравниваются все параметры endpoint и IPv6 scope; неизменённый port=0 сохраняет
порт ОС. TCP endpoint закрывается вместе с listener и своей TCP-моделью.
Ошибка подготовки сохраняет прежний instance; ошибка создания после полного destroy
возвращает NULL. Частичный reload не атомарен: отказ нового endpoint логируется,
новый config сохраняется для повторной попытки. Caller всегда использует результат reload.
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
@@ -81,9 +145,34 @@ TCP client/server владеют только своими bindings, listeners
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
Команды JOIN из Qt/JNI используют удерживаемый `chat_join_port`: frontend копирует ссылку,
а состояние чата читает только callback в uasync. Close закрывает приём под mutex, отменяет
ожидающие команды и отделяет порт от instance. Ссылка frontend остаётся безопасной после stop;
новый запуск публикует новый порт. Команды старого запуска не обращаются к новому ядру.
При stop чата порт и активная попытка закрываются до ожидания media workers. В принятой команде
есть отдельный request_id интерфейса; сетевой JOIN сопоставляется по peer/group/attempt_id/phase.
Результат публикуется после очистки состояния и собственного NCD handle, поэтому callback может
начать следующий JOIN. Отмена ожидания не откатывает уже сохранённое удалённое членство.
Кеш каналов/пиров chat_sync публикуется полным снимком; ошибка чтения или памяти сохраняет
предыдущий кеш. Обязательные подписки и первый таймер проверяются при запуске. Удаление
канала снимает его waiters, предложения и активный JOIN до уведомления об отмене.
При выборе invite все кандидаты берутся из полной таблицы участников и требуют живого пути
(self также должен иметь полную запись); ошибки SQLite не подменяются выбором self.
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Caller завершает заимствованные SQLite statements/backups до destroy. Ошибка close
логируется; close_v2 при BUSY откладывает освобождение БД до завершения этих объектов.
UASYNC живёт до конца destroy. Отменённые heap-таймеры могут оставаться в нём
до следующего poll; это не разрешает callback обращаться к освобождённому instance.
Headless chat/call/radio bind задаёт IPv4 literal:port, decimal 0..65535;
port=0 выбирает ОС. Ошибка bind/listener не останавливает чат и не открывает
порт по умолчанию. Stop снимает клиентов с UASYNC, отменяет cleanup/mix-таймеры,
освобождает headless-контексты и кодеки. PCM после 11-байтового заголовка
копируется в выровненный буфер перед Opus.
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
@@ -107,6 +196,21 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
завершает ранее запрошенное мягкое закрытие.
NCD возвращает новый handle после подготовки подписки на все события ETCP и нужных
таймеров. До возврата open новый владелец событий не получает; уже работающий
транспорт доставляет начальный UP через call_soon. Отказ подписки, таймера или
планирования UP возвращает ошибку и сохраняет прежних владельцев и FIN_WAIT.
TIMEOUT не закрывает удерживаемый транспорт: возможен поздний UP, новое open может
начать новый бюджет подключения. CLOSED отменяет таймеры, отсоединяет conn и требует
закрыть оставшийся handle; его можно закрыть после уничтожения ядра.
Операции открытия, обновления линков и закрытия удерживают NCD/ETCP через вложенные
callbacks, включая синхронный callback исходящей очереди при отправке CLOSE.
Более новый вложенный переход прекращает оставшуюся рассылку прежнего события.
Управляющий пакет NCD занимает ровно 10 байт: cmd=0x12, subcmd и node_id little-endian.
Приём проверяет длину, команду и принадлежность отправителя тому же транспорту.
Отказы ресурсов, вложенные события, обновление snapshot и удаление соседних линков
проверяет `test_ncd_faults`; готовый handle, TIMEOUT и сетевой CLOSE/KEEP_ALIVE — `test_node_conn_direct`.
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
@@ -120,6 +224,10 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
Whisper хранит модель и одну активную задачу отдельно для каждого экземпляра.
Запрос загружает модель в worker,повтор после отказа разрешён;при занятости приходит BUSY.
До ожидания workers `chat_whisper_destroy()` отсоединяет владельца и запрещает публикацию
результата. Занятая модель освобождается в completion после join;глобального триггера нет.
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
@@ -142,11 +250,25 @@ completion. Отказ запуска/неверные аргументы даю
`encrypted_file_write/read` только готовят закрытый durable temp; worker публикует
его одним rename, completion подтверждает результат в БД/UI. Отмена до completion
не публикует сообщение; cleanup учитывает реально созданные/заменённые файлы.
Модуль канальных вложений отдельно владеет списком до16 файлов: worker либо
завершение, ожидающее SQL commit. Захват decrypting фиксируется до worker;
ошибка записи результата сохраняет задачу для следующего tick без новой криптографии.
Quiesce останавливает эти повторы. После media_async_destroy chat_attachment_destroy
освобождает оставшиеся завершения до удаления chat_core; внешняя транзакция
откладывает запись, незавершённый rollback запрашивает штатную остановку владельцем.
Ошибка синхронизации каталога после rename означает установленное имя без подтверждённой
durability; отложенный discard уже не удаляет результат. Общей транзакции ФС/БД нет.
До запуска workers и после их завершения recovery очищает собственные stale temp
в `media/decrypted`; общий file_io-реестр сохраняет активные temp всех instance.
Две подписки backfill устанавливаются вместе: отказ любой отменяет частичный запуск
и возвращает ошибку chat_service_start. JSON медиа читается единым способом для live,
manual, startup и кеша; manifest проверяется целиком до использования блоков.
Очистка сирот и удаление по квоте требуют полного снимка ссылок: ошибка SQL/памяти
отменяет удаление. Один путь учитывается один раз; расшифрованные производные файлы
также входят в квоту. Локальные отметки сообщений изменяются отдельным проверяемым
commit; played задаётся через JSON без ограничения прежним буфером 256 байт.
`media_delivery_init()` вызывается после подготовки chat core и media_index, пока
общая БД и группы живы; подписка на роли обязательна. Репликация хранит отдельный
контекст и CM handle для каждой пары «CHAT-группа–пир». DOWN отменяет текущий RPC,
@@ -220,9 +342,17 @@ UI, ожидающие команды camera handler и каждая camera sess
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
после camera close и освобождения outputs всех созданных sessions.
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
Capture создаётся пассивно: сервис сохраняет ссылку до start. Stop запрещает attach и снимает своё владение target;
true возвращается после camera/session ACK, encoder/input Surface, ориентации, microphone subscription и join Opus worker.
До этого false оставляет engine у сервиса. Ошибки освобождения повторяются независимо; camera HandlerThread живёт до полного cleanup.
Таймаут camera close только логирует задержку SDK: encoder/Surface остаются до подтверждённого закрытия камеры.
Opus worker сохраняет свой handle до успешного close, также после ошибки discontinuity/recreate.
Отказ настройки preview до SDK submission или onConfigureFailed возвращает encoder-only session.
Исключение createCaptureSession сохраняет outputs до callback сессии или закрытия устройства; допускается восстановление прошлого режима.
Foreground-сервис сохраняет engine, CPU lock, токены и foreground/stopSelf до подтверждения каждого cleanup API, включая onDestroy.
FIN следует после capture.stop=true; нормальная остановка ждёт native END до 4 секунд, abort/destroy не ждут доставки.
Handler повторяет ошибки без переноса ближайшего retry при повторном shutdown. Publisher barrier запрещает native teardown
также при незавершённом foreground handshake. Старый notification token не останавливает новый session ID.
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
@@ -237,7 +367,7 @@ VIDEO_END/AUDIO_END следуют за последними пакетами с
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
ядро только уведомляет его. Surface освобождается после join video worker.
ядро только уведомляет его. Surface освобождается после join video worker и подтверждённого decoder release.
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
@@ -313,6 +443,16 @@ Fatal ошибка audio worker передаётся в GUI с session ID и з
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
Заимствованная регистрация `call_set_audio_ops` принадлежит `UTUN_INSTANCE`; stop/start чата её сохраняет.
`call_audio_init/destroy` выполняются в uasync: init допустим до старта чата, повтор того же владельца
сохраняет эпоху и занятые TX-слоты. Перед освобождением ядра frontend останавливает I/O и снимает привязку;
codec/jitter освобождаются отдельно через `call_audio_stop/release` после остановки устройства.
Android с `UTUN_WITH_VOICE` регистрирует voice в worker до SERVICE_STARTED и снимает привязку перед destroy
при stop/restart и ошибке частичного запуска. Main вызывает только start DSP; без живой привязки start отклоняется.
NativeLib перед stop/restart сбрасывает CallController на main и требует подтверждённую очистку CallService,
а также завершение mediaPreparation. Отказ stop API не разрешает разрушить используемое ядро;
Handler остаётся доступен для повторной очистки, следующий stop/restart повторяет проверку.
Диагностика и проверки исправления — разделы18–19 `doc/call_audit_findings.txt`.
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
@@ -380,8 +520,57 @@ GUI не уничтожает живой QProcess. При отказе новы
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
wake lock и сервис. Ошибочные операции сохраняют владельцев и повторяются независимо
с задержкой 250 мс..30 с; process-ссылка удерживает их также после onDestroy.
Успешный Deferred от shutdown подтверждает stop API audio/video и очистку receiver,
wake lock, токенов, foreground/stopSelf. Сервис сохраняет пассивный video engine до его start.
Video stop возвращает true после RX join/освобождения decoder, playout и Surface,
SDK-подтверждений camera/session, освобождения Image/readers/encoder и native close.
Пока TX выполняет cleanup, false оставляет engine у сервиса и Deferred незавершённым.
Ошибки RX не прерывают независимую очистку TX; native port остаётся до подтверждения обоих.
onConfigureFailed означает закрытую сессию без обязательного onClosed. При исключении
создания сессии выходы сохраняются до callback сессии или подтверждения закрытия камеры.
Codec и input Images с отказавшим release сохраняются также при ошибке конструктора/probe;
их отдельный retry использует только Android-ресурсы и не обращается к native-ядру.
RX stop ограничивает join 250 мс за попытку; ошибка release сохраняет decoder,
его Surface не освобождается до codec. Прочие Surface и playout очищаются независимо.
Process-ссылка и Handler повторяют cleanup после выхода worker; поздние Surface также
удерживаются до успешного callback release. Исключение callback требует повторяемого
освобождения у передавшего владельца. Неудачный RX stop запрещает закрывать native port.
Отказ поста или join оставляет stop повторяемым. Отмена Deferred не снимает владельцев.
Audio stop ждёт каждый worker до 250 мс за попытку; playback включает join reporter.
Main не ждёт device locks, занятые SDK-вызовами workers: пропущенные stop/pause
логируются, а живые workers сохраняются для следующей попытки. Маршрут и замер AEC
повторяются на main после освобождения lock только для прежнего активного call ID.
Живые workers и устройства с отказавшим release остаются у движка, native audio stop
откладывается до завершения потоков, освобождения устройств, communication listener,
router и восстановления собственного аудиорежима. Незавершённая очистка бросает ошибку сервису;
новый start сначала обязан завершить прежний stop. Проверка состояния/настройка нового
AudioRecord/AudioTrack выполняется после сохранения владельца для rollback.
Router сохраняет callback/proxy до успешного unregister/close, собственные глобальные
заявки communication device/SCO — до подтверждённого снятия. Отказ одной операции не
прерывает другие. Process-ссылка и main Handler повторяют очистку через 250 мс..30 с;
отказ post сохраняет владельца для явного stop/start. Поздние Bluetooth proxy после stop
получают тот же контракт освобождения, не меняя новый активный маршрут. До доставки
callback принятый getProfileProxy не даёт router объект, который можно закрыть.
Прямое превью использует отдельные идемпотентные ссылки UI/команды/camera session
на один Surface/texture. Последний release передаёт физическую очистку process-владельцу;
ошибки Surface и texture повторяются независимо без пропуска другой операции.
Предложенная listener ссылка отменяется при исключении, queued receiver игнорирует
released target. Закрытие preview не допускает последующей активации через update.
Viewer трансляции хранит decoder/DSP/signal до успешного cleanup; ошибки освобождения
повторяются его audio worker независимо, Surface callbacks выполняются после подтверждения всех владельцев.
GAP сохраняет следующий KEY из того же batch. Ошибка reset не допускает новой шкалы поверх старого DSP.
Общий mixer сохраняет AudioTrack при ошибке initialize/play/close и не открывает следующий выход до ACK.
Join viewer/video/mixer перед звонком ограничен 250 мс за попытку; незавершённая очистка запрещает передачу ресурсов.
Перед native teardown все viewers получают stop, затем проверяются их реестр и завершение общего mixer worker.
Routing listeners после неудавшегося attach/detach сохраняются отдельно от активных outputs
до подтверждённого удаления; поздние callbacks не обращаются к released track.
Общая MediaAudioSession при close блокирует новых потребителей и оставляет executor
до подтверждённой очистки hardware/router; отказ допускает повторный close.
Перед `NativeLib.stop/restart/destroy` проверяется подтверждение очистки сервиса и
окончание подготовки чужого media; ошибка блокирует native stop. Новый звонок
нового ядра разрешается после `SERVICE_STARTED`.
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
@@ -395,6 +584,11 @@ ACTIVE и выключают Android standby. Состояние не истек
сигналинг и звонковое аудио. Foreground-сервис резервируется сразу из видимой Activity.
Отмена во время освобождения не запускает устаревший звонок.
До окончания звонка повторное включение рации/трансляции заблокировано.
До подтверждённой очистки сервиса и Telecom Connection blocksMedia сохраняется также после ENDED карточки.
Telecom реестр не теряет отвергнутые/удалённые connections при ошибке destroy; Handler хранит владельцев и повторяет освобождение.
Отказ markDisconnected не отменяет независимый destroy; подтверждённый destroy завершает владение.
Восстановление рации ждёт Deferred shutdown, завершения mediaPreparation и awaitReleased Telecom.
Ожидание не отменяет SDK cleanup. Новый service/core stop проверяет тот же Telecom barrier на main.
После закрытия звонкового аудио и Telecom Connection ранее включённая рация
возвращается в прежний канал; удержания PTT сброшены. Удалённый канал и остановленное
ядро не восстанавливаются. Публикация и просмотр трансляции автоматически не возобновляются.
@@ -407,3 +601,10 @@ standby даже при начальном `client_activity=STANDBY`. Keepalive
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
Activity/peer-sleep подписки уникальны по fn+arg и вызываются синхронно в обратном
порядке добавления. Remove себя/соседа безопасен: память записи освобождается
после внешней рассылки. Новые подписки могут получить вложенное событие;
более новая эффективная activity прерывает остаток прежней рассылки.
Destroy/reload из этих callbacks запрещён. `test_instance_contract` проверяет
вложенные подписки, отказы ресурсов, manual/full reload, TCP listener и headless stop/start.
+224 -40
View File
@@ -1,5 +1,25 @@
Модуль стриминга. Техническое задание, этап 1.
Статус аудита 26.10.2026: сверены все 18 заголовков src/stream с полными
реализациями (15 C и playback_audio.cpp). Уточнены API, поля, владение,
сроки и результаты операций; последние правки заголовков не меняют C-токены.
Настоящий документ сверён в части общего ядра stream; платформенные разделы
desktop/Android и подробности внешнего сетевого стенда остаются частично проверенными.
Расхождение допуска deleted устранено: stream_member_load читает принятый флаг
в том же SELECT, что и профиль, и возвращает STREAM_ERR_NOT_MEMBER до crypto.
Штатный test_stream_member проверяет подписи/версии, source/supplier/self,
закрытие handles/выдач, изоляцию каналов и повторное разрешение участия.
Полный аудит платформенных разделов ещё не завершён. Подтверждения, архитектура
исправления и результаты проверок — doc/stream_audit_findings.txt.
Дефект publisher_block устранён: помещающийся Opus уходит в отдельную аудиопачку,
ожидающее видео получает следующий слот actor. Штатный test_stream_publisher
проверяет 23 сценария границ, продвижения треков и OPENING/FIN; раздел 7 отчёта.
Дефект expire_controls устранён: INFO_REQ9 сопоставляется по probe_id, просроченная
проба закрывается с OVERLOAD. Штатные проверки TTL и реальной ETCP-очереди пройдены;
полный check201/201 выполнен в отдельном IPv6 namespace — раздел9 отчёта.
Финальная сверка заголовков: сборка и check201/201, proxy9/9, burst/load PASS;
C-токены12 доработанных заголовков совпадают с базой — раздел10 отчёта.
1. Цель и принятые решения
Модуль размещается в src/stream. Узлы получают поток данных от источника через других подписчиков группы,
@@ -12,7 +32,7 @@
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
- Передаём произвольные данные как есть, без обработки форматов видео, кадров и ключевых точек.
- Транспортная часть передаёт произвольные данные как есть. Необязательный media-слой поверх неё разбирает uAV2 и точки KEY.
- Размер части автоматически уменьшается по длине пачки, максимум по умолчанию 1300 байт.
Побайтовый источник сбрасывает остаток через 100 мс; publish_block публикует сразу.
- На каждый локальный экземпляр трансляции — кольцевой буфер 32 МБ для всех её субпотоков вместе.
@@ -112,7 +132,7 @@ Handle получателя, поколение stream-сессии, измен
Невосстановимый пропуск обозначается событием GAP; нельзя молча склеивать данные по обе стороны разрыва или выдавать нули за исходные байты.
Окончательно пропускаем пачку, когда её данных уже нет в окне либо все нужные выдачи прошли её индекс/сообщили пропуск,
а k блоков не собрано. Получение более поздней пачки от одного поставщика само по себе не закрывает ожидание остальных.
Модуль не интерпретирует содержимое и не занимается видео, кодированием, воспроизведением или поиском ключевых кадров.
RS/сетевой слой не интерпретирует содержимое. Media store, publisher и playback — отдельные компоненты поверх него.
4. Подпись источника и timestamp
@@ -241,13 +261,19 @@ ACCEPT содержит handle, фактический начальный инд
Реальное получение блока, а не только ACCEPT, подтверждает пригодность выдачи.
Индекс сразу после последнего доступного блока разрешён: курсор ждёт его появления в том же кольце.
Более далёкий будущий индекс отвергается. Пустой источник допускает выдачу с 0/с конца,
которая ждёт первый блок; первый проверенный DATA по-прежнему устанавливает общий N.
которая ждёт первый блок; первая структурно допустимая DATA-часть по-прежнему устанавливает общий N.
Пустой ретранслятор пути не объявляет. Завершение без данных подтверждается подписанным END с last_seq=UINT64_MAX.
Общий N фиксирует первая структурно допустимая RS-часть, до проверки подписи всей пачки.
INFO/ACCEPT и счётчик принятых частей не являются подтверждением подписи; она проверяется при decode из k частей.
Отказы различают отсутствие подписки/трансляции, эпоху, FEC, позицию, слот, перегрузку, цикл и право доступа.
DATA содержит поколение сессии, handle, неизменяемую часть RS-пачки и изменяемый путь.
Источник подписывает и END с ключом трансляции и индексом последней пачки, чтобы ретранслятор не мог подделать окончание.
Нет продления 6 секунд — отправитель закрывает выдачу и сообщает CLOSED, при необходимости через маршрутизатор группы.
INFO/connect ограничен 5 секундами; готовая INFO-only проба остаётся до явного close.
Control FIFO удаляет сообщения старше 4 секунд. Просроченные INFO_REQ (9 байтов), SUBSCRIBE и ACCEPT
закрывают соответствующую незавершённую выдачу с OVERLOAD; INFO_REQ дополнительно сопоставляется по nonce.
После первого DATA отдельного таймера тишины receive нет: восстановление выполняет selector либо ручной владелец.
Аренда, пробы и timers используют монотонное локальное время. Таймаут аренды очищает ресурсы;
реакция на отсутствие прогресса начинается раньше по метрикам потока.
Потеря отдельного ETCP-линка не закрывает выдачи, пока NCD остаётся UP.
@@ -259,7 +285,7 @@ DOWN закрывает выдачи соответствующей сессии
Начальный запуск:
1) По опциям мемберов и BGP выбрать первый подходящий онлайн-узел, включая сам источник.
2) Запросить один субпоток с N=UINT64_MAX.
3) Получить и проверить первый блок, зафиксировать его source_epoch и batch_seq=N.
3) Принять структурно допустимую часть и зафиксировать batch_seq=N; source_epoch уже выбран по профилю источника.
4) У остальных поставщиков запросить субпотоки этой же эпохи с конкретного N.
5) Собрать достаточный набор блоков одной пачки, затем продолжать непрерывное получение.
@@ -270,7 +296,7 @@ DOWN закрывает выдачи соответствующей сессии
Обычный перенос одного субпотока:
1) Выбрать первую ещё нужную пачку N и поставщика, способного её выдать.
2) Создать новую выдачу, продолжая старую; временное перекрытие также занимает исходящий слот нового поставщика.
3) Получить блоки и подтвердить продвижение нового поставщика.
3) Получить структурно допустимые части и подтвердить продвижение нового поставщика до курсора старого.
4) Закрыть старую выдачу. При отказе новой сохранить старую и отменить неудачную попытку.
Дедупликация — по (stream key, batch_seq, subblock), независимо от handles.
@@ -309,7 +335,9 @@ DATA несёт фактически пройденные узлы; началь
Базу сбрасываем при отмеченном источником разрыве времени, неоднозначной дельте, новой выдаче/эпохе или разрыве последовательности.
Резкое изменение позиции выдачи не считается доказательством улучшения задержки.
Ожидаемый межблочный интервал и rate источника оцениваются по окну его timestamp и объёмам блоков.
Rate источника оценивается по timestamp и объёмам пар последовательных блоков за 60 секунд.
Ожидаемый интервал сначала равен 100 мс; после ненулевой суммы source_delta текущей секундной корзины
используется её среднее по интервалам, не меньше 1 мс. Первый/rebase-блок не даёт парных bytes/time для rate.
Если несколько блоков имеют один timestamp, агрегируем объём до ненулевой дельты, не делим на ноль.
Не используем время формирования всех n блоков одной пачки как n независимых временных интервалов.
При отсутствии новых данных продолжаем измерять отставание от ожидаемого следующего прихода:
@@ -348,8 +376,9 @@ INFO_RESP также сообщает число свободных исходя
Неуспех имеет backoff; одинаковые активные пробы одного пира для разных экземпляров переиспользуются.
Временные NCD handles невыбранных кандидатов освобождаются; рабочие экземпляры удерживают свои handles.
Используются route_connectivity, sock_match/NCD и метрики существующих соединений; второй адресный probe-движок не создаётся.
Прямая доступность — UNKNOWN/REACHABLE/UNREACHABLE с временем замера, а не вечный boolean.
Используются node_conn_direct_compatible и sock_match/NCD; stream не запускает собственные route_connectivity-пробы.
Совместимость означает возможность direct/reverse-попытки по известным сокетам/адресам либо уже работающий транспорт.
Фактическую доступность и RTT подтверждает INFO-обмен, снимки и неуспехи имеют ограниченный срок/backoff.
После проверки доступного субпотока/индекса и отсутствия цикла минимизируем интегральную оценку в миллисекундах:
score_ms = RTT_цепочки_до_поставщика + 2 * RTT_до_поставщика + 3 * hop_count + problem_penalty_ms
@@ -396,21 +425,26 @@ A — поступившая нагрузка, D — передано в ETCP, X
в интервале без смены выдач g = max(0, (A-D)/elapsed_seconds).
Rate выдачи — её средний вклад в поступающую нагрузку, а не урезанная скорость выхода из перегруженного канала.
Для разгрузки нужно снять суммарный rate больше g, чтобы очередь начала убывать.
Число выдач выбирается по их rates: закрываем достаточно выдач для требуемого сокращения;
при сильном росте это сразу несколько, не фиксированная одна и не случайное постоянное число.
Число выдач оценивается по их rates, но закрывать можно только избыточные части сверх k.
Без безопасных избыточных частей запрос завершается без принудительных закрытий. Возраст сам по себе не разрешает пропуск:
backpressure сохраняет cursor, GAP возникает при фактическом вытеснении/утрате диапазона.
OVERLOAD содержит request_id, расчётное число выдач, требуемое сокращение rate и список handles с фиксированными rates.
Получатель выбирает handles с учётом FEC и альтернативных поставщиков и отправляет OVERLOAD_SELECT.
Получатель выбирает handles только при сохранении k различных работающих частей и отправляет OVERLOAD_SELECT.
Альтернатива должна уже получать DATA с действительными метриками без остановки; ожидающий INFO не является заменой.
Одновременный выбор для других перегруженных пиров тоже исключается из доступных альтернатив.
Отправитель исполняет выбор, только пока актуален исходный request_id, и сообщает CLOSED.
Получатель не отсоединяет выбранные handles до CLOSED: поздний SELECT после принудительного закрытия не режет дополнительные выдачи.
При разных rates одного количества закрытий недостаточно: суммарный снятый rate должен покрыть требование.
Получатель не отсоединяет выбранные handles до CLOSED; поздний SELECT после завершения запроса не меняет выдачи.
При разных rates снятая нагрузка считается по сумме rates; безопасность FEC важнее полного исполнения требования.
Rate закрываемых выдач фиксируется по оценке на момент запроса, чтобы условие исполнения не менялось на каждом тике.
Срок реакции — 2 секунды с первого запроса; повторы не сбрасывают срок и не начинают новое требование.
Новые выдачи, компенсирующие запрошенное сокращение, до разгрузки не принимаются.
Если требование не выполнено, отправитель сам закрывает достаточный остаток выдач и сообщает handles/причину.
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку, а при равенстве допустима случайность.
Если очередь опустела, запрос завершается сразу. По истечении 2 секунд отправитель завершает запрос без слепых закрытий.
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку.
При глобальном отставании сокращение выбирается по всем пирам модуля.
Память и возраст очереди остаются ограниченными во время ожидания; просроченные задания удаляются с GAP/EXPIRED.
Возраст ожидающих DATA используется для диагностики перегрузки, но не разрешает пропуск данных.
Backpressure сохраняет курсоры и непрерывную цепочку кадров. GAP обозначает фактическое вытеснение/утрату диапазона.
Принятые ETCP пакеты и подписки сохраняются. Завершённый источник дочитывается до подписанного END.
13. Статусы через опции мембера и Merkle
@@ -502,7 +536,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
- Перекрытие поставщиков без дублирования потребителю, rollback, одновременное восстановление нескольких субпотоков.
- Цикл по устаревшим метрикам, одновременная перестройка и выдача сохранённых путей: DATA не циркулирует.
- Глитч короче подтверждения, устойчивое отставание 500 мс, нулевой выход и локальная перегрузка uasync.
- Выбор числа закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и закрытие остатка через 2 секунды.
- Выбор безопасных закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и сохранение k после 2 секунд.
- 32 совместимых кандидата на старте, далее 1/с, маленький/большой список и запрет повторного полного цикла раньше 30 с.
- Merkle-опции до/после BGP-события, офлайн-опция не кандидат, поздний участник, перезапуск и сохранение других полей userinfo.
- Мобильный источник/получатель и исключение мобильных из фоновых проб ретрансляторов.
@@ -537,8 +571,12 @@ Stop/destroy выполняются по правилам ядра, вне callb
stream.c/h — контекст в UTUN_INSTANCE, локальные handles публикации/подписки, общий экземпляр и буфер.
DATA/GAP/END/CLOSED доставляются отложенно через один callback экземпляра ядра;
callback может закрывать handles, заимствованные данные живут до его возврата.
DATA/GAP/END используют общую позицию сессии и не повторяются для каждого raw handle;
медиакурсоры отдельных handles независимы. Последний owner закрывает сессию;
после FIN оставшиеся owners удерживают кольцо, publisher close без FIN отсоединяет их.
stream_member.c/h — канонические опции, проверка подписанного профиля/ключа и членства,
online-кандидаты только из живых BGP-путей этой CHAT-группы; мобильный источник допускается, relay исключается.
online-кандидаты только из живых BGP-путей этой CHAT-группы; мобильный источник допускается,
мобильный промежуточный поставщик требует подписанную роль supernode.
chat_core_set_my_profile_options — атомарная замена опций с сохранением имени, аватарки и остальных userinfo.
При старте снимаются оставшиеся после аварии собственные опции; смена эпохи/ключа или утрата членства отменяют подписку.
Удаление группы и остановка CHAT отменяют таймеры/события, отсоединяют handles и освобождают буферы;
@@ -557,7 +595,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
stream_network.c/h — собственный NCD owner пира, общий для его потоков/групп; ожидание его UP,
прямые SUBSCRIBE/ACCEPT/REJECT/DATA/GAP/RENEW/UNSUBSCRIBE/CLOSED/END, routed REVERSE_REQ/CLOSED.
Новые handles назначаются при постановке SUBSCRIBE в общую FIFO; watermark защищает от закрытых/старых запросов.
Первый проверенный DATA с конца устанавливает общий N; прочие latest-выдачи ждут его и запрашивают конкретный N.
Первая структурно допустимая DATA-часть с конца устанавливает общий N; прочие latest-выдачи запрашивают конкретный N.
Повторный stream_receive_restart отменяет upstream старой попытки, оставляя кольцо/downstream;
старые receive handles отсоединяются и требуют close. Уже сохранённый первый блок также устанавливает новый старт.
DATA копируется из кольца только перед отправкой; пути сохраняются по каждому блоку и проверяются на LOOP.
@@ -584,7 +622,8 @@ Stop/destroy выполняются по правилам ядра, вне callb
Кольцо сообщает только фактические вставки/вытеснения; повторный блок не создаёт A.
Списки субпотоков по времени вставки дают точный возраст ещё ожидающих блоков, в том числе вне порядка batch_seq.
Логическая DATA-очередь представлена курсорами и счетчиками: содержимое и история остаются в одном 32-МБ кольце.
Частные/общие метрики lag/rates/late/stops подключены к подписанным DATA; после END ожидание прекращается.
Метрики lag/rates/late/stops обновляются при приёме структурно допустимых частей, до проверки общей подписи;
они описывают доставку поставщика и не подтверждают содержимое источника. После END ожидание прекращается.
INFO содержит nonce пробы и 64-битный снимок метрик; RTT — uint32 мс с признаком неизвестного.
Условие перегрузки: ожидающая нагрузка старше 200мс и рост либо нулевой D;
непрерывное подтверждение — 500мс по монотонным часам, независимо от количества тиков.
@@ -592,13 +631,14 @@ Stop/destroy выполняются по правилам ядра, вне callb
Rates фиксируются при создании OVERLOAD. Расчётное число — подсказка, исполнение проверяется по суммарному rate.
Получатель предпочитает закрытия, сохраняющие k с учётом других поставщиков, затем parity при равных условиях.
OVERLOAD_SELECT предлагает handles; source закрывает только актуальное требование, поздний/повторный выбор безопасен.
Срок 2с начинается с первого запроса и не меняется при повторе. Невыполненный остаток source закрывает сам.
Срок 2с начинается с первого запроса и не меняется при повторе. С 25.10 остаток без безопасного SELECT не закрывается.
Новые SUBSCRIBE не компенсируют разгрузку: запрет действует до исполнения и 500мс снятого давления;
stream_peer_get_load.accepting сообщает этот статус отдельно от request_active.
Глобальный бюджет включается только для backlog нескольких пиров вместе с CPU/опозданием callbacks;
CPU отдельно не отключает выдачи, уже запрошенное сокращение повторно не учитывается.
Control FIFO ограничена 128 кодограммами по 1094 байта, возрастом 4с и отменой неподтверждённых запросов.
Просроченные ожидающие DATA при активном OVERLOAD учитываются X и пропускаются явным GAP, кольцо не стирается.
Удаление DATA только по возрасту отменено: оно разрезало KEY и вызывало постоянные resync при недостаточной полосе.
GAP сообщает фактическую потерю диапазона; после END ожидающие DATA дочитываются.
Диагностика сообщает A/D/X/Q, age, input/output/g, длительность интервала, cpu/late callbacks,
frozen/released rates, request/deadline и нижние очереди/inflight; D остаётся принятием ETCP, а не доставкой.
test_stream_core: баланс, X отдельно от D, граница 500мс, глитч, смена состава, 10-секундное окно и широкие wire-метрики.
@@ -627,7 +667,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
INFO сохраняет диапазон, фактический путь, metrics, RTT, свободные slots, accepting, queue, CPU и возраст прогресса.
RTT цепочки относится к этому пути; смена фактического пути DATA делает старое измерение неизвестным,
новая проба обновляет RTT соответствующей активной выдачи. Невыбранные снимки истекают через 60с.
Первый поставщик запрашивается с конца; остальные стартуют с его проверенного N, даже если k уже позволили выдать пачку.
Первый поставщик запрашивается с конца; остальные стартуют с N его первой принятой части, даже если пачка уже выдана.
Поиск учитывает доступную старую позицию, запрет LOOP и лимит r при наличии альтернатив.
Оценка поставщика в мс: RTT цепочки до поставщика + 2 * RTT до поставщика + 3 * hops + штраф проблем (0..1000 мс).
hops = path.count: путь INFO содержит автора/поставщика, последний переход к получателю добавляет один hop.
@@ -635,15 +675,19 @@ Stop/destroy выполняются по правилам ядра, вне callb
текущая остановка или lag > 500 мс сразу дают 1000 мс. Неизвестные метрики не дают штрафа, неизвестный RTT даёт UINT64_MAX.
Меньшая оценка лучше; при равенстве выбираются меньше общих узлов путей, затем меньший ID поставщика.
Более новый индекс повторно проверяется свежим INFO/SUBSCRIBE: старое объявление last не является вечным верхним пределом live-потока.
Обычно переносится один субпоток; новая выдача перекрывает старую и заменяет её только после проверенного DATA и догоняющего cursor.
Обычно переносится один субпоток; новая выдача заменяет старую после структурно допустимого DATA и догоняющего cursor.
По умолчанию: подтверждение улучшения 2с, удержание поставщика 5с, гистерезис оценки 20мс; параметры доступны через stream_selection_configure.
Перенос сравнивает ту же оценку по фактическим метрикам текущей выдачи и INFO альтернативы;
распределение для FEC допускает ухудшение оценки не более гистерезиса. Логи выбора/переноса показывают оценку и её составляющие.
Новая выдача без достаточного продвижения отменяется через 5с, старая остаётся. CLOSED/DOWN восстанавливают несколько отсутствующих субпотоков.
При отсутствии decode-прогресса 5с, недостатке работающих субпотоков и наличии более новой истории возможен явный restart с конца;
пропущенный диапазон передаётся потребителю GAP, кольцо/downstream сохраняются. Сам по себе простой источника restart не вызывает.
После 5с без полной проверенной пачки, если anchor уже есть либо запрошен media-индекс, выполняется recovery_request;
следующая попытка не раньше чем через 5с. Зависшие выдачи переоткрываются, consumer/media cursors сохраняются.
Для media переход к более новому полному KEY выполняется лишь при доказанной недоступности нужного диапазона;
тогда stream_local_restart сохраняет кольцо/downstream и передаёт пропущенный диапазон как GAP.
Тишина источника может вызвать перезапрос, но сама по себе не доказывает потерю данных и не требует скачка к голове.
stream_selection_get_info сообщает пробы/циклы, выдачи, перекрытия, поставщиков, миграции, отказы/restart и фактическую FEC-защиту.
Защита подтверждается всеми n проверенными работающими субпотоками и не более r на поставщика; при недостатке поставщиков помечается ослабленной.
Защита оценивается по всем n субпотокам с наблюдёнными частями/метриками без stopped и не более r на поставщика;
эта оценка доступности частей не заменяет проверку общей подписи. При недостатке поставщиков защита ослаблена.
Владельцы публикации/подписки удерживают ACTIVE; запрошенный STANDBY восстанавливается после закрытия последнего экземпляра.
Подписанный END проходит также для источника без единого DATA.
test_stream_core проверяет границы 32/1с/30с, новые записи, долгий/незавершённый обход и отсутствие повторного burst.
@@ -665,27 +709,31 @@ Stop/destroy выполняются по правилам ядра, вне callb
17. Зафиксированный сетевой формат и пределы реализации
Все stream-числа на wire — big-endian, без packed-структур и зависимости от host byte order.
Версия stream wire — 4; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=5): cmd:1, version:1, command:1, subblock:1,
Версия stream wire — 7; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=7): etcp_id=0x07:1, version:1, command:1, subblock:1,
group_id:8, source_node_id:8, source_epoch:8, sender_epoch:8, receiver_epoch:8, handle:8.
INFO_REQ/RESP согласуют sender/receiver epochs; receiver_epoch=0 используется INFO_REQ для обнаружения поколения и REVERSE_REQ.
Подписанный routed payload содержит тот же заголовок; его group_id сверяется с реальной группой router,
sender берётся из end-to-end router metadata. DATA через routed service отвергается.
INFO_REQ: probe_nonce:8, не резервирует номер SUBSCRIBE.
INFO_REQ: probe_nonce:8, want_media:1 (0/1), не резервирует номер SUBSCRIBE.
want_media=1 просит поставщика подготовить общий медиаиндекс; поиск KEY выполняется отложенно с ограниченным бюджетом.
INFO_RESP/ACCEPT: probe_nonce:8 (0 в ACCEPT), actual_seq:8, first_available:8, last_available:8, n:1, r:1, block_size:2,
metric_flags:1, lag_ms:8, source_rate:8, receive_rate:8, blocks:8, late_blocks:8, stops:8,
offer_flags:1, free_slots:2, cpu_basis_points:2, queue_bytes:8, progress_age_ms:8, path_rtt_ms:4,
path_count:1, node_ids:8*path_count — фиксированная часть 111 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
key_seq:8, key_last_seq:8, path_count:1, node_ids:8*path_count — фиксированная часть 127 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
metric_flags: bit0 — измерение известно, bit1 — source_rate известен, bit2 — receive_rate известен, bit3 — stopped.
offer_flags: bit0 — принимает новые выдачи, bit1 — RTT цепочки известен, bit2 — возраст прогресса известен,
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока. CPU — 0..10000 (сотые доли процента).
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока, bit5 — полный KEY сохранён для выдачи этой части.
При bit5=0 обе позиции KEY равны UINT64_MAX; при bit5=1 first_available <= key_seq <= key_last_seq <= last_available.
Каждая часть диапазона KEY проверяется на наличие; свежий незавершённый KEY не заменяет предыдущий полный.
CPU — 0..10000 (сотые доли процента).
Пустой источник: first/last=UINT64_MAX, bit4=0, INFO actual_seq=UINT64_MAX, ACCEPT actual_seq=0, путь {source}.
Курсор last+1 допустим только в ACCEPT непустого кольца; DATA/END проверяются по точной позиции и подписи источника.
SUBSCRIBE: requested_seq:8; REJECT: reason:1, для SUBSCRIBE дополнительно first_available:8,last_available:8,
для INFO дополнительно probe_nonce:8. Старый nonce не подтверждает новую пробу.
RENEW: один header handle без payload (каждая кодограмма продлевает один элемент списка).
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,неизменяемая часть RS-пачки переменной длины.
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,frame_timing:48,неизменяемая часть RS-пачки переменной длины.
GAP: old_cursor:8,next_cursor:8, уведомление включает диапазон [old_cursor,next_cursor).
END: "stream_end":10,group_id:8,source_node_id:8,source_epoch:8,last_seq:8,Ed25519:64 — всего 106 байт.
UINT64_MAX в END означает пустой источник; принимается только выдачей с next=0 и без ранее принятых DATA.
@@ -738,7 +786,7 @@ Restart в той же source_epoch сохраняет consumer cursor: bootstra
8. Live media и Android
Транспортный wire v5 не меняется. stream_publish_block публикует одну непустую FEC-пачку
Текущий транспортный wire — v7. stream_publish_block публикует одну непустую FEC-пачку
немедленно, с сохранением границ. Первая запись выбирает режим источника: byte write или block write;
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
@@ -791,7 +839,8 @@ stream_media проверяет весь блок до изменения view.
Desktop и Android открывают просмотр через stream_media_open: ядро проверяет выбранный source_epoch
и эпоху существующей session, создаёт подписку и cursor; при ошибке снимает только нового владельца.
Платформы сохраняют собственное планирование чтения и вывод; закрытие использует обычный stream_close.
Ретрансляторы не разбирают uAV2 и не пересоздают подписи источника.
Пересылка RS-частей не требует разбора uAV2 и не пересоздаёт подписи источника.
По INFO_REQ.want_media ретранслятор дополнительно готовит общий индекс проверенных пачек для предложения полного KEY.
Android:
- Один foreground publisher на процесс; каждый viewer имеет независимый session ID.
@@ -828,6 +877,12 @@ Android:
UI не сохраняет borrowed inst/ua. Idle без pending data не будит мобильное ядро.
- TX<=2MiB/128 records, не более 250ms ожидания. Actor публикует <=2 пачек за вызов,
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
Opus целиком занимает header32 + audio descriptor16 + payload; точное заполнение допустимо.
Без ожидающего видео резерв под video descriptor отсутствует. Обычная смешанная пачка
сохраняет место под описание и непустой фрагмент видео. Если первый Opus помещается только отдельно,
второй слот того же actor отдаётся видео без ожидания сборки неполной пачки.
feed=1 означает копирование в очередь. Actor после определения FEC-ёмкости при open удаляет Opus,
не помещающийся даже отдельно, с WARN (ID, PTS, bytes, max, capacity), не блокируя следующие записи.
При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY.
Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms.
- RX: stream_viewers держит отдельный handle/cursor, demand, status и ограниченный owned батч на session ID.
@@ -850,7 +905,11 @@ Android:
и отсоединения своего mixer input; detach ядра будит signal, не освобождая его.
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
Просроченный кадр при нормальной последовательности декодируется без показа.
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
- Первый декодированный кадр разрешает audio output; общий PCM-модуль desktop/Android перед первым звуком
и после underrun накапливает 250ms готового PCM. Это порог готового звука, без аппаратной задержки и lookahead DSP.
Во время накопления backend выдаёт silence с неподвижным исходным PTS; decoder references сохраняются.
Уже переданный устройству PCM продолжает воспроизводиться. До возобновления регулятор держит темп 1x;
затем избыток сокращается по обычной истории минимального запаса. EOF снимает порог и выпускает весь короткий хвост.
AudioTrack задаёт время показа видео по реально услышанному PCM;
без аудио используются монотонные часы с остановкой на границе готовых кадров.
Android StreamAudioOutput принадлежит StreamAudioMixer и хранит один недописанный общий PCM (до 480 stereo frames).
@@ -1036,7 +1095,7 @@ Desktop:
UI показывает video queue, суммарный inflight, send buffer и ACK_1s каждые 100ms.
Stop закрывает источники и audio callback, join обоих workers, затем FIN и ограниченный drain 2s.
Удаление engine происходит до SoundManager и ядра; detach отменяет posts/timers и закрывает publisher
в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются.
в uasync. Общий publisher использует те же uAV2 и текущий wire v7 на обеих платформах.
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну.
- transport/stream_bridge владеет stream_viewers: независимый cursor и ближайший батч на reader.
@@ -1072,7 +1131,8 @@ Desktop:
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
Фиксированного prebuffer нет: показ следует физическим audio callback timestamps;
Звук начинается после накопления 250ms готового PCM; после underrun применяется тот же порог без сброса decoder/PTS.
EOF выпускает хвост любой длины. Показ следует физическим audio callback timestamps;
без звука — монотонным часам с ограничением готовым PTS. END до KEY завершает пустой reader.
Короткие пропуски аудио заполняются Opus PLC; большой разрыв/pressure сбрасывает timeline.
Закрытие окна снимает только reader, собственная публикация сохраняется.
@@ -1185,7 +1245,8 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
- Декодированный PCM пополняется до 250ms при наличии данных; фиксированного ожидания перед стартом нет.
- Декодированный PCM пополняется до 250ms при наличии данных; перед стартом и после underrun consumer ждёт этот порог.
При EOF остаток выпускается без порога. Во время накопления PTS удерживается silence span; Opus/SoundTouch не сбрасываются.
Это запас отдельного audio worker, размер аппаратного буфера не увеличивается. Audio lookahead — device lead + 500ms,
достаточный для 250ms PCM даже при x1.6. SPSC имеет 128 слотов по 960 stereo frames с собственными PTS spans.
Mixer/callback не вызывает Opus/SoundTouch; Android переиспользует PCM-массив каждого источника.
@@ -1215,7 +1276,7 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
Сам старт публикации не запускает приём на суперузлах. Новая подписка сразу запускает INFO-пробы и отдельный
дедлайн 100 мс. Если пригодного ответа ещё нет, запрашивается один суперузел. Первый валидный INFO с разрешённой
выдачей выигрывает: порядок приёма фиксируется локальным счётчиком, независимо от RTT и порядка списка кандидатов.
DATA сначала открывается только у победителя; проверенный первый блок фиксирует общий старт всех остальных субпотоков.
DATA сначала открывается только у победителя; первая структурно допустимая часть фиксирует общий старт остальных субпотоков.
Другие INFO-пробы продолжаются: после фиксации старта недостающие части можно брать у любых подходящих поставщиков,
даже до первого восстановления FEC. Победитель гонки не обязан иметь все k частей. Ненужная помощь отменяется.
Пустой доступный источник не требует помощи, пока он не публикует данные.
@@ -1293,3 +1354,126 @@ callback обнаружения поставщика без секундной
раздачу нескольким зрителям, общий owner с локальным viewer, остановку при уходе последнего,
поздний повтор запроса до/после удаления tombstone, CANCEL до REQUEST, истечение READY без SUBSCRIBE,
откат неуспешного добавления viewer, исключение relay-only кандидатов и передачу пустого подписанного END.
Диагностика доставки и индикатор буфера
[debug] video=debug включает две сводки с периодом не меньше 500 мс и фактическим dt_ms.
stream NET — одна строка на соседа для всех его трансляций и субпотоков вместе:
- TX: sent/drop/gap за интервал, remain до головы локального кольца, возраст очереди и скорость её роста.
- ETCP: очередь до нормализации/отправки, уникальные unacked DATA, физический flight и сумма окон линков,
уникальные acked_B, принятые транспортом повторы и число физических RTO за интервал; очередь пересборки RX.
Все поля ETCP относятся к общему соединению с пиром, включая другие сервисы. unacked и flight не складываются.
errors_total — суммы счётчиков send/recv/crypto существующих линков; удаление/сброс линка меняет базу этих сумм.
- PATH: рабочие UDP/TCP, stalled, заполненные окна для первого ожидающего DATA, pacing, RTT/jitter/RTO и причина ожидания.
- RX: received/gap/dup/expired за интервал, время без DATA, максимальное относительное отставание и число stopped.
worst=group/source/epoch/sub обозначает самый отставший измеренный субпоток. connecting — все ещё открывающиеся выдачи пира.
Счётчики блоков/байтов записаны как blocks/bytes. sent означает принятие ETCP, не ACK.
na — нет измерения. После смены эпохи changed=1, транспортные дельты na; следующая сводка имеет новую базу.
ack_idle_ms начинается только после реального логического ACK, не после отправки/keepalive.
Метрика lag относительна исходной базе timestamp, не является абсолютной задержкой от источника.
stream PLAY — одна строка на плеер/источник: TIME (позиция, непрерывная готовая граница, запас, минимум, guard,
темп и причина решения), VIDEO (показ/отброс за интервал, PTS показанного/переданного поверхности кадра,
av_ms=video PTS минус media clock, очередь и ожидание KEY), AUDIO (PCM, устройство, underruns/тишина за интервал).
Длительности — мс, tempo=1000 означает 1x. generation/epoch отделяют новые временные шкалы.
GAP, перегрузка, смена поставщика и restart выводятся отдельными событиями. Подробности кодеков/кадров/GUI — video=trace.
Kotlin videoDebug/videoTrace используют native-фильтр и общий log hook, включая файловый/UDP вывод.
Индикатор публикации обновляется раз в 500 мс независимо от уровня логирования:
Android — 96x48dp слева от Start/Stop в шапке, desktop — рядом с этой кнопкой над preview.
Горизонталь [first, head] показывает сохранённое окно, где head — следующий seq после последнего блока.
Красная/жёлтая/зелёная трети обозначают близость к вытеснению/середину окна/близость к голове.
Линия объединяет субпотоки одного получателя и отмечает худшую и лучшую позиции передачи в ETCP.
Показываются до 16 наиболее отставших получателей, порядок при равенстве — ID. Позиции вне окна прижимаются к краям.
Индикатор отражает чтение кольца для отправки; неподтверждённые транспортные данные учитываются отдельно в inflight.
Восстановление доставки и старт с полного KEY — 25.10.2026
Отправитель не пропускает очередь по времени: 1-секундный catchup удалён. Возраст и рост очереди остаются метриками.
Последняя полная проверенная RS-пачка обновляет progress_ms общей сессии. Приход отдельных частей/дубликатов,
INFO/RENEW и уже объявленные GAP не являются таким прогрессом. Отсутствие полной пачки 5с запускает перезапрос, в том числе до первого DATA;
следующая попытка не раньше чем через 5с. Сохраняются сессия, принятые части, consumer/media cursors и decoder references.
Переоткрываются лишь зависшие выдачи, свежие INFO обновляют диапазоны и точки KEY поставщиков.
Счётчик selection.restarts означает попытки перезапроса, а не сбросы медиабуфера.
INFO и ACCEPT сообщают last_available и [key_seq,key_last_seq] последнего полного сохранённого KEY.
Медиаиндекс обновляет KEY только после всех фрагментов; позднее создание индекса ищет начало назад с бюджетом
и индексирует вперёд. Снимок использует уже известную точку, без повторного сканирования всего 32-МБ кольца.
INFO_REQ.want_media позволяет реле подготовить индекс без локального плеера; частичный relay не объявляет непроверенный KEY.
Начальная media-подписка выбирает KEY из INFO и открывает части параллельно с одной явной позиции.
Обычный непрозрачный stream сохраняет прежний старт с последней доступной пачки.
При восстановлении сначала запрашивается текущая недостающая пачка. Если свежие диапазоны/отказы поставщиков
и локальные части не позволяют собрать k, выбирается полный доступный KEY не ниже уже пройденной границы.
Отправитель не заменяет согласованную позицию самостоятельно; вытеснение между INFO и SUBSCRIBE возвращает EXPIRED.
Данные старых handles не примешиваются к новой попытке. Пропущенный диапазон выдаётся одним явным GAP;
новая цепочка начинает декодирование с KEY, уже выданные DATA/PTS не повторяются.
Desktop/Android не закрывают соединение по отдельному 10-секундному ожиданию KEY: отсутствие DATA обслуживает ядро.
Ошибка декодера, смена эпохи/членства, остановка сервиса и подписанный END сохраняют собственную обработку.
Новый KEY может занимать несколько пачек; продвижение этих пачек предотвращает ложный перезапрос во время сборки кадра.
Автор публикует все n частей вместе: один INFO разрешает параллельный выбор, каждое открытие проходит собственный INFO/ACCEPT.
Реле проверяются отдельно для каждой части. Подтверждённая поломка уже работавшего транспорта допускает повтор через 250мс.
Диагностика video:
- stream source offers ready — first/last, key/key_last, valid;
- stream recovery request — epoch, недостающий next, idle_ms, число переоткрытых выдач, preserve_media=1;
- stream recovery resumed — продвижение после попытки без сброса медиа;
- stream recovery KEY — пройденная граница, выбранный KEY и причина недоступности прежнего диапазона;
- stream GAP — точные first/next и направление; stream NET/PLAY — сводки каждые >=500мс.
- stream BUFFER — худшая цепочка, сумма её буферов, число устаревших/недоступных/неизвестных узлов, максимум возраста раз в секунду.
Буферы цепочки воспроизведения (raw protocol v7):
TELEMETRY=18 идёт через общий raw ETCP отдельной служебной кодограммой. В очередь видео DATA он не ставится:
перед очередной DATA pump сначала отправляет просроченную/обновлённую телеметрию. Снимок строится в момент отправки,
не копится в control FIFO; при backpressure сохраняется только dirty-флаг. Уже принятые ETCP данные он не обгоняет.
Источник и relay обновляют собственные измерения каждые 200ms; получение нового upstream сразу будит downstream.
Если upstream не обновлялся 400ms, его значения сохраняются с STALE. Подтверждённый разрыв помечает последний переход DOWN,
предыдущую часть цепочки — STALE. Отсутствовавшее измерение отличается от известного нулевого буфера.
Payload TELEMETRY: count:1, count записей по 33 байта:
node_id:8, buffered_us:8, sample:8, frames:4, age_ms:4, flags:1. Все числа big-endian.
Флаги KNOWN=1, STALE=2, DOWN=4. Путь начинается источником, заканчивается отправителем и не содержит повторов/получателя.
sample — монотонный номер измерения узла; возраст увеличивается при каждом хранении/пересылке, без синхронизации часов.
Потеря выдачи не удаляет последний снимок из общего кольца; новая эпоха получает отдельное кольцо.
Подсказка frame_timing в DATA содержит first_frame:8,last_frame:8,first_pts:8,last_pts:8,period_us:4,
started:4,audio_us:4,flags:1,reserved:3. Флаги: valid=1,video=2,continuation=4; reserved=0.
Она формируется из uAV2 у источника и неизменно пересылается relay. Это диагностическая подсказка непосредственного пира,
а не часть подписанного RS-тела: на проверку подписи, выбор частей и декодирование видео она не влияет.
Считаются только части >= cursor конкретной выдачи, ещё не переданные в ETCP. История кольца и другие получатели не прибавляются.
Начала кадров плюс одно продолжение в начале очереди дают число уникальных кадров, независимо от FEC и дробления большого KEY.
Длительность определяется их PTS и последним измеренным периодом кадра; при постоянном FPS соответствует frames/FPS.
До измерения периода первого кадра время неизвестно. Аудио считается по сэмплам 48kHz; параллельные треки не суммируются.
Источник также учитывает ещё не упакованную publisher-очередь, объединяя границы частично упакованного кадра без дублирования.
Для каждого viewer выбирается целый фактический входящий путь с максимальной суммой buffered_us.
Desktop/Android показывают две строки: «0.6S → 0.5S → 2S 1.1x», под каждым временем имя узла.
Имя длиннее восьми Unicode code points сокращается до первых восьми и одного символа «…».
Последняя колонка — собственный непрерывный запас до фактической playback-позиции, включая decoder/PCM/device;
перенос между локальными очередями не уменьшает запас повторно. Темп берётся из общего playback clock и виден только при ускорении.
STALE отмечается приглушением и «?», DOWN — приглушением и «×»; последнее известное время сохраняется.
Регрессии test_stream_service --recovery-only используют настоящий CHAT/ETCP, подписи и RS 8+0.
Медиатест создаёт 50 кадров/с по 6000 байт, KEY 55000 байт на нескольких пачках раз в 50 кадров.
Проверяются порядок, возрастающий PTS, длины и каждый байт после штатного media cursor; H.264-декодер здесь не запускается.
Сценарии: блокировка отправки 850мс и 2600мс без GAP; REINIT с задержкой stream-управления 100мс;
10с выдачи 60КБ/с без потери KEY; остановка 5600мс с одной попыткой после 5с; поздний viewer с общим KEY-стартом;
настоящее вытеснение 32-МБ source-кольца с переходом на KEY; зависание одной части при продолжающейся доставке семи других;
вытеснение KEY между INFO и SUBSCRIBE ещё до первого DATA с обновлением стартовой точки через 5с.
После коротких задержек сохраняются все кадры и состояние media; при фактической утрате диапазона требуется явный GAP.
test_stream_media отдельно проверяет пустой индекс, неполный KEY, сохранение предыдущего KEY, завершение, вытеснение и GAP.
Предыдущая проверка 25.10 до этого изменения: ./check.sh — 187/187 и proxy/burst/load;
Qt stream_view/audio/publish — 3/3, Android assembleDebug — PASS.
Предыдущий изолированный стенд с 16 узлами: baseline 10с, WAN 2Мбит/с + RTT 300мс + loss 5% на 10с,
восстановление, закрытие двух подписок и END — PASS. Результат: /tmp/stream-recovery-netns/result.json.
Историческая проверка реализации c3cf86b9 относилась к wire v6; затем телеметрия повысила версию до v7.
При обновлении нужны отправитель, получатели и задействованные реле одной версии; совместимости с v5/v6 нет.
Указанные ниже старые результаты desktop/Android получены из utun2 и не заменяют текущую платформенную проверку.
Проверка реализации c3cf86b9: ./check.sh — 187/187, proxy/burst/load — PASS; Android assembleDebug — PASS.
Девять сетевых регрессий входят в test_stream_service; лог полного прогона: /tmp/utun2-key-check-final-pass.log.
Известный нестабильный Qt audio-gap тест описан в doc/tasks.md; результаты отдельного Qt-прогона: /tmp/utun2-key-qt-isolated.log.
+523
View File
@@ -0,0 +1,523 @@
Аудит документации src/stream — 26.10.2026, заголовки завершены
Текущий статус: deleted исправлен и покрыт штатным test_stream_member; сборка,
профиль7/7 и check.sh195/195 PASS относятся к внедрению этого исправления (раздел 5).
При продолжении аудита подтверждена потеря помещающегося Opus в publisher_block (раздел 6).
Исправление внедрено: сборка и штатный test_stream_publisher23/23 PASS; после
объединения origin/master профиль8/8 и check.sh200/200 PASS (раздел 7).
Остальные частичные/непроверенные контракты каталога не закрыты. Разделы 1–4
описывают первоначальную диагностику и проверку архитектуры до внедрения deleted.
В продолжении полностью прочитаны stream.c и stream_optimizer.c, уточнены их
заголовки; теперь6[V]/12[~]/0[ ]. В частично прочитанном network подтверждён
дефект истечения INFO_REQ (раздел8). Исправление и штатные проверки выполнены:
TTL4/4, обе реальные backpressure-сцены и check201/201 в отдельном IPv6 namespace PASS (раздел9).
Текущий итог продолжения:18[V]/0[~]/0[ ] для заголовков src/stream; полностью
прочитаны все16 реализаций. Раздел10 описывает финальную сверку контрактов.
doc/stream.txt остаётся[~] из-за неполной проверки платформ desktop/Android
и внешнего многопроцессного стенда. Старые показатели выше и в разделах1–9 — история отдельных этапов.
1. Подтверждённый дефект: deleted не учитывается stream_member_load
src/stream/stream_member.c:166–167 выбирает строку только по node_id. Условие
source=0 проверяется ниже, но столбец deleted не выбирается и не проверяется.
Проверка дерева приглашений и подписи профиля не заменяет проверку блока владельца:
после принятого deleted=yes эти подписи участника остаются корректными.
Поэтому loader возвращает STREAM_OK и публикует прежние stream-опции/ключ.
Это расходится с допуском CHAT (source=0, deleted=0), закреплённым в topo_group.h,
и с результатом topo_node_sqlite_member_allowed. stream_member_supernode уже
отбирает deleted=0. Существующий тест deleted проверяет только supernode lookup.
Обработчик member_changed в stream.c:539–546 использует stream_member_load:
данный результат не запускает ветку инвалидирования STREAM_CLOSE_MEMBER.
Для исходной реализации полный сетевой сценарий продолжения выдачи после удаления не проверялся;
обход сетевой авторизации этим воспроизведением не заявляется.
Изолированная проверка
Настоящее ядро с SQLite :memory:, отдельные ключи канала и тестового участника.
Join/update/tree подписи создаются штатными функциями и принимаются member_sync_put.
В профиле корректные stream_source/subscriptions. Участник отличается от локального
узла; никаких удалённых узлов или пользовательской БД проверка не затрагивает.
Затем через member_sync_put применяется подписанный ключом владельца блок
{"ver":"1","deleted":"yes"}; прямой UPDATE deleted для воспроизведения не используется.
Фактический лог:
before load=0 allowed=1; after load=0 allowed=0 published=1; expected load=-9
Тест завершился exit=1 на несовпадении с ожидаемым STREAM_ERR_NOT_MEMBER.
Стенд: /tmp/stream-audit-member-deleted.c.
Сборка через существующие Makefile/LIBUTUN/COMMON_LIBS и дополнительную цель:
make -C tests -f Makefile -f /tmp/stream-audit-member.mk /tmp/stream-audit-member-deleted
Логи: /tmp/stream-audit-member-build.log, /tmp/stream-audit-member-deleted.log.
Runtime выполнен вне sandbox из-за ограничений сокетов uasync.
Предлагаемый вариант исправления
Добавить deleted в список полей этого же SELECT и до проверки профиля вернуть
STREAM_ERR_NOT_MEMBER для удалённого участника, с отдельным логом причины и ID.
Вторая SQL-проверка и новый API не требуются. Вариант WHERE deleted=0 тоже
отсечёт запись, но существующий лог "member absent" не отличит удаление от отсутствия.
Сохранить различие отсутствующего/удалённого участника и неполного профиля (WAIT),
атомарность state/public_key при отказе.
Регрессия должна проверить подписанное удаление источника/локального участника/
поставщика, закрытие соответствующих handles и отсутствие выдачи новых DATA.
После исправления продолжить сверку оставшихся заголовков и сетевых контрактов.
Реализация не исправлялась: аудит остановлен по правилу AGENTS.md:10.
2. Выполненная сверка
Все 18 заголовков src/stream прочитаны предварительно. Полностью прочитаны
10 реализаций: stream_block/source/buffer/media/media_buffer/wire/member/metrics/
load/playback_clock. Другие реализации stream, включая playback_audio.cpp,
network/optimizer/publisher/viewer и stream.c целиком, ещё не проверены.
В stream.c просмотрена только затронутая ветка member_changed; полный аудит
жизненного цикла сервиса этим не заявляется.
Завершены и готовы к [V]: stream_proto.h, stream_block.h, stream_source.h,
stream_media.h. Исправлены описание полного размера части и области подписи,
предусловия BE helpers, Ed25519 seed/ключ, владение и ошибки codec/source,
нулевой write/accepted, отсутствие END в source_finish, ограничения uAV2,
пересчёт first_frame_offset и атомарность views/выходов при ошибке.
В этих четырёх заголовках изменены комментарии/оформление, C-токены сохранены.
Частично проверены [~]: stream_buffer.h, stream_load.h, stream_media_buffer.h,
stream_member.h, stream_metrics.h, stream_playback_clock.h, stream_wire.h.
Их реализации прочитаны, но оставшиеся уточнения контрактов не внесены после
остановки. В stream_member.h добавлено предупреждение о подтверждённом дефекте.
Остальные семь заголовков остаются [ ]: stream.h, stream_network.h,
stream_optimizer.h, stream_playback_audio.h, stream_priv.h, stream_publisher.h,
stream_viewer.h. Прочтение одного заголовка без реализации не закрывает аудит.
doc/stream.txt остаётся [~]: просмотрены начальные разделы о FEC, подписи,
кольце и выдачах; последующие сетевые/media/desktop/Android разделы не сверены.
Добавлен статус аудита.
Полные .h/doc контракты всего модуля ещё не закрыты.
3. Проверки и ограничения
Сборка диагностического стенда PASS; отрицательная проверка deleted воспроизвела дефект.
Профильные наборы stream, полный check.sh, санитайзеры, Windows/FreeBSD и
GUI/Android runtime не запускались. Неизменность C-токенов заголовков и
git diff --check — PASS; успешные сетевые/медиа тесты не заявляются.
Предыдущие изменения Local_transport и чужие записи чек-листа сохранены.
4. Архитектурная проверка предлагаемого исправления — 26.10.2026
Вывод: проверка deleted в stream_member_load архитектурно корректна. На этом
этапе проверка выполнялась на временной копии модуля без изменений .c проекта;
внедрение описано в разделе 5. Статусы полного аудита каталога этим не закрываются.
Граница ответственности
Блок A участника описывает личность/stream-опции. Блок B владельца определяет
в том числе исключение из канала. Корректный A не отменяет запрет из B.
member_apply_record проверяет подпись B и его версию; owner_put обновляет
adm_tags/adm_tags_sig/storage/deleted одним UPDATE. После успешного commit
member_committed посылает props-событие; Merkle-page уведомляет после commit
всей страницы. Поэтому достаточно прочитать принятую проекцию deleted в том же
SELECT, не вводя вторую криптопроверку блока B или отдельный SQL-запрос допуска.
STREAM_ERR_NOT_MEMBER уже обработан в stream_open_check, каталоге источников,
member_changed/service_reconcile, authorized и network_membership_changed.
Событие источника/self немедленно ставит invalidated, затем reconcile закрывает
сессию. session_find/handle_active/session_deliver учитывают invalidated до
отложенного освобождения. Закрытие сохраняет владение handles у caller до close.
Для исключённого поставщика закрываются его flow, а локальная сессия источника
может продолжать получать от другого поставщика. Сфера запрета — конкретный
канал; общий ETCP/NCD и владельцы других каналов не должны закрываться.
Результаты реального стенда
В /tmp/stream-arch-member.c добавлены только поле deleted в существующий SELECT
и ветка NOT_MEMBER с причиной в логе до разбора профиля. Копия связана перед
штатной libutun.a, остальные модули взяты из существующей сборки.
Стенд использует fixture test_stream_service: четыре живых ядра, два канала,
подписанные owner/member записи, настоящий Merkle, UDP/TCP и NCD.
Все четыре группы проверок PASS:
- Неполный профиль/placeholder остаётся WAIT; отсутствие и deleted дают
NOT_MEMBER; state/public_key не меняются на отказе. Неверная подпись владельца
отвергается, старая версия не снимает запрет, более свежая разрешает участие.
- Исключение источника сразу закрывает receive с MEMBER и инвалидирует reader;
новых DATA callback нет. Запрет через Merkle останавливает сам publisher.
Handle другого канала и транспорт с дополнительным NCD-владельцем сохраняются.
- Исключение поставщика закрывает только его локальную выдачу; reader источника,
другой канал и upstream другого ядра в этот момент остаются активными.
- Исключение self закрывает handles данного канала и запрещает новые publish/
subscribe. Другой канал остаётся активен. После разрешения новый open работает,
прежние отсоединённые handles не оживают. Новая публикация получает новую эпоху.
Итоговый exit=0, teardown flows=0 и ноль оставшихся tracked allocations.
Сборка: /tmp/stream-arch.mk, /tmp/stream-arch-build-final.log.
Кейсы: /tmp/stream-arch-cases.h; harness: /tmp/stream-arch-service.c.
Результат: /tmp/stream-arch-results-final.log.
Запуск: /tmp/stream-arch-service --architecture-only, вне sandbox.
Первый прогон /tmp/stream-arch-results.log неверно пытался повторно использовать
publisher после распространённого через Merkle запрета: старый publisher уже
правильно закрыт. Ожидание стенда исправлено на новый запуск после разрешения,
сама временная проверка deleted при повторе не менялась.
Гарантии и ограничения
Уведомления/освобождение остаются отложенными; нужен обычный uasync loop.
Проверено отсутствие новых DATA callback у исключённого reader. Уже принятые
ETCP пакеты не отзываются, удалённые узлы узнают запрет при его доставке через
Merkle. Архитектурная проверка не обещает мгновенного глобального отзыва данных.
Полный check.sh, санитайзеры и платформенные/GUI runtime не запускались.
На этапе архитектурной проверки C-код репозитория не менялся; последующее
внедрение и постоянная регрессия описаны ниже.
5. Внедрение согласованного исправления — 26.10.2026
В stream_member_load существующий SELECT дополнен deleted. Для удалённой записи
до crypto возвращается STREAM_ERR_NOT_MEMBER, с отдельным DEBUG_WARN о запрете
владельца, group_id/member_id. Другого SQL-запроса, нового API или формата нет.
Остальные результаты loader, включая WAIT, и атомарность выходов сохранены.
Предупреждение о неисправленном дефекте в stream_member.h заменено действующим
контрактом. Задача deleted в doc/tasks.md закрыта, текущий статус stream.txt уточнён.
Постоянная регрессия — test_stream_member в check_PROGRAMS. Тест использует
test_stream_service.c с теми же четырьмя ядрами/подписанными записями/сетью,
а STREAM_MEMBER_TEST_ONLY выбирает изолированный сценарий в отдельном процессе.
Fixture не дублируется, её изменённые member-версии/активность не влияют на
обычный test_stream_service. Сценарий доступен также через --member-only.
Проверены подписи/версии владельца, WAIT/NOT_MEMBER и сохранность выходов,
удаление источника/поставщика/self, причины закрытия, отсутствие новых DATA
callback после запрета, распространение через Merkle до остановки publisher,
сохранность чужого NCD-владения и работоспособность другого канала после
исключения self. Разрешение не оживляет старые handles; новая публикация
проверяется на новую эпоху. Ожидание удалённой остановки зависит от состояния,
а не фиксированного sleep; teardown подтверждает отсутствие tracked allocations.
./build.sh --no-gui -j4 PASS: /tmp/stream-member-fix-build.log.
Профиль7/7 PASS: stream_member, stream_service, stream_core, stream_media,
stream_playback, stream_tx_stats, android_stream.
Сводка /tmp/stream-member-fix-profile-results.log,
подробности /tmp/stream-member-fix-test_*.log.
Последующий перенос длинной строки лога не меняет содержимое сообщения;
обе test-stream цели пересобраны, test_stream_member повторно PASS:
/tmp/stream-member-fix-test-rebuild.log, /tmp/stream-member-fix-test_stream_member-final.log.
Полный Linux check.sh:195/195 PASS,0 failed/0 skipped; proxy9/9,burst PASS,
load8/8,134217728 байт,167 секунд. Лог /tmp/stream-member-fix-full-check.log.
git diff --check PASS. Sanitizers и нативные Windows/FreeBSD/GUI/Android runtime
в этом этапе не запускались; test_android_stream — тест общего C bridge на Linux.
Отметки полного аудита заголовков не расширены: устранение одного дефекта
не завершает оставшуюся сверку src/stream и всего doc/stream.txt.
6. Продолжение аудита: помещающийся Opus не упаковывается — 26.10.2026
Полностью прочитаны stream_playback_audio.cpp, stream_viewer.c, stream_publisher.c
и их заголовки. Всего прочитаны13 реализаций; полные stream.c/network/optimizer
пока не сверены. До исправления документов трёх новых модулей подтверждён дефект,
поэтому publisher.h/viewer.h/playback_audio.h отмечены[~], не[V].
Теперь4 заголовка[V],10[~],4[ ]; doc/stream.txt[~] сохраняется.
Причина
В publisher_block (stream_publisher.c:145) перед каждым Opus проверяется
used + STREAM_MEDIA_AUDIO_SIZE + packet_length + STREAM_MEDIA_VIDEO_SIZE >= capacity.
Резерв video descriptor добавляется всегда, включая полностью пустую video-очередь.
В результате допустимая audio-only запись, которая целиком помещается в uAV2,
не попадает в block. При отсутствии видео publisher_block возвращает WAIT,
actor повторяет попытки и после MEDIA_AGE_US=250000 удаляет принятый пакет.
Штатный лог говорит audio expired, не объясняя блокировку из-за лишнего резерва.
Фактическое воспроизведение
Один настоящий локальный publisher, отдельный автоматически созданный канал,
SQLite в памяти, stream_subblocks=1/stream_rs_redundant=0. Ёмкость содержимого
FEC-пачки1125 байт. Видео в очереди нет. Codec Opus48kHz stereo/20мс:
штатный opus_codec_encode плюс opus_packet_pad; оба тестовых пакета дают
opus_packet_get_nb_samples=960 и действительно декодируются в960 frames/channel.
Самостоятельный stream_media_encode успешно помещает оба блока в ту же ёмкость.
После publisher feed принимаются настоящие RS-пачки, проверяется подпись,
содержимое декодируется штатным stream_media_parse в DATA callback.
packet=1044 valid_samples=960 media_bytes=1092 capacity=1125 feed=1 delivered=1 queued=0 state=LIVE
packet=1045 valid_samples=960 media_bytes=1093 capacity=1125 feed=1 delivered=0 queued=0 state=LIVE
В логе между этими строками: publisher audio expired pts=1020000.
Результат: expected=2 received=1, exit=1; после destroy нет оставшихся tracked allocations.
Для каждой проверки отсчитано500мс: больше TTL и периода обновления TX snapshot.
Это не переполнение TX, не испорченный Opus и не нехватка места в полной пачке.
Стенд: /tmp/stream-audit-audio-pack.c, дополнительная цель /tmp/stream-audit-audio-pack.mk.
Сборка через штатные Makefile/LIBUTUN/COMMON_LIBS/test_clock.o PASS:
/tmp/stream-audit-audio-pack-build.log. Полный лог /tmp/stream-audit-audio-pack.log.
Первый диагностический запуск остановился на неверной константе capacity1133
в стенде; она заменена вычислением границы из фактической ёмкости1125.
Реализация проекта при диагностике не менялась.
Предлагаемый вариант исправления
Упаковка должна учитывать фактически добавляемые описания/данные и допускать
audio-only пачку, если целый Opus в неё помещается. Резерв под видео не должен
навсегда блокировать принятую audio-запись. Простой вариант — заполнять audio
до его реальной ёмкости, затем видео в оставшееся место; нехватка места под видео
означает следующую пачку, а не потерю audio. Wire и неделимость Opus сохраняются.
Если важно гарантировать часть видео в каждой смешанной пачке, нужен явный
переход к отдельной audio-only пачке для такой головы очереди; это более сложный вариант.
Проверить1044/1045 байтов, максимальное audio-only содержимое1077 для FEC1+0,
точное заполнение capacity, очередь видео/пустую очередь и прежний default8+0.
Отдельно различать запись, которая вообще не помещается в доступную FEC-пачку,
и запись, не поместившуюся только в текущий остаток: диагностика должна показывать
packet_length, остаток/полную ёмкость и причину, не маскируя всё как expiry.
Реализация не исправлялась; дальнейший аудит остановлен по AGENTS.md:10.
В publisher.h добавлено предупреждение, задача открыта в doc/tasks.md.
Новый полный build/check, sanitizers и Qt/Android runtime в этом продолжении
не запускались; прежний check195/195 из раздела5 новый дефект не покрывает.
Оставшиеся уточнения новых заголовков (не выполнены после остановки):
- playback_audio: create/destroy lifetime, producer/consumer дисциплина всех getters,
потребление через needs_input, feed0/1/-1/-2, PLC до100мс, prepare после finish,
EOF в empty означает извлечённый PCM, а не проигранный аппаратурой звук.
- viewer: единый callback manager, notify без reentry, асинхронный stop до освобождения ID,
пределы demand/video<=records/audio_ahead, нулевой снимок status до открытия,
уничтожение manager требует остановки callers и живого instance.
- publisher: чтение настроек FEC при actor-open, snapshot смешивает локальные очереди
текущего источника и общие ETCP-счётчики ядра, PTS/revision/KEY admission,
ошибки/состояния и условное сохранение кольца локальным readers после FIN.
7. Исправление упаковки и продвижения треков — 26.10.2026
Принят локальный вариант без изменения wire, публичного API и неделимости Opus.
Без ожидающего видео рассчитывается только фактическая стоимость audio: header32,
descriptor16, payload. Условие допуска допускает равенство полной ёмкости.
Для обычной смешанной пачки сохранён резерв descriptor32 и хотя бы байта видео.
Если первая audio-запись помещается только отдельно, публикуется audio-only пачка.
Локальный video_turn отдаёт второй слот того же actor видео; coalescing20ms в этом
слоте не применяется. Между actor-вызовами состояние не добавлено; лимит2 пачки,
mutex, порядок PTS и смещения фрагментов сохраняются. Публикуемые записи расходуются
только после успешных encode/publish; очередь при ошибке закрывается прежним путём.
Opus длиннее capacity-header32-descriptor16 actor удаляет с WARN, содержащим ID,
PTS, bytes, max и capacity. Это окончательный отказ конкретной записи, не WAIT
до истечения TTL. Следующая запись продолжает обрабатываться. Проверка находится
в actor: во время OPENING producer ещё не знает выбранную FEC-ёмкость.
feed=1 означает копирование в ограниченную очередь; асинхронный отказ и TTL
оговорены в publisher.h и stream.txt. Удаление ошибочной записи не меняет video KEY.
DEBUG показывает успешную отдельную аудиопачку и отложенное видео, TRACE — состав
каждой опубликованной пачки; WARN сообщает точную причину невозможности упаковки.
Практический масштаб
Воспроизведение использует валидный Opus, искусственно увеличенный opus_packet_pad.
Обычный desktop/Android encoder настроен AUDIO128kbit/s/20ms; средний бюджет320 байт
не является пределом VBR. Потеря обычного естественно закодированного звука не
подтверждена. При default8+0 capacity9833 одиночный Opus<=1275 помещается.
Ёмкость1125 относится ко всем конфигурациям с n-r=1 при штатном RS-size, включая2/1.
Проверки
Временная копия и реальный publisher/RS/uAV2:21/21 PASS, без tracked leaks;
/tmp/stream-plan-check.log. Старый бинарник проваливает границу1045:
/tmp/stream-plan-baseline.log. Исходники проекта при этой проверке плана не менялись.
Штатный tests/test_stream_publisher.c и цель tests/Makefile.am:23/23 PASS.
Границы1044/1045/1077/1078/1275, точная пачка из двух audio-записей, раздельные
пачки, крупная audio-очередь32 записи, unfit в начале/середине/подряд, video-only,
mixed и продолжения видео, OPENING, Stop/FIN, n/r1/0,2/1,2/0,8/0, лимит8 Opus.
Callback проверяет фактические PTS/длины, декодирует Opus960 frames/channel,
проверяет каждый байт/offset видео. Все ожидаемые данные получены до TTL250ms.
На Linux wrap uasync_set_timeout запрещает publisher_media timer после крупного
Opus и короткого P-кадра: оба должны публиковаться в том же actor. Проверка не
сравнивает задержки процессора с порогом5ms. Cleanup каждого ядра восстанавливает
tracked allocation count. Другие платформы сохраняют проверки содержимого/порядка.
./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-build.log.
./tests/test_stream_publisher PASS: /tmp/stream-publisher-fix-regression.log.
Перед итоговой проверкой origin/master8e746195 объединён без конфликтов в ced9dc23.
Повторная ./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-merged-build.log.
Профиль8/8 PASS: stream_publisher,core,media,playback,tx_stats,android_stream,service,member;
/tmp/stream-publisher-fix-profile-results.log и /tmp/stream-publisher-fix-test_*.log.
Полный ./check.sh PASS:200 passed,0 failed,0 skipped; новый publisher-тест764ms.
Proxy9/9,burst PASS,load8/8,134217728 байт; полное время175с.
Лог /tmp/stream-publisher-fix-full-check.log. git diff --check PASS.
Sanitizers, Qt GUI и Android на устройстве в этом исправлении не запускались.
Отметки аудита4[V]/10[~]/4[ ] не расширяются исправлением кода.
8. Аудит stream.c/optimizer и просроченный INFO_REQ — 26.10.2026
Полностью прочитаны stream.c1249 строк и stream_optimizer.c712 строк; теперь
15 из16 реализаций прочитаны полностью. В stream_network.c прочитаны строки1–1615;
оставшаяся часть, перегрузка/relay и общая сверка network ещё не завершены.
Уточнены stream.h и stream_optimizer.h: общая сессия/позиция raw событий,
независимость media cursors, detached ownership, FIN, режим manual/automatic,
условия ACTIVE и mobile-supernode, параметры/оценки/сроки и recovery selector.
Публичные структуры переформатированы с сохранением полей/порядка/типов.
stream.h и optimizer.h отмечены[V]; network.h и priv.h остаются частичными[~].
Итог6[V]/12[~]/0[ ]; doc/stream.txt[~]. Все остальные отметки сохранены.
Подтверждённое расхождение
prepare_requests формирует uint8_t probe[9]: probe_id8 + media_flag1.
Raw dispatch тоже требует INFO_REQ length9. В expire_controls (stream_network.c:1147)
для поиска связанного flow ошибочно оставлена проверка m.length != 8.
Поэтому любой штатный INFO_REQ, просроченный в управляющей FIFO, удаляется из
очереди после4000мс, но соответствующий flow не закрывается с OVERLOAD.
Его info_sent остаётся1, info_ready=0, state=INFO. Повтор INFO не ставится.
По network_tick он ждёт общий deadline5000мс и получает TRANSPORT; selector
обрабатывает такой результат иначе, включая peer backoff. Полный цикл
NCD/ETCP/selector и величина последующего backoff в этом воспроизведении не измерялись.
Диагностика
Стенд /tmp/stream-audit-info-expiry.c включает настоящую реализацию network,
использует uasync/FIFO, control_put, stream_wire_parse и expire_controls.
Это изолированная проверка очереди/flow без запуска сетевых подключений:
время передаётся непосредственно как queued_ms + TTL, не моделируется потеря реального ETCP.
До/на TTL4000мс все записи сохраняются; после4001мс:
INFO_REQ9 media0: queue0/stateINFO/reason0, ожидалось CLOSED/OVERLOAD — FAIL.
INFO_REQ9 media1: queue0/stateINFO/reason0, ожидалось CLOSED/OVERLOAD — FAIL.
INFO_REQ9 с прежним probe_id: queue0/stateINFO — PASS, новую пробу закрывать нельзя.
SUBSCRIBE8: queue0/stateCLOSED/reasonOVERLOAD — PASS.
Итог2 failed из4, exit1, после cleanup tracked allocations восстановлены.
Сборка через штатный Makefile/LIBUTUN/COMMON_LIBS и дополнение
/tmp/stream-audit-info-expiry.mk PASS; лог /tmp/stream-audit-info-expiry-build.log.
Runtime: /tmp/stream-audit-info-expiry.log. Включены stream=debug, video=warn,
debug=info; видно удаление control и отсутствие закрытия INFO-flow.
Временная копия /tmp/stream-network-info-expiry-checked.c отличается только
сверкой length9 вместо8. На ней тот же стенд4/4 PASS, exit0, tracked allocations
восстановлены: /tmp/stream-audit-info-expiry-checked-build.log и ...-checked.log.
C/C++ реализации проекта не менялись; исправлены только описания.
Предлагаемое исправление
Сверять полный INFO_REQ9 (ID8 + media flag1), оставив проверку конкретного probe_id.
Просроченная проба должна закрываться существующим flow_close(OVERLOAD,notify0);
новые состояния, протокол или дополнительный таймер не нужны. Старый probe_id
не должен закрывать новую попытку. Закрепить обе media-формы, SUBSCRIBE8 и границы
TTL отдельным штатным regression; дополнительно удержать настоящую ETCP-очередь
дольше4с и проверить CLOSED/OVERLOAD до общего connect deadline5с.
Аудит остановлен по AGENTS.md:10 до изменения реализации. Задача добавлена
в doc/tasks.md; текущий статус отражён в stream.txt и внешнем чек-листе.
Полная сборка/check и sanitizers в этом продолжении не запускались; прежний
check200/200 относится к исправлению publisher и новый дефект не покрывает.
9. Исправление INFO expiry и проверка ETCP backpressure — 26.10.2026
expire_controls теперь сопоставляет полный INFO_REQ9 (ID8 + media1) с текущей
пробой. Проверка probe_id сохранена; истёкший запрос закрывает flow через прежний
flow_close(OVERLOAD,notify0). Wire, состояния и таймеры не менялись.
Существующий WARN дополнен bytes/age_ms/flow_closed: видно удаление конкретного
control и наличие связанного закрытия. Код исправления c1483512, тесты b116b20b.
Во время работы история перенесена поверх GUI-изменений; отсутствие различий
в src/lib/tests относительно собранного d89c1efa проверено отдельно.
Штатный test_stream_control_expiry включён в общий check и напрямую использует
реальные control_put/wire parser/expire_controls. Проверены границы3999/4000/4001мс:
INFO9 media0/1 после TTL закрывают flow с OVERLOAD, несовпадающий probe_id
не закрывает текущую попытку, SUBSCRIBE8 сохраняет прежнюю обработку.
4/4 PASS; отслеживаемые allocations после cleanup восстановлены.
В test_stream_service добавлена проверка на четырёх настоящих CHAT-ядрах с Merkle,
NCD и живым ETCP. Она выполняется в общем наборе и отдельно через --control-expiry-only.
После прогрева NCD/INFO callback настоящей send_input_q временно снимается.
Первый INFO остаётся в ETCP, второй — в control FIFO. Через4200мс в обоих media-режимах:
expired_sub0 state=CLOSED reason=OVERLOAD; held_sub1 state=INFO; publication/subscription
активны, исходное ETCP-соединение initialized/links_up. После возврата callback
приходит INFO_RESP; просроченная проба переоткрывается и получает новый ответ через
то же ETCP-соединение. Cleanup всех четырёх ядер восстанавливает tracked allocations.
В общем check этот же сценарий также прошёл внутри полного test_stream_service.
Проверки и условия среды
./build.sh --no-gui -j4 PASS: /tmp/stream-info-expiry-fix-build.log.
test_stream_control_expiry4/4 PASS: /tmp/stream-info-expiry-fix-unit.log.
test_stream_service --control-expiry-only PASS: /tmp/stream-info-expiry-fix-integration.log.
В изолированном сетевом прогоне включён stream=debug; unit включает video=warn,
поэтому проверен также новый WARN с bytes9/flow_closed1 и несовпадением ID/flow_closed0.
В fixture остаются логи отрицательных reachability-проб TCP; потоковые asserts,
готовность транспорта, восстановление и счётчики ресурсов проверены отдельно.
Первый ./check.sh:198 passed,3 failed,0 skipped. Все три отказа вызваны bind(::1):
EADDRNOTAVAIL в test_ncd_faults, test_ipv6_sockets, test_route_connectivity.
IPv6 в основном namespace отключён (all/default/lo=1), ::1 отсутствует,
bind(::) работает. Сводка /tmp/stream-info-expiry-fix-full-check.log;
состояние среды /tmp/stream-info-expiry-host-ipv6.log. Код из-за этих отказов не менялся.
Создан отдельный временный network namespace; IPv6 включён только в нём,
поднят lo/::1, сами проверки выполнялись от user. Все три прежних отказа PASS,
затем ./check.sh:201 passed,0 failed,0 skipped. Новый unit32мс, service80173мс.
Proxy9/9,burst PASS,load8/8,134217728 байт; полное время175с.
Логи /tmp/stream-info-expiry-fix-ipv6-env.log,
/tmp/stream-info-expiry-fix-ipv6-full-check.log и /tmp/stream-info-expiry-ipv6-test_*.log.
Основной namespace сохранён: all/default/lo=1 и bind(::1) по-прежнему EADDRNOTAVAIL.
Необходимость явной IPv6-диагностики runner записана отдельной задачей в doc/tasks.md.
git diff --check PASS. Sanitizers, Qt GUI и Android на устройстве в этом исправлении
не запускались. Устранение дефекта не расширяет аудит:6[V]/12[~]/0[ ],doc/stream.txt[~].
10. Завершение сверки заголовков общего stream-ядра — 26.10.2026
Дочитан stream_network.c1616–2185: relay lifecycle, dispatch, общий tick,
перегрузка нескольких пиров, учёт кольца, позиции получателей и телеметрия.
Вместе с предыдущими этапами полностью прочитаны все15 C-реализаций и
stream_playback_audio.cpp (всего7775 строк), все18 заголовков.
Для уточнения контрактов повторно прочитаны полные buffer/media_buffer,
member/wire, load/metrics, playback_clock/audio, publisher/viewer.
Уточнения заголовков
- network/priv: принадлежность caller-owned handles после detach, manual/internal
ограничения, INFO nonce/повторы/сроки, аренда6с и RENEW2с, control TTL>4с,
пределы общих бюджетов, готовая INFO-проба без автоматического close,
первый DATA как структурная часть до проверки подписи, диапазоны/RTT/validity,
restart с сохранением consumer floor, relay READY/BOUND и сроки помощи.
- load/metrics: A/D/X/Q, атомарность add/remove, revision и окна роста/rates,
live-счётчики против последнего снимка rates, rebase и 16-битный timestamp,
первая пара для rate, строгий порог остановки, END и старение истории.
- buffer/media_buffer: размер слотов против фактических wire-байтов,
заимствованные данные/observer, поиск частей против проверки подписи,
неизменяемый набор k после decode, неподписанные timing/telemetry,
независимые позиции треков, generation/GAP/revision, status до KEY,
bounded MORE/WAIT, audio lookahead и продолжение последнего кадра после finish.
- member/wire: parse/format против доверенного load, BGP отдельно от членства,
собственный online, роль deleted=0, переключатель audio против codec эпохи,
конкретные размеры/предусловия writers/readers, путь, телеметрия и подпись END.
- playback_clock/audio: состояние регулятора/история и часы, допустимые входы,
producer/consumer и внешняя блокировка, 20-мс stereo output, PLC/разрыв,
needs_input/prepare/finish, hardware frame origin, empty до опустошения устройства.
- publisher/viewer: асинхронное открытие/stop, время занятия session ID,
KEY/revision/admission/TTL, feed как принятие очередью, mute без смены codec,
общие ETCP-счётчики instance, notify под lock/исключительный destroy,
владение батчем, диапазоны demand и нулевые снимки до обновления actor.
Последние изменения этих12 заголовков — комментарии/оформление. Сравнение
лексических C-токенов с сохранённой до правок базой проверяет имена, типы,
порядок полей, объявления и выражения; все12 совпали. Публичный API/ABI,
состояния, протокол и C/C++ логика в данном продолжении не менялись.
Новых подтверждённых дефектов реализации в этой части аудита не найдено.
В stream.txt согласованы native-разделы: текущая версия wire7, INFO_REQ9,
сроки и закрытие control, совместимость через NCD без собственного
route_connectivity probe-движка, структурная часть против подписанной пачки,
60-секундные rates, recovery без сброса media и отсутствие пропуска по одному
возрасту DATA. Уточнено, что relay может готовить media-индекс по want_media.
Старые результаты wire6/utun2 помечены историческими.
Покрытие:18[V]/0[~]/0[ ] в src/stream. doc/stream.txt[~]: ещё нужны
полная сверка desktop/Android capture/decoder/audio/UI и внешнего
tests/stream_netns, их аппаратные и старые ASAN-результаты здесь не перепроверены.
Завершение заголовков не объявляет эти внешние компоненты проверенными.
Проверка ./build.sh --no-gui -j4 PASS: /tmp/stream-header-audit-build.log.
Полный ./check.sh PASS:201 passed,0 failed,0 skipped; proxy9/9,burst PASS,
load8/8,134217728 байт. Полное время172с. Все9 профильных core/media/playback/
tx_stats/Android bridge/service/member/publisher/control_expiry наборов PASS.
Лог /tmp/stream-header-audit-check.log; условия /tmp/stream-header-audit-netns.log.
Прогон выполнен от user в отдельном network namespace с IPv6/lo/::1;
основной namespace сохранён (all/default/lo.disable_ipv6=1).
Неизменность C-токенов12/12, отсутствие правок C/C++ реализаций/lib/tests
относительно5259c746, строки<=150, diff --check и отметки чек-листа PASS:
/tmp/stream-header-audit-contract-check.log. После прогона менялись лишь
два комментария (Ed25519 public key и ненулевая база expected interval);
лексические токены проверены повторно, новый прогон не требовался.
Sanitizers, отдельные Qt/C++ audio tests, desktop GUI и Android на устройстве
в этом продолжении не запускались. Новых runtime-проверок многопроцессного
tests/stream_netns не было; тестовый namespace здесь служил только средой check.sh.
+6 -2
View File
@@ -31,7 +31,8 @@ nat=debug
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
IPv6 и отсутствие выбранного IPv4 дают `SKIPPED` с причиной `unsupported socket`.
TCP, служебные `probe_only`, SOCKS UDP и `only_local` исключены из списка проб: API возвращает для них -1.
## Результат
@@ -40,6 +41,8 @@ IPv6, SOCKS UDP и `only_local` пропускаются с явным стат
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
Нулевой возврат означает наличие снимка, включая PENDING/FAILED/SKIPPED: вызывающий проверяет статус
и возраст `measured_at_tb` относительно `STUN_NAT_TTL_TB`. -1 не меняет выходную структуру.
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
@@ -89,7 +92,8 @@ TTL результата 300 секунд, refresh 180 секунд; непол
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
История портов освобождается только при окончательном destroy ядра.
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
`stun_nat_recheck(instance)` сверяет текущие пути и явно повторяет измерение каждого ровно один раз
с новым generation/портом и без старого кэша, в том числе после смены маршрута без смены IP.
Публичные операции выполняются в uasync-потоке экземпляра.
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
+700 -15
View File
@@ -4,16 +4,665 @@
## Текущие задачи
[ ] **member_files: cleanup удаляет чужой temp при отказе создания загрузки** — воспроизведено 24.10.2026 на a198cfb9.
test_member_files_network падает в фазе23: локальный O_EXCL open возвращает MF_EXISTS,
затем TRANSFER_FINISH без проверки владения вызывает mf_disk_local_remove(t->temp).
Прежнее назначение37байт сохраняется, заранее созданный чужой .part исчезает.
Проверены общий check.sh и отдельный повтор: одинаковый FAIL preservation case=0, target=1, foreign_temp=0;
отсутствие .part дополнительно проверено в обоих оставшихся тестовых каталогах.
Нужен учёт успешного создания собственного temp, включая отмену до PREPARE и отказ max_bytes;
проверять/удалять можно только созданный этой передачей файл. Код этого модуля в ETCP-ветке не менялся.
Логи: /tmp/etcp-solo-native-full-check.log, /tmp/etcp-solo-member-network-repeat.log;
подробный первый лог: /tmp/etcp-solo-member-network-suite-failed.log.
[+] **BLE/ETCP: ограничить очередь удалёнными ACK GATT-фрагментов** — 29.10.2026.
BLE version3: циклический ID16bit каждого DATA GATT-value, отдельная ACK Write/Indicate характеристика.
Окно12500байт на направление = фиксированные2Мбит/с ×50ms; локальный completion не возвращает кредит.
Следующий ACK не раньше15ms после completion предыдущего; DATA/INVITE получают время даже при медленном ATT.
Третья подписка обязательна до HELLO. Дубликаты/старые ACK игнорируются, неверный ACK и15s без прогресса логируются/закрывают link.
Новые регрессии: строгий duplex byte bound, ACK>=15ms,70000 IDs, медленный20ms ACK completion, timeout и lifecycle.
Desktop central:512/512 echo за72.344s, суммарно21232B/s; Android central:512/512 за115.141s,13340B/s.
По768000 проверенных байт в каждом направлении в каждой роли. HCI TX queue <=0.174s/0.358s, ранее до20..26s.
Максимум DATA flight8713/10204байт на desktop и4442/4411 на Android, ниже12500 во всех пробах.
C-ядро230/230 + proxy/burst/load, BLE/BlueZ/identity, GUI/ETCP/packet-link, Android unit tasks PASS.
ASAN/UBSan3/3 без ошибок/утечек; desktop/APK и WinRT cross-build PASS. Физический Windows не проверен.
Результаты, параметры проб и пути диагностических журналов: [ble_etcp.txt](ble_etcp.txt).
[+] **ETCP: учитывать измеренный RTT больше секунды в RTO** — 29.10.2026.
На BLE измерялся RTT2..6s, но RTO=1s удалял попытку до ACK и добавлял повтор в ту же очередь.
Сохранены формула1.2*max(DATA_RTT,KA_RTT)+1.5*jitter+3ms, минимум10ms и начальное1s без RTT.
Верхняя граница теперь UINT32_MAX, расчёт uint64; насыщение логируется WARN.
test_etcp_windows:63 сценария PASS, включая RTT2s/ACK1.5s, настоящий таймер7.8672s и переполнение.
Исправлена фикстура server_lifecycle: успешную отправку задаёт codec, busy/error не увеличивают keepalive counter.
Итоговый ./check.sh230/230 и proxy/burst/load PASS; desktop GUI/probe и Android APK/instrumentation собираются.
[+] **BLE: ждать ServicesResolved и запрашивать HIGH для incoming Android** — 29.10.2026.
BlueZ ждёт Connected/ServicesResolved до подписок и перечитывает MTU; регрессия кеша MTU23→517 проходит.
Android использует дополнительный BluetoothGatt handle того же ACL только для HIGH, с явным teardown.
HCI подтвердил входящий запрос 11.25..15ms и согласование 15ms/latency0/timeout5s без ручной настройки.
Desktop и Android собираются, private-bus BlueZ и профильные BLE/packet-link/unit тесты проходят.
[+] **BLE: DATA без ATT ACK и профиль скорости desktop/Android** — 29.10.2026.
Write Without Response/Notification для DATA в Android/BlueZ/WinRT; INVITE сохраняет Request/Indication.
Android HIGH/MTU517/2M, Windows ThroughputOptimized с проверкой API и фактическими параметрами.
Сводка скорости/очередей/ожиданий GATT; нагрузочная проба с ограниченным окном и проверкой содержимого.
Общие тесты 230/230 + proxy/burst/load, BLE 5/5, ASAN/UBSAN 3/3, Android unit 770/770.
Desktop и APK собираются; WinRT cross-build PASS. Ограничение обратной роли вынесено выше.
[ ] **Windows: единые UTF-8 пути конфигурации** — 29.10.2026.
GUI передаёт toUtf8, а config_parser/config_updater используют fopen и ANSI Win32 API.
Перевести эти операции на существующий file_io с wide-путями, проверить профиль в каталоге с кириллицей.
Пути журнала исправлены при добавлении ротации: wide open/rename/remove, проверены Windows-тестом через Wine.
Это отдельный риск; причиной отказа присланного 2.log на Windows он не подтверждён.
[+] **Вложения: отправка независимо от лимита автозагрузки, метаданные фото/видео и результат GUI** — 28.10.2026.
Исходящие файлы не ограничиваются storage_unit_size; defaults автозагрузки/кеша — 100 MiB/1 GiB.
Парсер принимает подписанный хвост dur/w/h; канальный файл остаётся в composer до подтверждения, отказ показывает имя.
test_chat_attachment и четыре теста GUI PASS; на SM фотография скачалась и совпала с desktop по SHA-256.
[ ] **Тестовая среда: явно проверять доступность IPv6 loopback перед общим прогоном** — 28.10.2026.
При disable_ipv6=1 отсутствует ::1: test_ncd_faults, test_ipv6_sockets и test_route_connectivity падают на bind.
Зафиксировать требование к среде или явный SKIP соответствующих сценариев; текущий ./check.sh: 222 PASS, 3 FAIL.
[+] **chat headless: полный разбор JSON,целые TCP-кадры и результат публикации** — 27.10.2026.
SQLite проверяет весь объект до dispatch;параметры типизированы и копируются без усечения,дубли/NUL/JSON5 запрещены.
TX сохраняет FIFO при short write/EAGAIN и освобождает отправленный префикс;переполнение закрывает клиента с WARN.
Status/candidates/invite names используют checked JSON escaping;прежний event handler сохраняется/восстанавливается.
send проверяет результат,attach отвечает из нового media completion после commit/отказа;disconnect отсоединяет адресата.
Контекст completion освобождается после закрытия control;quit дописывает bye,init полностью убирает частичные ресурсы.
test_chat_headless_control:настоящие TCP/SQLite/media,Unicode/опросы,SQL finalize fault,short write/pressure,
async disconnect и handler restore PASS;services/chat_attachment/media_index_regressions PASS.
[+] **chat: атомарное глобальное имя и подписанный снимок** — 27.10.2026.
Один commit сохраняет local_identity,nodes,NODEINFO и полные собственные профили каналов/Merkle.
Память/LAN/уведомления меняются после commit;отказ сохраняет все представления,no-op не меняет версии.
Чтение timestamp различает отсутствие/SQL,запись snapshot проверяет SQL/finalize под savepoint.
test_chat_profile:32 SQL fault/retry,crypto/veto/no-op/foreign txn,два профиля и независимый observer PASS;
standalone snapshot finalize failure также откатывается. Контракт:[chat_core.h](../src/chat/chat_core.h).
[+] **chat_whisper: per-instance модель,повтор после отказа и безопасная остановка** — 27.10.2026.
Глобальный init/trigger заменён прямым API:ленивая загрузка в worker,кеш модели,одна задача и явный BUSY.
Stop отсоединяет владельца;модель освобождается после join,каждый запрос получает один callback.
Opus reader согласован с voice_file_encode (native header/lengths,stereo);WAV/ресемплинг проверяют размеры.
Результат отправки транскрипции проверяется;отметка played — только после сохранения текста.
Два теста:повтор/смена модели,занятость,изоляция,cancel/restart,allocation,PCM и10 rate/channel Opus вариантов PASS.
Backend распознавания управляемый;настоящая модель не запускалась. Контракт:[chat_whisper.h](../src/chat/chat_whisper.h).
[+] **chat preferences: собственный commit UI-state и рассылка сохранённого звука** — 27.10.2026.
Подтверждён commit после injected finalize error прежнего UI-state setter. Новая операция проверяет
read/write/finalize/changes/commit под BEGIN IMMEDIATE;отказ сохраняет значение,no-op не вызывает hook.
Sound setter возвращает результат и рассылает только committed/no-op;повтор восстанавливает доставку.
Getter различает отсутствие/SQL/неверный тип и boolean;initial sync пропускает ошибку чтения.
Settings wrapper сохраняет коды registry и не применяет invalid group_autoconnect;trampoline проверяют NUL.
В звуковой форме исправлен указатель на временный QByteArray;добавлена проверка18 сохранённых значений.
test_chat_preferences:13 UI-state+13 sound+5 reader SQL fault/retry,readonly observer,no-op/veto/triggers,
foreign txn,malformed requests,owner stop при rollback failure PASS. Общие результаты — в отчёте.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Настройки и UI-state»;core ещё частичен.
[+] **chat member tags: проверенный снимок,конфликт правки и атомарный commit** — 27.10.2026.
Подтверждён ложный успех load после finalize error. Чтение закрывает SQL и проверяет owner-подпись/формат.
Commit владеет BEGIN IMMEDIATE,отклоняет устаревший снимок (-2),фиксирует подпись/версию/Merkle вместе;
отказ сохраняет editor и историю ролей для повтора,no-op не меняет версию/события. Приватный ключ очищается.
GUI применяет частичные поля,сохраняя остальные;Android не сохраняет частичный набор после ошибки setter.
ver1..INT_MAX проверяется без atoi overflow,JSON — целиком;display/JNI читают историю ролей до8192 байт.
test_chat_profile дополнен37 SQL и10 allocation отказами/повторами,sign/veto/foreign/nested rollback,
stale/no-op/repeated edits,строгим JSON/control,patch и долгой историей,удалением из callback;PASS.
Общая проверка записывается в отчёт;остальные core/headless/JOIN/sync/invite/whisper продолжаются.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Редактор прав участников».
[+] **chat profile: проверка прежней подписи, собственная транзакция и безопасные уведомления** — 27.10.2026.
Getter публикует все выходы только после SQL/finalize,проверки JOIN/update и целого строкового JSON.
Options/avatar читают версию,проверяют допуск,подписывают и меняют запись/Merkle под BEGIN IMMEDIATE;
семантический no-op не меняет подпись/версию. Неподписанные старые поля не переподписываются.
Неудачный member ROLLBACK TO не сопровождается RELEASE частичных данных;проверяется changes=1 owner/nodes.
Callback допускает отмену подписок,вложенный dispatch и удаление группы;JOIN-подписка проверяется при старте.
Unicode escapes больше не скрывают следующие поля профиля. test_chat_profile:20 read/54 write/9 owner/4 ACL
SQL-отказа с повторами,crypto/trigger/veto/foreign/rollback,readonly observer и startup retry PASS.
Общая проверка записывается в отчёт после завершения;глобальное имя/UI-state/tags и core остаются в работе.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Подписанные профили».
[+] **chat_core JSON: полное escaping и атомарный результат чтения** — 27.10.2026.
Подтверждён невалидный JSON для допустимого подписанного имени с ASCII01. Channels/members используют
checked strbuf и полное escaping;все listing проверяют SQL-этапы,включая bind/finalize/вложенные запросы.
Отказ оставляет out_len=0 и пустой допустимый buffer;нулевой/точный размер не повреждает canaries.
Скрытый NUL/неверный тип/размер полей отклоняются. Committed-канал без history-таблицы виден с msgs=0;
чтение не меняет БД и не завершает транзакцию caller. Новая test_chat_json:36 отказов с повторами,
ошибка после готовой строки,все ASCII controls,UTF-8/длинные имена/8192-byte tags,IPv4/IPv6/REALITY,
повреждённые строки и настоящая deferred activation/retry PASS. Общие результаты — в отчёте.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«JSON-интроспекция».
[+] **chat_member/status: полный снимок,границы формата и committed mute** — 27.10.2026.
Подтверждён ложный успех снимка после finalize error;теперь SQL/память не дают частичный список.
Список ограничен65535 записями,сортируется qsort (online,unsigned node_id);строки wire дополняются NUL.
Запросы/канальные ID проверяются до чтения;callback свойств использует один завершённый SELECT.
radio_set_member_muted владеет транзакцией,кэш/аудио обновляются после commit;чат готовит снимок до изменения,
последующий SQL-отказ не превращает сохранённый переключатель в ошибку. Чужая транзакция запрещена.
Диагностика использует checked strbuf;member_detail проверяет finalize и согласованный error codec.
test_chat_member:SQL/память/ABORT/IGNORE/veto/foreign/rollback,readonly DB+cache+audio observer,
65535/65536 участников,300 линков,OOM и allocator boundary PASS. Общие проверки — в отчёте.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Снимки участников и диагностика».
[+] **group_avatar: атомарная публикация, проверенное чтение и безопасное событие приёма** — 27.10.2026.
Подтверждён прежний ложный успех при отказе bind SHA256. set/apply владеют BEGIN IMMEDIATE/COMMIT,
проверяют все SQL-этапы,отклоняют чужую транзакцию;события/STATE идут после commit.
Выход get сохраняется при отказе;ключи читаются как BLOB ровно32 байта и очищаются.
Schema savepoint не фиксирует частичные ALTER после неудачного ROLLBACK TO.
Приём завершает работу со сборкой/peer до callback,который может отменить задачи группы.
Native профиль6/6 PASS;новый test_group_avatar_sql:27 SQL-отказов публикации,16 чтения,9 схемы,
signing/ABORT/IGNORE/veto/foreign/no-op/max-version/rollback и readonly observer;повторы PASS.
Сетевой test_group_avatar проверяет отмену peer из CHANNEL_UPDATED;header/doc сверены полностью.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Аватарки групп»;общие результаты — в отчёте.
[+] **chat_channel: атомарное создание канала и правдивый результат команды** — 27.10.2026.
Канальные пары ключей и подпись проверяются до записи; описание, схема, root JOIN, подписанный профиль,
приглашение и Merkle сохраняются одним commit. Runtime-подготовка и события выполняются после него.
Ошибка подготовки не удаляет сохранённый канал, допускается повтор ensure_channel_ready.
Исправлены частичная схема channel_put и незамеченный отказ bind подписи профиля в member_block_put.
Auto возвращает committed ID или0; native headless возвращает ошибку при отказе, очищаются временные ключи.
test_chat_channel:42 SQL-отказа и19 криптоотказов с успешными повторами,ABORT/IGNORE/veto,
чужая транзакция,ошибка rollback,границы и TCP API;readonly-проверка данных/схемы/Merkle до события.
Профильные проверки PASS;общая проверка текущего блока записывается в отчёт после завершения.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Атомарное создание канала».
[+] **feedback/poll: проверенное чтение версий, собственные транзакции и подтверждение уже сохранённой команды** — 27.10.2026.
SQL-ошибка поиска не считается отсутствием старого состояния:отмена не заменяется прежней реакцией.
apply/set/page/init владеют BEGIN IMMEDIATE/COMMIT;версия/допуск/запись/Merkle фиксируются вместе,
уведомления — после commit. Неудачный rollback не выполняет RELEASE и запрашивает stop владельцем.
Проверены SQL-этапы get/put/page/hash и команд/снимков poll. Ошибка чтения после commit даёт успешный
committed/snapshot_pending;Qt/Android повторяют только feedback. Ack резервируется до изменения.
test_chat_feedback:подтверждён старый дефект,20 SQL-отказов изменения и10 чтения,ABORT/IGNORE/veto,
чужой no-op/page,rollback failure,readonly record/root перед событием. test_chat_poll:51 SQL-отказ,
8 readback-отказов после commit,2 отказа памяти до изменения,полная строка emoji;повторы PASS.
Native/ASAN профиль6/6 до readback,новая unit-регрессия readback PASS;общие результаты — в отчёте.
Подробнее:[chat_audit_findings.txt](chat_audit_findings.txt),«Feedback и poll».
[+] **chat_attachment: неизменяемая привязка, атомарный локальный индекс и восстановление из подписанной истории** — 27.10.2026.
bind проверяет собственный commit и запрещает замену message_id/hash; observe сохраняет прежний ключ/флаги точного дубликата.
wanted/downloaded обновляются своей транзакцией, события идут после commit; повторное обнаружение cache не сбрасывает error.
Отказ производного индекса не скрывает сохранённое сообщение: backfill восстанавливает запись, включая startup/manual download;
очередь16 каналов имеет полный scan при переполнении. Ключ из истории не восстанавливается.
Outbox фиксирует retry_at до сети; idle scan не вызывает commit hook. Recovery при открытой транзакции сохраняет файлы.
Native профиль7/7 и последующие settings/attachment2/2 PASS;ASAN/UBSan/LSan17/17 PASS,detect_leaks=1.
Подробности:[chat_audit_findings.txt](chat_audit_findings.txt),«Идентичность и восстановление локального индекса».
[+] **chat_setting: чтение const без изменения буфера, атомарный отказ валидации и повтор применения LAN** — 27.10.2026.
Числовые строки хранятся отдельно по настройке и обновляются setter; NULL/строковый тип дают default.
Неудачный set не инициализирует/не меняет state;пути можно очистить. Ошибка apply LAN возвращает -3,
сохраняя желаемое значение;идентичный повтор повторяет применение. Удалён пустой общий apply.
Новый test_chat_setting проверяет неизменность всех байтов,независимые копии,границы/ошибки и apply retry;
native и ASAN/UBSan/LSan PASS. Общая проверка новых изменений записывается отдельным итогом аудита.
[+] **chat_attachment: исходящий REQUEST и outbox фиксируются вместе до уведомления/отправки** — 27.10.2026.
Собственная транзакция повторно проверяет состояние файла,создаёт ровно один запрос и очередь.
Внешняя транзакция откладывает весь tick;неудачный rollback не перезапускает таймер после quiesce/stop.
test_attachment_requests:52 SQL-отказа/повтора с пустой/существующей очередью,ABORT/IGNORE,commit veto,
внешняя транзакция до создания/отправки,readonly reader перед событием/REQUEST,реальный restart
при отсутствии маршрута/backpressure/send failure с сохранением даты. Старый код ожидаемо FAIL новой регрессии.
Общая проверка ниже:212/212 native,16/16 sanitizers,11/11 GUI/Android host,desktop/APK build PASS.
Отчёт:[chat_audit_findings.txt](chat_audit_findings.txt),«Исходящий REQUEST»;/tmp/attachment-outgoing-*.
[+] **chat_attachment: захват расшифровки до worker и владение completion до commit результата** — 27.10.2026.
decrypting=1 фиксируется собственной проверенной транзакцией;completion отличает SQL-отказ от удаления.
Готовая задача остаётся у модуля до commit;таймер повторяет только SQL,ключ/файл не теряются и второй worker не создаётся.
Лимит16 включает workers/завершения;quiesce останавливает повторы,destroy после workers освобождает остаток.
test_chat_attachment:9 отказов claim,15 успешного результата,8 записи ошибки worker,ABORT/IGNORE/veto,
внешняя транзакция,delete/stop/restart,rollback failure и16 задержанных авторских задач с сохранением17-го source.
Старый sanitizer-runtime подтвердил ready1/infoNULL после bind NOMEM;новый профиль и баланс выделений PASS.
Общая проверка:check.sh212/212,0 skipped,proxy9/9,burst1/1,load8/8,177с;
ASAN/UBSan/LSan16/16 detect_leaks=1,desktop/APK build,GUI/Android host11/11 PASS.
Отчёт:[chat_audit_findings.txt](chat_audit_findings.txt),«Завершение файлов»;/tmp/attachment-jobs-*.
[+] **chat: удалять attachment-записи атомарно с каналом,файлы — после commit** — 27.10.2026.
SQLite-trigger каскадно удаляет ключи/запросы/outbox и проверяет полный результат;topo deletion проверяет finalize
и отсутствие канала/Merkle-хешей. Ошибка SQL сохраняет данные/файлы;внешняя транзакция запрещена до отмены ресурсов.
Файлы удаляются после commit,незавершённый rollback требует stop/закрытия владельцем;NULL instance безопасен.
test_chat_attachment:14 отказов с точным readonly-снимком,ABORT/IGNORE всех таблиц и Merkle,commit veto,
внешняя транзакция и rollback fault;до фикса подтверждены reader_keys2 вместо3 и потерянный plaintext при живом канале.
Native build/check.sh212/212 PASS,proxy9/9,burst1/1,load8/8 (134217728байт),177с;
ASAN/UBSan/LSan16/16 detect_leaks=1,GUI/Android host11/11,desktop/APK build PASS.
Логи:/tmp/chat-attachment-followup-{build,full-check,asan-results,desktop-build,android-build,gui-results}.log.
Отчёт:[chat_audit_findings.txt](chat_audit_findings.txt),«Удаление канала»;полный аудит chat/attachment остаётся[~].
[+] **chat_attachment: подтверждать входящий REQUEST после собственного commit** — исправлено27.10.2026.
Собственная BEGIN IMMEDIATE/COMMIT включает identity,права,чтение/создание запроса и повтор прежнего ответа.
Внешняя транзакция отклоняется без изменений;дубликаты incoming1/state0,2,3,4 сохраняют решение и created_at.
Проверены все SQL-этапы;новая строка/очередь требуют changes1,ACK/событие следуют после commit.
Общий rollback сохраняет прежние данные;открытая транзакция требует quiesce/stop и закрытия владельцем после poll.
test_attachment_requests:247 SQL-отказов/повторов,commit veto,ABORT/IGNORE,внешняя транзакция,
неверные состояния/права,независимые reader в ACK/событии,backpressure/send fault и rollback fault.
Сеть:первый REQUEST без ACK→очередь B сохранена→restart автора→настоящий повтор→committed ACK→phase10 PASS.
Native build/check.sh212/212 PASS,0 skipped,proxy9/9,burst1/1,load8/8 (134217728байт),173с.
ASAN/UBSan/LSan16/16 PASS,detect_leaks=1;desktop/APK build PASS,GUI/Android host11/11 PASS.
Отчёт:[chat_audit_findings.txt](chat_audit_findings.txt),«Исправление приёма REQUEST»;/tmp/attachment-request-fix-*.
Полный аудит attachment/chat остаётся частичным;исходящие запросы и файловые completion/recovery проверяются далее.
[+] **chat_attachment: фиксировать решение автора вместе с исходящим ответом до успеха/UI** — исправлено27.10.2026.
Собственная BEGIN IMMEDIATE/COMMIT включает проверку прав,state и outbox;внешняя транзакция остаётся нетронутой.
ak_queue проверяет все SQL-этапы и одну строку,транзакцией владеет вызывающий;допуск автора проверяет finalize.
Успех/одно событие — после commit,ошибка сохраняет прежние строки;открытая после rollback БД требует stop владельцем.
Trampoline сообщает только ошибку команды,не дублируя успешное обновление. Новых API/форматов нет.
test_attachment_decisions:116 отказов/повторов выдачи/отказа с пустой/существующей очередью,
настоящий commit veto,ABORT/IGNORE,внешняя транзакция,мембер/направление/повтор,GUI и rollback fault.
Readonly-снимки в событии проверяют точные committed key/state/outbox/retry/created и соседний ключ;allocations0->0.
Сетевой тест:оба отказа автора→pending без события/очереди→restart→повтор→commit→GRANT→расшифровка phase10 PASS.
Build/check.sh211/211 PASS,0 skipped,proxy9/9,burst1/1,load8/8 (134217728байт),177с.
ASAN/UBSan/LSan15/15 PASS,detect_leaks=1;desktop/APK build PASS,GUI/Android host11/11 PASS.
Отчёт:[chat_audit_findings.txt](chat_audit_findings.txt),«Исправление решения автора»;/tmp/attachment-decision-fix-*.
Полный аудит attachment/chat остаётся частичным;отметки chat_attachment и encrypted_attachments остаются[~].
[+] **chat_attachment: не подтверждать GRANT без сохранённого ключа получателя** — исправлено26.10.2026.
Ответы REQUEST_ACK/GRANT/DENY/RESULT_ACK применяются собственной проверенной транзакцией key+state+outbox.
Проверяются все SQL-этапы и ожидаемые числа строк;ACK/событие — после commit,внешняя транзакция отклоняется.
Полный rollback сохраняет прежний ключ/запрос/очередь;оставшаяся открытой БД требует stop владельцем после poll.
Новый test_attachment_results:105 отказов/повторов четырёх ответов,readonly-снимки в ACK/событии,
commit veto,ABORT/IGNORE,внешняя транзакция,дубликаты/конфликты/backpressure и rollback fault.
Штатный сетевой test_encrypted_attachment дополнен bind-fault→без ACK→повтор GRANT→расшифровка.
До Pull native8/8 и ASAN/UBSan/LSan8/8 PASS,detect_leaks=1;исходный сетевой стенд теперь phase10 PASS.
После merge845bf258 с доработками редактирования чата:native14/14,ASAN/UBSan/LSan14/14 PASS.
check.sh210/210 PASS,0 skipped,proxy9/9,burst1/1,load8/8;GUI/Android host11/11,desktop/APK build PASS.
Подробности:[chat_audit_findings.txt](chat_audit_findings.txt),интеграция после Pull;/tmp/grant-key-fix-merge-*.log.
Полный аудит attachment/chat остаётся частичным;дальнейший аудит остановлен по просьбе пользователя перед Pull.
[+] **chat_attachment: проверять сохранение ключа до передачи source и запуска worker** — исправлено26.10.2026.
Авторская подготовка фиксируется собственной BEGIN IMMEDIATE/COMMIT до передачи source и запуска worker.
Проверяются prepare/восемь bind/step/одна строка/finalize/commit;отказ откатывается,source остаётся caller.
Внешняя транзакция запрещена;после commit синхронный отказ worker даёт один MSG_SUBMITTED при возврате0.
Неуспешный rollback с открытой транзакцией снимает модуль и запрашивает stop;desktop/Android,включая restart,
выходят после poll и освобождают ядро вне callback. Политика WAL/NORMAL и wire не менялись.
test_chat_attachment:13 SQL-отказов,реальный commit veto,INSERT ABORT/IGNORE,внешняя транзакция,
точный ключ/метаданные через readonly SQLite до worker,настоящие повторы,inline-отказ,restart и rollback fault.
Native build/check.sh209/209 PASS,0 skipped,proxy9/9,burst1/1,load8/8 (134217728байт),174с;
полный check выполнен в частном netns с IPv6 loopback — на хосте IPv6 lo отключён.
ASAN/UBSan/LSan8/8 PASS,detect_leaks=1;core/libuasync/Opus инструментированы согласованно.
Desktop/APK build PASS;Qt/Android host7/7 PASS. Исходный стенд теперь defects0,allocations0->0;
отдельный старый объект из d877e888 ожидаемо нарушает новую проверку,без отката рабочей копии.
Подробности: [chat_audit_findings.txt](chat_audit_findings.txt),исправление chat_attachment;/tmp/attachment-key-fix-*.log.
Полный аудит chat_attachment/chat остаётся [~];прочие операции SQL и платформы Windows/FreeBSD не закрыты.
[+] **member_files: безопасное завершение completion при stop из uasync batch** — исправлено26.10.2026.
После rebase на bc623f23 сохранена общая отмена из93ec1222:task удаляется и освобождается
из общей очереди или активного batch до освобождения mf_work;локальное обнуление callback/arg убрано.
Done доставляется ровно раз,включая обязательные cleanup continuations;нет новых аллокаций/рекурсивного poll.
Контракты worker/uasync/docs согласованы:stop из внешней команды допустим,из собственного done запрещён.
Постоянный test_member_files:4 барьерных сценария normal/queued destroy/detached drain/detached destroy,
по2 работы и cleanup,правильные потоки,единственный done и полный баланс выделений.
До rebase на прежнем worker.c новая регрессия выявила heap-use-after-free в строке45 под ASAN;
/tmp/member-worker-negative.log подтверждает чтение после освобождения в drain,без изменения рабочей копии.
До rebase:native/lifecycle8/8 PASS,JOIN50/50 PASS;ASAN/UBSan/LSan8/8 PASS,detect_leaks=1.
До rebase:check.sh207/207 PASS,0 failed/0 skipped,STUN/proxy9/9/burst1/1/load8/8,182с.
После rebase на bc623f23:native и ASAN/UBSan/LSan6/6 PASS,JOIN50/50 PASS,обе сборки PASS;
runtime отмены/worker совпадает с origin,admin и регрессии сохранены;логи /tmp/admin-worker-rebase-*.log.
Первоначальный SIGSEGV и повтор GDB сохранены,стек worker.c:45 → u_async.c:432 → join_faults.c:298.
Core /tmp/admin-key-fix-join.core,бинарник /tmp/admin-key-fix-test_chat_join_faults.elf,
разбор /tmp/admin-key-fix-join-core-analysis.log,повтор /tmp/admin-key-fix-join-repeat-gdb.log (10-й запуск).
worker.h закрыт [V];полный аудит chat/member_files остаётся частичным,платформенные GUI не проверялись.
Подробности: [chat_audit_findings.txt](chat_audit_findings.txt),исправление worker;/tmp/member-worker-fix-*.log.
[+] **chat_admin: атомарный импорт ключей и безопасный отказ NULL transfer** — исправлено26.10.2026.
channel_set_privs проверяет savepoint/prepare/все bind/step/одну строку/finalize/RELEASE и откатывает отказ.
Получатель отвергает внешнюю транзакцию,подтверждает только commit;незавершённый rollback требует restart.
transfer проверяет inst до CC;SQL/crypto/OOM причины логируются без секретных байтов,wire не менялся.
Постоянный test_chat_admin:22 SQL/crypto-отказа и отдельный rollback fault,старая пара/другой канал,
настоящее veto commit hook,trigger ABORT/IGNORE,неверные ключи,повтор,внешняя транзакция и NULL.
Независимое чтение пары/autocommit в событии,отсутствие ложного успеха,allocations восстановлены.
Исходный стенд теперь exit0,defects0,bind hits3. Native build/check.sh207/207 PASS,0 failed/0 skipped,
STUN/proxy9/9/burst1/1/load8/8 (134217728байт),201с;ASAN/UBSan/LSan5/5 PASS,detect_leaks=1.
Первый native-профиль4/5 с SIGSEGV JOIN сохранён;отдельный дефект worker исправлен в задаче выше.
[chat_audit_findings.txt](chat_audit_findings.txt),продолжение chat_admin;/tmp/admin-key-fix-*.log.
chat_admin.h/topo_node_sqlite.h закрыты [V]. Полный аудит chat остаётся частичным.
[+] **DM media: восстанавливать завершение TTL после SQL-отказа при EXPIRED** — исправлено 26.10.2026.
EXPIRED/REJECT атомарно освобождают назначение и сбрасывают текущий holder; все результаты SQL проверяются.
Таймер завершает stored-назначение по сохранённому expires при offline B/потерянном EXPIRED и после restart.
Для неподтверждённого STORE сохраняется повтор с ответом custody/tombstone M; квитанция B не отменяется.
test_dm_media:18 отказов prepare/bind/step/finalize двух UPDATE и commit для EXPIRED/REJECT,
точный rollback, повтор, дубликат и поздний STORED; реальная сеть A/B/S/M с потерянным EXPIRED,
SQL-отказом, restart S, восстановлением TTL и поздними STORE/STORED; allocations0->0.
Старый координатор ожидаемо FAIL новой регрессии; исходный one-shot IOERR-стенд теперь phase9 PASS.
Независимое readonly чтение по точному UUID:на M custody0/tombstone1,на S holder0/stored0/expired1/released1,
без ложной квитанции; прежняя доставленная квитанция сохранена. Native/санитайзеры подтверждают те же состояния.
Build/DM-профиль8/8 PASS;ASAN/UBSan/LSan8/8 PASS,detect_leaks=1,core/libuasync/Opus инструментированы.
Полный check.sh205/205 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8 (134217728байт),exit0,173с.
Подробности: [dm_audit_findings.txt](dm_audit_findings.txt),раздел11; /tmp/dm-expiry-fix-*.log.
Общий аудит DM остаётся частичным;платформенные GUI/Android/Windows/FreeBSD не проверялись.
[+] **DM: сохранять границы UTF-8 при усечении имени собеседника** — исправлено 26.10.2026.
Единый предел DM_PEER_NAME_MAX=63 байта; создание, загрузка, входящее автосоздание и JSON
копируют только целые code points. Повреждённый суффикс отбрасывается с WARN и точным смещением.
Снимок нормализует также fallback из nodes без изменения БД; обычная запись сохраняет нормализованное имя.
test_dm_names:68 случаев создания/SQL/load/save/fallback/restart, ASCII и 2/3/4-байтовые границы,
overlong/surrogate/>U+10FFFF/оборванные последовательности/NUL/JSON escapes; allocations0->0.
Независимый Python-парсер:273 JSON-снимка,9452 строки,строгие UTF-8/JSON и предел63 PASS.
Старый core ожидаемо FAIL новой регрессии; исходный пример32×«Я» теперь сохраняет62байта и валидный JSON.
Сборка/DM-профиль8/8 PASS; ASAN/UBSan/LSan8/8 PASS,detect_leaks=1,core/libuasync/Opus инструментированы.
После rebase на fa76447f:build/check.sh205/205 PASS,0 failed/0 skipped;
proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,175с;273 JSON-снимка повторно PASS.
Подробности: [dm_audit_findings.txt](dm_audit_findings.txt),раздел9; /tmp/dm-utf8-fix-*.log.
Исправление закрыто; полный аудит DM остаётся частичным, аппаратные GUI/Android не проверялись.
[+] **call_audio: сохранить регистрацию при stop/start чата и согласовать Android lifecycle** — исправлено 26.10.2026.
Заимствованный audio_ops хранится у UTUN_INSTANCE и переживает пересоздание call_ctx; повторный init не нужен.
Идемпотентный init того же owner сохраняет эпоху/занятые TX-слоты. Init/destroy выполняются в uasync;
Android worker привязывает voice до SERVICE_STARTED и снимает привязку перед destroy при stop/restart/ошибке запуска.
GUI больше не читает raw instance для lazy-init; поздний start без ядра отклоняется.
Восемь новых регрессий с реальными ядрами/Opus/worker PASS, allocations0->0; общий voice/desktop38/38 PASS.
Диагностика/исправление: [call_audit_findings.txt](call_audit_findings.txt),разделы18–19;
/tmp/call-registration-fix-{voice-tests,native-build,android-build,full-check}.log.
[+] **call/JNI: удалить неиспользуемый межпоточный online-API** — исправлено 26.10.2026.
call_is_online обещал любой поток, но напрямую читал uasync-очереди topo_groups/group_nodes без синхронизации.
Диагностика на двух настоящих ядрах: owner и post в uasync PASS; прямой worker — два поиска в чужом потоке,
ожидаемый FAIL, без конкурентного удаления объектов; cleanup во всех трёх режимах allocations0->0.
Внешних потребителей Android-метода нет: удалены Kotlin/JNI/C bridge и публичная декларация,
проверка стала static внутри call.c и выполняется из call_start в uasync. Алгоритм проверки сохранён.
Native build и Android assembleDebug для arm64-v8a/x86_64 PASS; Android/JVM170/170 PASS.
Отсутствие старого API проверено в исходниках, core archive, Kotlin-классе и обеих JNI-библиотеках APK.
check.sh204/204 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8,exit0,176с.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел17;
/tmp/call-online-thread-audit-{owner,worker,posted}.log, /tmp/call-online-fix-{validation,full-check}.log.
Общий аудит call остаётся частичным; чек-лист6[V]/5[~]/10[ ] не менялся.
[+] **Android CallAudioRouter: владение Bluetooth proxy при stop/restart** — исправлено 26.10.2026.
Поколение запроса и общий монитор start/stop/BT callbacks исключают принятие proxy старого lifecycle.
Disconnect сохраняет owner до close; stop отсоединяет ссылки и закрывает через создавший adapter,
поздний connected закрывает свой proxy. Диагностика содержит proxy/generation/accepted и ошибки API.
Отдельный JVM-модуль компилирует production router с управляемой моделью Android API:15/15 PASS,
включая64 конкурентных callback/stop и I/O start с задержанным callback; после cleanup0 proxy/callbacks.
Прежний исходник:штатный контроль PASS,три ownership-сценария ожидаемо FAIL.
Android clock/trace/buffer/recovery26/26,voice/desktop30/30,нативная сборка и assembleDebug для двух ABI PASS.
check.sh204/204 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8,exit0,173с.
Fixtures не входят в APK;Bluetooth/Binder/звук на устройстве не проверялись. Общий аудит call остаётся частичным.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел15;
/tmp/call-bt-fix-{gradle,negative,voice-tests,full-check}.log.
[+] **Android call mute: настоящий PCM и флаг mute передаются через JNI до core** — исправлено 26.10.2026.
Kotlin передаёт исходный buf и snapshot muted на кадр; NativeLib/JNI/C bridge вызывают feed_pcm_muted.
AEC/AGC продолжают получать микрофон, mute зануляет PCM непосредственно перед Opus; wire/DSP/desktop не менялись.
Добавлен секундный mutedFrames и контекст feed-отказа. Постоянный test_android_call_mute:5/5 PASS —
AEC/AGC on/off,20 кадров до mute/во время/после,PTS/эпоха и360 TX callbacks,allocations0->0;
контроль раннего mute воспроизводит ненулевой сигнал. Прежнее тело bridge даёт ожидаемый FAIL с полным cleanup.
Native build PASS; voice/desktop-профиль30/30 PASS; Android unit14/14 PASS;
assembleDebug arm64-v8a+x86_64 PASS, JVM/JNI descriptor (J[SZJ)Z сверён.
check.sh203/203 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8,exit0,168с.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел14;
/tmp/call-android-mute-fix-{voice-tests,voice-details,negative,android-final-build,android-unit,full-check}.log.
Установка/звонок на устройстве не выполнялись; общий аудит голосового и платформенного кода остаётся частичным.
[+] **call_headless: целостность PCM-кадра при переполнении TCP-очереди** — исправлено 26.10.2026.
Header/PCM собираются до отправки; очередь принимает целый кадр или отбрасывает его целиком.
Частичная запись сохраняет хвост перед следующими кадрами. Потери учитываются отдельно для клиента/сервиса;
WARN содержит причину/состояние очереди, итоговые счётчики выводятся при закрытии/destroy.
Постоянный test_call_headless_queue:11/11 PASS — TCP/uasync/Opus, EAGAIN/short-write, точная ёмкость,
переполнение после частичного drain, следующий кадр, destroy с очередью и недопустимый размер/NULL.
Весь полученный поток сравнивается с независимым decoder, allocations0->0; старый код ожидаемо FAIL.
Штатный e2e1/1 PASS; native build PASS; check.sh203/203 PASS,0 failed/0 skipped,
proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,188с. Полный аудит call остаётся частичным.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел12;
/tmp/call-headless-fix-{queue,e2e,full-check}.log. Аппаратный GUI/Android не проверялись.
[ ] **radio_headless: проверить целостность PCM-кадра при переполнении TCP-очереди**.
При сравнении замечен сходный шаблон раздельной отправки header/PCM в rh_write_frame/rh_send_raw.
Runtime overflow отдельно не воспроизводился; нужны диагностика и компонентная проверка до решения об исправлении.
Наблюдение: [call_audit_findings.txt](call_audit_findings.txt),раздел11. Полный аудит рации не выполнен.
[+] **DM media: не выдавать отказ квоты при ошибке чтения занятого места** — исправлено 26.10.2026.
dm_custody_store проверяет step/finalize отдельно;SQL-ошибка возвращает управление без REJECT/регистрации custody.
S сохраняет назначение M и повторяет STORE;настоящий недостаток квоты выдаёт REJECT по прежнему контракту.
test_dm_media дополнен Linux wrappers:4 SQL-отказа без записей/REJECT/STORED,настоящая квота с одним REJECT;
реальная UDP-цепочка с prepare/step/finalize на первых3 STORE,успешный4-й STORE на том же M,
restart S/M при выключенном A,точный файл на B,удаление custody/тела S,поздние control и TTL;allocations0->0.
С прежним dm_media регрессия ожидаемо FAIL;исходный one-shot IOERR-стенд и вариант без fault injection PASS.
Сборка/DM-профиль7/7 PASS;ASAN/UBSan/LSan7/7 PASS,detect_leaks=1,core/libuasync/Opus инструментированы.
Полный check.sh203/203 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,176с.
Подробности полного набора и границы проверки: [dm_audit_findings.txt](dm_audit_findings.txt),раздел6.
[+] **DM: различать отсутствие беседы и ошибку её загрузки, сохранять seq при отказе** — исправлено 26.10.2026.
dm_conv_load различает FOUND/NOT_FOUND/ERROR; только успешный SELECT без строки разрешает создание.
SQL/короткие ключи/X25519 возвращают ошибку без перезаписи беседы, изменения hidden или события успеха.
Проверены все5 callers; приём/invite не пересоздают беседу после отказа. Ошибка сохранения области передаётся наружу.
test_dm_load:79 отказных сценариев, все поля7 DM-таблиц, отсутствие SQL-записей/ложных квитанций,
поколения до/после удаления, restart/продолжение seq и отдельные ошибки подготовки медиа; allocations0->0.
Прежний core даёт ожидаемый FAIL новой регрессии; исходный SQLITE_DENY-стенд теперь PASS,out1/in2 сохранены.
Сборка и DM-профиль7/7 PASS; ASAN/UBSan/LSan7/7 PASS,detect_leaks=1,ядро/libuasync/Opus инструментированы.
Окончательный check.sh203/203 PASS,0 failed/0 skipped;proxy9/9,burst1/1,load8/8 (134217728 байт),175с.
Первый прогон202/203 с отдельным отказом JOIN сохранён;задача диагностики ниже,критерии теста не менялись.
Подробности полного набора и ограничения: [dm_audit_findings.txt](dm_audit_findings.txt), раздел4.
[ ] **test_chat_join_e2e: диагностировать раннее чтение файла приглашения** — замечено 26.10.2026.
Первый общий прогон исправления DM:202/203 PASS; happy A!=C завершился invalid invitation length or prefix у J,
затем A/C не нашли подписанного J. Degraded/invalid-key сценарии прошли. Отдельный повтор3/3 PASS.
wf(link.txt) создаёт файл до vfprintf/fclose, wait_file проверяет только access(F_OK): возможна гонка публикации.
В исходном отказе нет размера/прочитанных байтов, поэтому конкретная причина пока не доказана.
Повторный полный check.sh203/203 PASS;это не доказательство устранения причины исходного отказа.
Добавить диагностику длины/результатов I/O и при подтверждении публиковать файл атомарно после закрытия.
Критерии JOIN не ослаблять. Логи /tmp/dm-load-fix-{full-check-first,join-first,join-repeat}.log;
production JOIN и штатный test_chat_join_e2e в исправлении DM не менялись.
[+] **db_sync: финальная сверка принимает разные хеши при равном count** — исправлено26.10.2026.
db_handle_sync_done явно выставляет synced и вызывает done даже при chain_hash mismatch.
Дополнительный локальный стенд подтвердил ложный done и разные корни одинаковых подписанных записей
с разными неподписанными id. Для200 записей с отличием в первой sparse-поиск даёт fm148;
выбранный хвост состоит из дубликатов, простой повтор не сходится. Стенд exit0, без сетевых пакетов,
/tmp/db-sync-completion-design.c/.mk, лог /tmp/db-sync-completion-design.log.
MESSAGES=2 подключён к общему Merkle; канонические записи/полные корни без local id и сетевого OFFSET.
Собственный bulk/chain удалён; PUSH/ACK сохранены. Wire v4 со свежими round_id/accept_id обеих сторон
изолирует cancel/start от старых PAGE/DONE/ERROR. RNG failure, дубликаты и CHECK/DONE revision проверены.
Локальная вставка и индекс — одна короткая транзакция; страница до16 записей. Preflight16 строк
проверяет подписи вне write-lock; индекс16 листьев за commit, timer1 tick между порциями.
SQL/OpenSSL/NOMEM/неизвестный ключ сохраняют строки, доказанная порча удаляется; late key возобновляет обход.
Deleted автора не фильтрует историю; допуск пересылающего READY-пира проверяется отдельно.
Sound/sleep, callback dispatch/remove и сохранность схемы проверены. TTL ms, короткие DELETE/index commits.
check.sh203/203 PASS,0 failed/0 skipped; proxy9/9,burst1/1,load8/8,STUN/stream PASS,exit0,179с.
ASAN/UBSan/LSan10/10 PASS,detect_leaks=1. Крупный сценарий5000 новых записей:5002/5002,полные корни равны.
После общего прогона добавлены только крупный тест и CHECK/DONE:оба PASS, последний также sanitizer PASS.
Логи /tmp/history-merkle-verified-check.log, /tmp/history-merkle-asan-results.log, /tmp/history-merkle-large.log.
[member_merkle_audit_findings.txt](member_merkle_audit_findings.txt), раздел13; исходный план — раздел12.
[+] **db_sync: отказ проверки/битый дубликат удаляли корректную историю** — исправлено26.10.2026 после rebase на03b05094.
VALID/BAD_SIGNATURE/BAD_FORMAT/DEFERRED/ERROR разделены; перед DELETE проверяется сама сохранённая копия.
SQL/OpenSSL/NOMEM/неизвестный ключ сохраняют данные; доказанная порча удаляется в транзакции.
Recalc сохраняет pending при отказах; после COMMIT удаления сбрасываются позиции пиров и цепочка строится с0.
Прежний стенд сохранения2/2 PASS; новый test_db_sync_integrity:10 введённых отказов, реальные SQL-триггеры,
формат0/1/63/65/255/4001, два канала, откат удаления/COMMIT/next_id, несколько батчей, allocations восстановлены.
check.sh203/203 PASS,0 failed/0 skipped; proxy9/9,burst1/1,load8/8,exit0,195с.
STUN по новому запросу пользователя PASS отдельно и в общем наборе; прежний bind assert не воспроизведён,
первый падающий коммит/причина не установлены. ASAN/UBSan/LSan8/8 PASS с detect_leaks=1.
Логи /tmp/db-sync-full-final-check.log, /tmp/db-sync-integrity-final.log, /tmp/db-sync-asan-results.log.
Подробности: [member_merkle_audit_findings.txt](member_merkle_audit_findings.txt), раздел11.
[+] **db_sync: неверный SQL и неоднозначная идентичность ACK доставки** — исправлено26.10.2026.
ACK_PUSH содержит message_id[32]; один идемпотентный UPDATE адресует только конкретную запись.
Счётчик отражает ACK, а не очередь отправки; WAS_SENT — только для локального автора. Все SQL-отказы логируются.
Две записи одного автора с одним timestamp проверены отдельно и через настоящие PUSH/ACK;
повтор ACK не увеличивает счётчик. Общий и sanitizer прогоны выше PASS, совместимость старого wire не нужна.
Подробности: [member_merkle_audit_findings.txt](member_merkle_audit_findings.txt), раздел11.
[+] **feedback: исключение и отложенное удостоверение не блокируют запуск чата** — исправлено 26.10.2026.
Исключённые/ожидающие авторы скрываются из результатов, Merkle и выдачи; записи, ver и отмены сохраняются.
Единый member_sync_author_key различает ACTIVE/INACTIVE/PENDING/INVALID/ERROR; radio/attachments остаются строгими.
Изменение удостоверения/deleted/общего ключа атомарно обновляет зависимые feedback-листья во всех каналах.
Отложенный signer не блокирует MEMBERS; поступление его ключа через другой канал включает допустимую историю.
MS_APPLY_FEEDBACK переносится через внешний commit; профиль/stream options не запускают лишние раунды.
Отзыв завершает ожидание текущего раунда, сохраняя подписку; возврат запускает обмен по прежнему UP-транспорту.
Регрессии: restart, реакция/ответ/отмена, purge, два канала, межканальный ключ, mixed page,
SQL/index/commit faults и внешний rollback без событий; новый сетевой D и новые изменения вернувшегося A.
Окончательный check.sh200/200 PASS,0 failed/0 skipped; proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,186с.
ASAN/UBSan/LSan15/15 PASS,detect_leaks=1; core/libuasync/Opus инструментированы согласованно.
Логи /tmp/feedback-author-fix-full-final-check.log и /tmp/feedback-author-fix-asan-final-results.log.
Исправление закрыто; полный аудит остальных контрактов остаётся частичным, push не выполнялся.
Подробности: [member_merkle_audit_findings.txt](member_merkle_audit_findings.txt), раздел9; исходные дефекты — разделы7–8.
[+] **member_sync: атомарная очистка повреждённых записей с передачей ошибок** — исправлено 26.10.2026.
Очистка владеет транзакцией канала от чтения до commit удаления/индекса; чужая транзакция
отклоняется до изменений. Ошибки чтения/проверки/памяти/SQL/commit возвращаются через init/start.
Недействительная подпись отделена от отсутствующей зависимости и SQL/OpenSSL-отказа,
включая sc_ed25519_verify и SQL-getters ключей. Корректные данные при отказе не удаляются.
Props/mute/группа меняются после commit; self-unsubscribe и повторный вход безопасны.
Штатный test_member_adapter расширен test_member_purge: SQL/commit/allocator faults,
24 crypto faults, identity hash, malformed BLOB, placeholders/deferred и init/start/retry.
Реальный stream_member проверяет rollback mute/READY/reader, успешное удаление,
сохранение другого канала и внешнего NCD-владельца. Профиль11/11 PASS.
Общий набор200/200 PASS в IPv6 namespace; proxy9/9,burst1/1,load8/8,134217728 байт PASS
отдельно в root network/mount namespace. Первые отказы среды и повторные прогоны описаны в отчёте.
ASAN/UBSan/LSan11/11 PASS,detect_leaks=1; lib/src/Opus инструментированы согласованно.
Полный аудит остальных контрактов остаётся частичным; wire/формат БД не менялись.
Подробности: [member_merkle_audit_findings.txt](member_merkle_audit_findings.txt).
[+] **test_radio_mesh: учитывать дедупликацию на любом узле diamond** — исправлено 26.10.2026.
Проверяется ровно5 упорядоченных кадров и один FIN у B/D/C, отсутствие эха у A и дедупликация всего burst.
Unix-наблюдение ждёт обработки всех отправленных RADIO-пакетов; gate воспроизводит обходную копию раньше A→B/A→D.
Обычный сценарий и принудительные dupB6/dupC0,dupD6/dupC0 проходят; паузы/диагностический хвост удалены.
Один прогон и16 параллельных повторов (по3 сценария) PASS, allocations0->0; сборка без wrappers также PASS.
Отрицательные стенды: старое условие dupC>0 и лишняя локальная доставка ожидаемо FAIL без утечки.
После rebase нативная/CMake-сборки PASS, C++ voice17/17 PASS; check.sh202/202 PASS,0 failed/0 skipped,
proxy9/9,burst1/1,load8/8 (134217728 байт),exit0,176с. Production radio/uasync не менялись.
Логи /tmp/radio-mesh-fix-{test,repeat-results,negative-results,voice-tests,full-check}.log;
подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел10. Windows/FreeBSD/GUI/Android не запускались.
[+] **call_audio: изолировать pending TX-задачи разных привязок instance** — исправлено 26.10.2026.
Отдельная эпоха привязки ограждает pending, отправку и поздние post-time/age-drop;
старые звонки того же owner освобождают свои слоты, повтор init идемпотентен.
Предел8 относится к активной привязке; uasync освобождает старые embedded tasks без дополнительных owner-объектов.
Добавлены INFO эпох init/destroy, TRACE отклонённой задачи и ERROR нарушения счётчика.
Постоянный test_call_audio_tx:13/13 PASS, allocations0->0; прежний C++ voice-профиль4/4 PASS.
Те же тесты со старым voice-архивом:7/13 PASS, шесть ожидаемых отказов; production не подменялся.
Нативная/CMake-сборки PASS. Два полных check201/202: test_radio_mesh с dupC=0,
одиночный повтор PASS; флак вынесен отдельно. Подробности полного прогона — раздел8 отчёта.
После исправления критерия mesh и rebase полный check202/202 PASS; актуальная проверка — раздел10 отчёта.
Интеграции выполнены отдельно: proxy9/9,burst1/1,load8/8 (134217728 байт) PASS.
Аппаратный GUI/Android и санитайзеры не запускались; полный аудит call/ ещё не завершён.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),разделы6–8.
[+] **call: запрещать второй исходящий вызов при незавершённом обычном звонке** — исправлено 26.10.2026.
call_start до session/CM/INVITE отклоняет новый ID при обычной занятости с ENDED/BUSY.
Уже зарегистрированный ID, включая ENDED до очистки, отклоняется без события/изменения старого звонка.
INFO содержит новый/занятый ID, peer/ch и состояние; нулевые peer/id отклоняются с ERROR.
Штатный test_call_admission:547 assertions,22 BUSY,18 отказов без событий,7 сочетаний состояния/роли,
CHAT/DM/другой tuple до поиска маршрута, отсутствие новых CM/INVITE/started, сохранение состояния/таймеров,
двустороннее Opus RX, новый вызов при старом tombstone, продолжение PHONE_BUSY и взаимодействие с рацией.
Полные join/update/tree подписи fixture через member_sync; tracked allocations0->0.
Прежний диагностический стенд5/5 теперь PASS. Сборка PASS, C++ voice4/4 PASS,
check.sh202/202 PASS,0 failed/0 skipped; proxy9/9,burst PASS,load8/8,134217728 байт,174с.
Полный прогон в отдельном IPv6 namespace, сеть хоста сохранена. Санитайзеры/аппаратный GUI/Android не запускались.
Подробности: [call_audit_findings.txt](call_audit_findings.txt),раздел5;
логи /tmp/call-admission-{fix-regression,voice-tests,full-check,netns}.log. Остальной аудит src/call частичный.
[+] **stream: закрывать INFO-пробу при истечении её 9-байтового запроса** — исправлено и проверено 26.10.2026.
expire_controls сверяет length9 и текущий probe_id; истёкший INFO закрывает flow с OVERLOAD.
Лог содержит размер, возраст control и факт закрытия flow; чужой probe_id не закрывает новую попытку.
test_stream_control_expiry4/4 PASS: обе media-формы, SUBSCRIBE8, до/на/после TTL и несовпадающий ID.
test_stream_service проверяет оба media-режима: настоящая очередь ETCP удерживается4,2с,
второй INFO истекает в control FIFO до общего deadline5с; после разблокировки ответ/повторное открытие успешны.
Общее ETCP-соединение и handles публикации/подписки сохраняются; cleanup без tracked leaks.
Сборка PASS. Первый check198/201: три отказа из-за отсутствующего ::1 при отключённом IPv6 окружения.
В отдельном IPv6 namespace check201/201 PASS,0 failed/0 skipped; proxy9/9,burst PASS,load8/8,134217728 байт,175с.
Настройки основного namespace не менялись. Логи /tmp/stream-info-expiry-fix-build.log,
/tmp/stream-info-expiry-fix-unit.log, ...-integration.log, ...-ipv6-full-check.log.
Подробности: [stream_audit_findings.txt](stream_audit_findings.txt),разделы8–9. Аудит остальных контрактов stream частичный.
[ ] **tests: явно проверять IPv6-предпосылку полного набора** — замечено 26.10.2026.
В основном окружении disable_ipv6=1 для all/default/lo, ::1 отсутствует; bind(::1) даёт EADDRNOTAVAIL.
Из-за этого падают test_ncd_faults, test_ipv6_sockets и test_route_connectivity до проверки своей логики.
Предусмотреть ясную предварительную диагностику/подготовленный отдельный namespace без изменения сети хоста.
Все три проверки и полный набор201/201 проходят в таком namespace; пропуски не требуются.
Подтверждение: /tmp/stream-info-expiry-host-ipv6.log и /tmp/stream-info-expiry-fix-ipv6-env.log.
[+] **stream: упаковывать помещающийся Opus без обязательного резерва под видео** — исправлено и проверено 26.10.2026.
Без ожидающего видео используется вся ёмкость пачки, включая точное заполнение.
Крупный Opus уходит отдельно; ожидающее видео получает второй слот actor без coalescing delay.
Пакет, не помещающийся даже отдельно, удаляется с WARN размера/максимума/ID/PTS до блокировки следующей записи.
Wire и неделимость Opus сохраняются; feed=1 подтверждает копирование в очередь, в том числе во время OPENING.
test_stream_publisher включён в общий check:23 сценария PASS, полученный Opus декодируется,
видео проверяется по байтам/offset, P-кадр не запускает media timer; tracked allocations восстановлены.
После merge origin/master повторная сборка PASS, профиль8/8 PASS; check.sh200/200 PASS,0 failed/0 skipped.
Интеграции: proxy9/9,burst PASS,load8/8 (134217728 байт), полное время175с.
Логи /tmp/stream-publisher-fix-merged-build.log, /tmp/stream-publisher-fix-profile-results.log,
/tmp/stream-publisher-fix-full-check.log. Полный аудит остальных контрактов stream не закрыт.
Подробности: [stream_audit_findings.txt](stream_audit_findings.txt), разделы6–7.
[+] **stream: учитывать подписанный deleted при загрузке мембера** — исправлено 26.10.2026.
stream_member_load читает deleted вместе с профилем и возвращает STREAM_ERR_NOT_MEMBER до crypto,
с отдельным логом причины/группы/узла. WAIT для неполного профиля и выходы при отказе сохраняются.
test_stream_member включён в общий check и использует fixture test_stream_service в отдельном процессе.
Проверены подписанные запрет/разрешение, неверная подпись/старая версия, source/supplier/self,
закрытие handles/выдач, отсутствие новых DATA callback, Merkle-остановка publisher,
другой канал с рабочей записью и чужое NCD-владение; после разрешения нужны новые handles/эпоха.
Воспроизведение: /tmp/stream-audit-member-deleted.c и /tmp/stream-audit-member.mk;
до исправления /tmp/stream-audit-member-deleted.log, exit=1; архитектурная копия PASS,
/tmp/stream-arch-results-final.log. Штатная сборка PASS, профиль7/7 PASS, tracked allocations=0.
check.sh:195/195 PASS,0 failed/0 skipped; proxy9/9,burst PASS,load8/8 (134217728 байт),167с.
Логи: /tmp/stream-member-fix-build.log, /tmp/stream-member-fix-profile-results.log,
/tmp/stream-member-fix-full-check.log. Полный аудит остальных контрактов stream этим не закрывается.
Подробности: [stream_audit_findings.txt](stream_audit_findings.txt).
[+] **utun_instance: завершить аудит владения, lifecycle и reload** — завершено 26.10.2026 после config_parser.
Исправлены вложенные подписки, отказ конструкторов, индекс TCP-реестра, жизнь пути общего logger,
selective/full reload, TCP listener teardown, освобождение очередей/подписок и SQLite BUSY.
Headless start/stop освобождает контексты/клиентов/таймеры; PCM и radio hop-list читаются выровненно.
Заголовок и документ полностью сверены; core-разделы service_lifecycle.md согласованы, общий [~] сохранён.
Новый test_instance_contract покрывает callbacks/OOM, reload/fault/retry, duplicate servers, database/logger
ownership и headless lifecycle. Сборка PASS; check.sh196/196 PASS,0 skipped,proxy9/9,burst1/1,load8/8,173с.
ASAN/UBSan/LSan11/11 PASS,detect_leaks=1; ограничения partial reload и нативных платформ описаны
в [utun_instance_audit_findings.txt](utun_instance_audit_findings.txt).
[+] **config_parser: завершить сверку и строгий разбор значений** — завершено 26.10.2026.
Исправлены IPv6 /32, control /0 и порядок ip/port, числовые границы/таймеры, storage overflow,
потерянные ошибки обработчиков и памяти, повторное включение proxy, HEX и диагностический dump секретов.
Config updater нормализует ошибочные ранние дубли выбранными последними значениями без смены идентичности.
Заголовок и документы согласованы; профиль6/6, ASAN/UBSan/LSan6/6, detect_leaks=1.
Полный check.sh195/195 PASS,0 skipped,proxy9/9,burst1/1,load8/8,134217728 байт,169с.
Подробности, платформенные ограничения и логи: [config_parser_audit_findings.txt](config_parser_audit_findings.txt).
[+] **chat JOIN: исправить кодек, владение попыткой и покрыть нюансы тестами** — завершено 26.10.2026.
Общий C-кодек для Qt/JNI/headless, отдельные request/attempt IDs, защита от старых ответов,
потокобезопасный порт команд, целостный запуск и очистка до уведомления, SQL/runtime-откат.
Кодек116342 случаев; отказы памяти/таймеров/NCD/send, REALITY, SQL, повторные callbacks,
чужие владельцы и JOIN, детерминированная гонка stop/producer и отмена извлечённого batch — PASS.
check.sh196/196, proxy9/9, burst1/1, load8/8 (134217728 байт); ASAN/UBSan/LSan6/6, detect_leaks=1.
Desktop/Android APK build PASS, Qt invite/local UI2/2. JOIN wire изменён без совместимости со старыми узлами.
Подробности: [chat_audit_findings.txt](chat_audit_findings.txt). Коммит/push не выполнялись.
[ ] **chat: продолжить аудит заголовков и документов** — проверка всего каталога не завершена.
Приглашения исправлены по согласованному плану: общий кодек, токены попыток, целостный запуск,
SQL/runtime-границы, единое завершение и отдельный потокобезопасный порт команд GUI/JNI.
Добавлены test_invite_codec и test_chat_join_faults; подробности и результаты:
[chat_audit_findings.txt](chat_audit_findings.txt). Остальные непроверенные пункты чек-листа сохраняются.
[+] **transport_layer: завершить аудит заголовков и документов** — завершено 26.10.2026.
С учётом прежних этапов сверены все32 собственных заголовка и17 документов каталога плюс doc/stun_nat.md.
Закрыты auto_socket/socket_monitor/SOCKS/call_ring/REALITY/STUN; исправлены владение endpoints/posts,
потеря TCP banner, ошибки запуска/очистки, relay HUP/half-close и двойной STUN recheck.
Обычная сборка PASS; check.sh194/194 PASS,0 skipped, proxy9/9,burst1/1,load8/8,134217728 байт,170с.
Динамические IPv4/IPv6 auto_socket PASS; проверки build.sh44/44; ASAN/UBSan/LSan12/12,detect_leaks=1.
Два первых отказа sanitizer-стенда вызваны отсутствием glibc FORTIFY wrappers; исправлены, повторные прогоны PASS.
Нативные сборки/runtime Windows и FreeBSD не выполнялись; платформенные ветки сверены по исходникам.
Подробности, ограничения и логи: [transport_layer_audit_findings.txt](transport_layer_audit_findings.txt).
[+] **socket_monitor: исправить FreeBSD-разбор уведомлений адреса** — исправлено 25–26.10.2026.
Прежний bsd_parse_rtm безусловно писал gw[0] при NULL из RTM_NEWADDR/RTM_DELADDR.
Ненужный разбор sockaddr удалён; уведомления проверяются по длине/версии и правильным native структурам
ifa_msghdr/if_msghdr/if_announcemsghdr/rt_msghdr с копированием выровненного заголовка.
Также исправлены исчезновение адресов, IPv6 scope, изменение endpoints внутри callbacks и pending Windows post.
Linux native-fixture, init faults и ASAN/UBSan/LSan PASS; FreeBSD определения сверены с releng/14.3.
Нативный runtime FreeBSD недоступен; он не подменяется Linux-тестом. Заголовок и lifecycle-контракт согласованы.
[+] **packet_dump: ограничить чтение входного IPv4-пакета** — исправлено 25.10.2026 по указанию пользователя.
Единый hex-цикл читает min(32, total length-IHL) байт; проверяются NULL/version/IHL/total length,
хвост после total length игнорируется. Адреса читаются побайтно; последующие IP-фрагменты
не выдаются за транспортный заголовок. Исправлены обрезание полного набора TCP-флагов и вывод нулевого порта.
Ошибки входа отражаются в строке и логируются в dump/warn, частичный первый фрагмент — dump/debug.
Прежний тест не вызывал модуль; теперь проверяет реальный форматтер: протоколы, поля, фрагменты,
77 длин захвата, невыровненный вход, сохранность входа и точный hex в2904 случаях.
Контракты .h/документа согласованы. Код235c2c4f, тестыc3fa6d4d; rebase на e035f53a без конфликтов.
Обычная сборка PASS; полный check.sh после rebase:190/190 PASS,0 skipped,
proxy9/9,burst1/1,load8/8 PASS,134217728 байт,214с. ASAN/UBSan/LSan packet_dump PASS сdetect_leaks=1;
встроенные lib/src/Opus в изолированной копии собраны с согласованными sanitizer-флагами.
Логи: /tmp/packet-dump-fix-test.log, /tmp/packet-dump-fix-full-check.log,
/tmp/packet-dump-fix-asan-build.log, /tmp/packet-dump-fix-asan-test.log.
[+] **member_files: сверить контракты и исправить ошибки владения/повторов** — завершено 24.10.2026.
Прочитаны все6 реализаций и6 заголовков C-модуля. Исправлены удаление чужого temp,
повторный запуск после раннего cancel с UAF, обрыв массовых мутаций при64 квитанциях
и блокирующий open FIFO до проверки типа. Заголовки и серверная/клиентская документация согласованы.
Подтверждены сохранность файлов при трёх отказах, 76 созданий/76 удалений и потеря успешного ответа без повторной мутации.
Сборка PASS; профиль7/7, ASAN/UBSan/LSan4/4 PASS с detect_leaks=1. Общий GUI/avatar документ остаётся [~].
Отчёт и границы: [member_files_audit_findings.txt](member_files_audit_findings.txt).
Логи: /tmp/member-files-audit-profile-results.log, /tmp/member-files-sanitizer-results.log.
Прежний FAIL удаления чужого temp из ETCP-проверки закрыт коммитом0f42f92f;
защита от возобновления после раннего cancel —20b4975d. Повторный фикс не требуется.
После объединения с ETCP-диагностикой: сборка PASS, общий check.sh186/186,
proxy9/9, burst1/1, load8/8 PASS,134217728 проверенных байт,162с.
Сохранность target/temp подтверждена всеми тремя сценариями; test_member_files_network PASS phase21.
Логи: /tmp/etcp-master-integration-build.log, /tmp/etcp-master-integration-check.log.
[+] **media_async/media_delivery: завершить сверку контрактов и исправлений** — завершено 24.10.2026.
Проверены все11 заголовков двух каталогов по полным реализациям; исправлены alias файлов,
@@ -113,6 +762,9 @@
Windows/FreeBSD runtime не проверялся.
[ ] **ETCP: полезная утилизация нескольких каналов и доставка при зависании части линков** — общая задача открыта.
Небольшой недобор до условного порога85% сохраняется как результат измерений;
сам по себе не оправдывает изменения/усложнения BBR. Далее по приоритету — STCP backpressure,
смена RTT нескольких путей/BW и повторный отказ; строгие критерии — доставка, паузы и рост очередей.
После push исследован одиночный дуплекс2Мбит/с на изолированной базе a198cfb9.
Точные7763 ACK показывают задержку обратной очереди: пример DATA15.909мс, hold0.029мс,
обратный ACK32.219мс; DOWN продолжает ждать flight7644 при цели5915байт.
@@ -682,11 +1334,41 @@
Получатель принял приглашение и дважды отправил ACK; до проверки в логе отправителя нет приёма этого ACK. Отдельный повтор прошёл.
Логи: /tmp/utun-stream-preview-local-transport-failure.log, /tmp/utun-stream-preview-local-transport-retry.log.
[ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17–20.10.2026:
[x] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — исправлено 25.10.2026:
test_stream_view::realH264RenderingAndClose нестабильно получает silenceGaps=3–4 вместо 2.
Зафиксированы pcm_short с got=96/480 и 288/480; видео продолжало выводиться.
Сбой повторялся с общей нагрузкой и после её завершения; изолированные повторы проходили.
Проверить актуальные PCM workers/часы под нагрузкой; единичный PASS не подтверждает устранение.
Повтор 25.10 при проверке восстановления потоков: silenceGaps=3 вместо 2, got=384/480;
stream-перезапросов/GAP в этом локальном воспроизведении не было. Проверка шла рядом с полным C-прогоном.
Новый лог: /tmp/utun2-key-qt-tests-verified.log; отдельный повтор: /tmp/utun2-key-qt-isolated.log.
Диагностика 25.10 в utun2: изолированный сбой воспроизведён до искусственного stall видеодекодера.
В момент провала got=96/480 (8мс тишины), PCM_before=96, tempo=1000, worker_age=239мкс;
последний Opus feed был 104585мкс назад. Следующие 100мс аудио уже опубликованы 10771мкс назад,
но ещё не поданы в decoder. Источник выдаёт 100мс аудио с фактическими интервалами 95–106мс.
Старт с KEY оставляет 33мс аудио до головы; DSP имеет initial_latency=30мс, предзаполнения нет.
PCM_target=250мс ограничивает дальнейшую подготовку, но не задаёт минимальный запас перед стартом/возобновлением.
reserve включает 200мс очереди устройства и опубликованные, ещё не декодированные пакеты;
minimum=233мс при guard=250мс лишь отключает ускорение, не восстанавливает недостающий запас.
Это реальная нехватка PCM на границе пакетной подачи и асинхронной подготовки, а не ложный silenceGaps
или доказательство блокировки аудио видео-декодером. Проверка после фиксированных 400мс зависит от фазы таймеров.
Добавлены снимки PCM/темпа/guard/времени callback и отдельные эпизоды underrun;
UTUN_TEST_AUDIO_DIAG=1 включает замеры публикации/кодирования источника и тишины тестового устройства.
Воспроизведение: /tmp/utun2-audio-diag-repro.log, /tmp/utun2-audio-episodes-repro.log (повтор 2 из 5).
Отдельный TRACE-прогон video-stall прошёл: 500мс задержки видео, 63352 ненулевых sample, новых пауз нет.
Лог /tmp/utun2-audio-causal-trace.log; это подтверждает отдельность audio worker, но не устраняет PCM starvation.
Исправление: общий PCM consumer при старте/underrun ждёт уже заданные 250мс готового PCM.
Накопление сохраняет decoder и PTS; desktop/Android держат темп 1x до возобновления.
EOF снимает ожидание порога, ограничение pull по PTS не считается нехваткой PCM.
Счётчик preparedFrames публикуется после готового SPSC-слота, чтобы порог не учитывал ещё недоступный PCM.
Детерминированная регрессия без таймеров падает до фикса и проходит после: 33мс после KEY,
пачки по 100мс с джиттером 95–106мс + 20мс до feed, 767 полных callback без провала после старта.
Отдельно проверены 600мс паузы, дробное поступление, возобновление/PTS, ограничение PTS и короткий EOF 20мс.
Старые проверки точной длительности/темпа, stereo, конкурентного producer/consumer и позднего аудио проходят.
GUI test_stream_view прошёл 8 повторов подряд; итоговый desktop stream-набор 4/4,
Android stream unit tests 55/55, APK и основная C-сборка успешны; часы и Android bridge также проверены.
Логи: /tmp/utun2-audio-regression-before.log, /tmp/utun2-audio-recovery-test-after.log,
/tmp/utun2-audio-fix-view-repeat.log, /tmp/utun2-audio-fix-stream-tests.log, /tmp/utun2-audio-fix-android.log.
Логи: /tmp/stream-zoom-full-view-audio-gap.log, /tmp/stream-zoom-full-view.log,
/tmp/multiview-review/desktop-review.log, /tmp/multiview-review/audio-original.log.
@@ -805,13 +1487,16 @@
нескольких ретрансляторов, нескольких автоматических источников/групп и mobile standby на устройстве
(doc/stream.txt, раздел 16). Сохранённые проверки отдельных этапов не закрывают всю матрицу.
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
[+] **Member files: исчерпание кеша результатов массовых изменений** — исправлено 24.10.2026.
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
следующая команда получает MF_BUSY, остаются 12. storage.c: STORE_RECEIPTS=64, RECEIPT_TB=1200000 (120 секунд).
Лимит общий для успешных COMMIT/MKDIR/RENAME/REMOVE; затрагивает также рекурсивное копирование.
Нужен отдельный этап управления временем жизни receipts с сохранением идемпотентности повторных запросов;
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
Сохранены64 квитанции/120с без досрочного вытеснения. BUSY.cursor сообщает ближайшее освобождение,
клиент этих мутаций ждёт отменяемый таймер с прежним ID/параметрами и общим сроком120с без продления.
Новая сетевая регрессия через настоящий клиентский API:76 созданий, потеря успешного ответа,
затем76 удалений; QUOTA подтверждает76 и0 каталогов без дубликатов. Обычный и sanitizer-прогоны PASS.
Старый Qt-stub вызывает storage напрямую и по-прежнему видит допустимый сырой MF_BUSY;
он не проверяет новый клиентский retry. Подробности: [member_files_audit_findings.txt](member_files_audit_findings.txt).
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
+92
View File
@@ -0,0 +1,92 @@
Аудит оставшихся контрактов transport_layer — 25–26.10.2026
Область и итог
Закрыты оставшиеся auto_socket, socket_monitor, socks_client, call_ring,
reality/reality_fingerprint/reality_record/reality_io/reality_relay и stun_nat.
Полностью прочитаны их реализации, заголовки и связанные участки владельцев/API.
С учётом ранее завершённых ETCP/STCP/crypto/normalizer/NCD/dummynet/packet_dump/CRC
сверены все 32 собственных заголовка и 17 документов src/transport_layer, а также doc/stun_nat.md.
Вложенный BBR/bbr_v3.c не имеет отдельного заголовка/документа и относится к ранее сверенной
реализации etcp_bbr. Это закрытие аудита описаний и найденных
при нём дефектов, а не утверждение об отсутствии любых возможных ошибок транспорта.
socket_monitor
- Удалён разбор FreeBSD sockaddr, безусловно писавший gw[0] при переданном NULL.
Уведомления разбираются по родному типу if_msghdr/ifa_msghdr/if_announcemsghdr/rt_msghdr;
проверяются длина и версия, используется только нужный индекс интерфейса.
- При исчезновении адреса сбрасываются старые IPv4/IPv6, сохраняется порт, корректируется IPv6 scope.
- Обход callbacks использует ID/поколение: удаление/пересоздание сокетов не оставляет висячий next.
- Linux проверяет источник, границы netlink, EINTR/overflow; init откатывает каждый отказ.
- Windows объединяет уведомления и отменяет pending post при остановке после завершения native producers.
auto_socket
- UDP factory возвращает именно созданный endpoint; один общий reconcile заменяет дубли IPv4/IPv6 × UDP/TCP.
- При замене сначала снимается старый слот, неудачная попытка не оставляет dangling pointer;
частичный начальный запуск откатывается, последующие ошибки сохраняют работающие endpoints.
- Для wildcard TCP listener отдельно хранится опубликованный IP интерфейса: неизменённый listener стабилен,
реальная смена адреса/типа пересоздаёт его. DOWN/исчезнувшие интерфейсы удаляются после полного scan.
- Порты SQLite проверяются по типу/диапазону, атомарный INSERT не оставляет открытую транзакцию.
RNG проверяется, 65535 доступен. Остановка сохраняет кеш портов, удаление интерфейса очищает его.
- Внешние/Windows scans объединяются в отменяемый post; native источники отключаются до освобождения.
Внешние producers должны завершиться до destroy согласно контракту.
- Android reload сохраняет одинаковые port=0 endpoints, учитывает scope/опции и обновляет ссылки client links
до освобождения старого config. Ошибка частичного reload логируется, новый config остаётся владельцу instance.
- Проверены длины/границы Linux/FreeBSD уведомлений, ошибки nonblocking/register и Windows API.
socks_client
- CONNECT читает ровно control frame и сохраняет следующий TCP banner для владельца.
- Start не вызывает completion inline при немедленном connect; timer/nonblocking обязательны с полным rollback.
- Проверяются пределы строк, VER/RSV/REP, relay port/ATYP; wildcard UDP BND IP берётся из TCP peer.
- Отправка учитывает EINTR, SIGPIPE и нулевой прогресс, проверяются переключения write monitoring.
- UDP send возвращает payload bytes, не wire bytes; размер проверяется до сложения, NULL/0 допустимы.
Unwrap сохраняет выходы при отказе и допускает in-place payload.
- DNS start остаётся синхронным getaddrinfo с первым адресом; это явно отражено в контракте.
call_ring
- История отдельная для каждого потока; насыщенный count сохраняет последние 1024 записи после wrap idx.
- Указатели печатаются через PRIxPTR, включая Win64. Dump учитывает short write/EINTR и отказ файла,
продолжает stderr без обычного logger/выделений памяти. snprintf не объявлен async-signal-safe.
REALITY
- Уточнены одноразовость session, zero-init/cleanup, сохранение traffic после Finished,
границы codec, отсутствие replay-защиты в stateless обёртках и отдельное владение owner/I/O.
- Traffic init сбрасывает generation, KeyUpdate увеличивает его; replay entries различают пустой slot и Unix time=0.
- Ошибки client_start полностью удаляют новое I/O; send забирает вход только при успехе.
- Resume сохраняет штатный finish при close_notify; transient recv EINTR не закрывает поток.
- Relay строго проверяет destination port/timeout/длину prefix, mandatory nonblocking/timer/monitor setup.
SIGPIPE/EINTR/send=0 обрабатываются, счётчики учитывают каждый отправленный байт.
- HUP дочитывает остаток/EOF: прежде uasync снимал регистрацию без except handler и relay ждал idle timeout.
Регрессия отключает idle timeout и требует полного двустороннего half-close.
- Закрытые relay остаются в owner до call_soon; отказ его выделения не теряет контекст.
Shutdown отменяет отложенные frees и синхронно освобождает оба списка до уничтожения owner/UASYNC.
STUN
- Recheck одним scan перезапускает старые и создаёт новые пути по одному разу; раньше новый путь
немедленно открывал второй тестовый fd, расходуя первый свежий порт без измерения.
- Уточнены исключение TCP/probe_only/SOCKS/only_local из API, SKIPPED IPv6/неизвестного IPv4,
статусы результата, неизменность выхода при -1 и обязанность caller проверять TTL.
- Контракты и RFC-последовательность сверены с https://www.rfc-editor.org/rfc/rfc5780
и https://www.rfc-editor.org/rfc/rfc8489; обычный MATCH не объявляется доказательством EIM.
Проверки
- ./build.sh --no-gui -j4: PASS, /tmp/transport-audit-final-build.log.
- ./check.sh: 194/194 PASS, 0 failed/0 skipped; proxy 9/9, burst 1/1, load 8/8,
134217728 проверенных байт, 170 секунд; /tmp/transport-audit-final-check.log.
- Динамические IPv4/IPv6 сценарии auto_socket во всех UDP/TCP режимах и 8 циклах: PASS,
/tmp/transport-audit-auto-dynamic-final.log, в отдельном Linux netns.
- Проверки скрипта сборки: 44/44 PASS, /tmp/transport-audit-build-script.log.
- ASAN/UBSan/LSan: 12/12 PASS с detect_leaks=1/ halt_on_error=1:
socket_monitor, auto_socket_faults, socks_client, call_ring, reality_record, reality_hello,
reality_tls, reality_fallback, reality_relay_faults, stcp_reality_rotation, stun_nat, tun_route.
Все встроенные lib/src/Opus инструментированы согласованно в /tmp/utun-packet-dump-asan-20261025.
Сборка: /tmp/transport-audit-asan-build.log и /tmp/transport-audit-asan-tests-build.log.
Первый прогон /tmp/transport-audit-asan-results.log: 10 PASS и 2 отказа тестовых перехватов glibc FORTIFY.
Добавлены checked recvfrom/poll wrappers; оба повторных прогона PASS, без отчётов санитайзеров:
/tmp/transport-audit-asan-test_socket_monitor-final.log,
/tmp/transport-audit-asan-test_tun_route-final.log. Последние изменения только стенда проверены адресно.
Остальные индивидуальные логи: /tmp/transport-audit-asan-test_*.log.
- Нативные runtime/сборки Windows и FreeBSD недоступны и не заявляются.
FreeBSD типы уведомлений сверены с официальными заголовками releng/14.3:
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/if.h
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/route.h
Полнота [V] означает сверку описаний по исходникам; платформенные runtime-ограничения сохранены здесь явно.
+104
View File
@@ -0,0 +1,104 @@
Аудит utun_instance — 26.10.2026
Объём
Полностью прочитаны utun_instance.c/.h и прежний utun_instance_doc.md; сверены
вызовы конструкторов/start/stop/reload из CLI/встраивания и связанные механизмы
ETCP/NCD/STCP, sockets, logger, очередей, SQLite и независимых сервисов.
Заголовок и документ переписаны по фактическому коду. Core-разделы
doc/service_lifecycle.md согласованы; весь поперечный документ остаётся [~].
Это не полный аудит реализаций chat/call/radio/stream или GUI/Android.
Исправления
- Activity/peer-sleep подписки удерживают снятые записи до выхода из внешней
рассылки. Безопасны удаление себя/соседа, вложенное событие и добавление новой
подписки. Повтор fn+arg не дублируется; более новая эффективная activity
отменяет остаток старой рассылки. OOM обеих подписок логируется.
- Конструкторы проверяют UASYNC/config/path до побочных действий; общий путь
создания освобождает частичные ресурсы, сохраняя caller config при отказе.
Проверяются crypto derivation, запись/close временного файла, создание каталогов
и индекс сетей. Старый TCP-реестр индексируется по node_id, а не указателю link.
- Выбранный путь общего logger копируется в постоянный буфер; освобождение config
больше не оставляет заимствованную строку в logger. Уже открытый файловый logger
сохраняет первый путь при создании/reload другого instance.
- Reload готовит config/firewall/networks до изменения ресурсов. Смена владельцев
(в том числе db_path, control, TUN/NAT/proxy), подсетей или automatic server-конфига
пересоздаёт ядро с прежними core/UTUN/chat/running. Повторные имена server также
выбирают полный путь, сохраняя отдельные секции parser без неоднозначного matching.
- Manual reload сравнивает все server-параметры и IPv6 scope по конфигурации,
сохраняя неизменённые sockets/ephemeral ports. TCP listener закрывается вместе
с правильной TCP-моделью; UDP создаётся/удаляется своим API. Устранён лог через
освобождённое имя. Probe и endpoints других владельцев не удаляются по отсутствию
server-секции. Конфигурационные clients обновляются также без UTUN; неизменённые
handles/таймеры сохраняются, копии не ссылаются на освобождённый config.
- Destroy обходит sockets/connections от свежей головы после callbacks,
освобождает старые TCP queue entries и оставшиеся transport-подписки.
Повторный destroy/start/reload при разрушении отклоняется; SQLite close ошибки
видны, BUSY завершается close_v2 до финализации заимствованных SQL-объектов.
- Три headless bind разбираются одинаково: IPv4 literal, полный decimal port
0..65535. Port=0 выбирает ОС; плохое значение не открывает порт по умолчанию.
Отказ listener не фатален для чата, частичный контекст очищается.
- Call/radio headless stop освобождает контекст/кодеки, снимает клиентов с UASYNC
и отменяет cleanup/mix таймеры. Их отказы при старте дают rollback.
- UBSan нашёл невыровненный PCM после 11-байтового headless заголовка и uint64_t
hop-list в radio_recv. Оба читаются через выровненное хранилище; тест рации также
использует memcpy вместо int16_t* к невыровненному payload. Wire не менялся.
- Из заголовка удалено объявление несуществующего utun_instance_run; poll принадлежит
caller. Уточнены владение, состояния, ошибки init/reload, подписки и диагностика.
Контракты и ограничения
Все lifecycle операции выполняются в потоке своего UASYNC, stop/destroy/reload —
вне callbacks останавливаемых модулей. UASYNC принадлежит caller и живёт до конца
очистки. Core start при отказе сохраняет подготовленные ресурсы для retry/destroy;
init при отказе позднего сервиса сохраняет ранее запущенные сервисы.
Create уже выполняет I/O и создаёт listeners/SQLite/router/proxy/monitoring.
Create_from_str требует временного файла /tmp и не является чистым buffer parser.
Logger общий для процесса; его изменения и config updater не входят в транзакцию reload.
Partial reload не атомарен: отказ endpoint сохраняет новый config и отсутствие
endpoint для следующей попытки. Подготовка clients при ошибке сохраняет прежние
самостоятельные handles. Full reload возвращает NULL, если новое ядро не удалось
создать/запустить после уничтожения старого; caller использует возвращённый указатель.
Порядок списков/padding модели может консервативно выбрать restart. IPv6 selector
temporary/permanent меняет адрес в модели при разрешении, поэтому повторный reload
может консервативно пересоздать такой endpoint.
Caller завершает borrowed statements/backups; close_v2 не разрешает использовать
уничтоженный instance. Живые ресурсы в diagnose_leaks сами по себе не доказывают утечку.
Отменённые heap-таймеры UASYNC освобождаются при следующем poll, а pool cache — при
уничтожении UASYNC; тест сверяет active/counters и общий баланс после обоих шагов.
Проверки
- Полная обычная сборка PASS: /tmp/utun-instance-audit-build-final.log.
- Первый полный check.sh: 196/196 PASS, 0 failed/0 skipped; proxy9/9, burst1/1,
load8/8, 134217728 байт, 189с; /tmp/utun-instance-audit-final-check.log.
Он предшествовал последней правке выравнивания radio hop-list.
- Профиль после исправлений: test_instance_contract, test_services, test_ncd_config,
test_tun_route, test_etcp_transport, test_stcp, test_node_conn_direct,
test_call_headless, test_radio_headless PASS; три изменённых адресных теста
повторены в /tmp/utun-instance-audit-final-test_*.log.
- Новый test_instance_contract проверяет nested callbacks/OOM, constructor ownership,
правильный TCP queue index, неизменённые ephemeral endpoints, malformed config,
отказ network queue/create endpoint с retry, UDP/TCP замену и удаление listener,
duplicate server names, full reload с сохранением stopped/core-only состояния,
SQLite BUSY/finalize, другой UASYNC, db_path restart, recursive mkdir и жизнь
logger path после освобождения config. Headless: три port=0, реальные TCP-клиенты,
stop/start с сохранением SQLite, три timer faults и malformed binds.
- ASAN/UBSan/LSan 11/11 PASS, detect_leaks=1, halt_on_error=1:
те же девять instance/transport/headless тестов плюс config_parser/config_updater.
Итог: /tmp/utun-instance-audit-asan-results-complete.log; отдельные логи
/tmp/utun-instance-audit-asan-complete-test_*.log.
Отдельная копия /tmp/utun-config-instance-asan-20261026, все lib/src/Opus
инструментированы согласованно. Встроенный Opus не перекрывает CFLAGS в этой
тестовой копии; рабочая normal-сборка с sanitizer-объектами не смешивалась.
Сборки: /tmp/utun-instance-audit-asan-build.log,
/tmp/utun-instance-audit-asan-core-final.log и ...-asan-tests-build.log.
Ранние наборы 9/11 и 10/11 выявили описанные alignment defects; окончательный
набор после исправления обеих причин полностью PASS.
- git diff --check PASS. Нативные Windows/FreeBSD сборки/runtime, GUI/Android и
аппаратные TUN/media в этом этапе не запускались; Linux TUN lifecycle проверен mock-тестом.
Окончательный штатный check.sh после последней правки: 196/196 PASS,
0 failed/0 skipped, proxy9/9, burst1/1, load8/8, 134217728 байт, 173с.
Лог /tmp/utun-instance-audit-complete-check.log. После этого изменены только комментарии
о повторном core_start (он сохраняет running) и итоговые документы/чек-лист.
utun_instance.h и utun_instance_doc.md отмечены [V]; ограничения общего документа
service_lifecycle.md и прежние записи остальных модулей сохранены.
+3
View File
@@ -5,6 +5,9 @@ libuasync_a_SOURCES = \
u_async.h \
ll_queue.h \
ll_queue.c \
packet_link.c \
packet_link.h \
packet_link_backend.h \
debug_config.c \
debug_config.h \
timeout_heap.c \
+67
View File
@@ -0,0 +1,67 @@
cmake_minimum_required(VERSION 3.16)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
project(vibechat_ble LANGUAGES C CXX)
enable_testing()
set(CMAKE_C_STANDARD 99)
set(BLE_LIB "${CMAKE_CURRENT_SOURCE_DIR}/..")
add_library(uasync STATIC ${BLE_LIB}/u_async.c ${BLE_LIB}/ll_queue.c ${BLE_LIB}/timeout_heap.c
${BLE_LIB}/memory_pool.c ${BLE_LIB}/mem.c ${BLE_LIB}/debug_config.c
${BLE_LIB}/platform_compat.c ${BLE_LIB}/socket_compat.c)
target_include_directories(uasync PUBLIC "${BLE_LIB}")
if(WIN32)
target_link_libraries(uasync PUBLIC ws2_32 iphlpapi bcrypt)
else()
target_link_libraries(uasync PUBLIC pthread m)
endif()
endif()
add_library(vibechat_ble_core OBJECT ble_transport.c ble_identity.c)
target_link_libraries(vibechat_ble_core PUBLIC uasync)
target_include_directories(vibechat_ble_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
set_target_properties(vibechat_ble_core PROPERTIES AUTOMOC OFF)
if(WIN32)
add_library(vibechat_ble STATIC ble_winrt.cpp)
target_compile_features(vibechat_ble PRIVATE cxx_std_20)
find_path(BLE_WINRT_INCLUDE winrt/Windows.Devices.Bluetooth.h REQUIRED)
target_include_directories(vibechat_ble PRIVATE "${BLE_WINRT_INCLUDE}")
target_link_libraries(vibechat_ble PRIVATE windowsapp)
add_executable(test_ble_winrt tests/test_ble_winrt.cpp)
target_compile_features(test_ble_winrt PRIVATE cxx_std_20)
target_include_directories(test_ble_winrt PRIVATE "${BLE_WINRT_INCLUDE}")
target_link_libraries(test_ble_winrt PRIVATE uasync windowsapp)
set_target_properties(test_ble_winrt PROPERTIES AUTOMOC OFF)
add_test(NAME test_ble_winrt COMMAND test_ble_winrt)
set_tests_properties(test_ble_winrt PROPERTIES TIMEOUT 30)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(BLE_SYSTEMD REQUIRED IMPORTED_TARGET libsystemd)
add_library(vibechat_ble STATIC ble_bluez.c)
target_link_libraries(vibechat_ble PRIVATE PkgConfig::BLE_SYSTEMD)
else()
message(FATAL_ERROR "BLE backend supports Linux and Windows")
endif()
target_link_libraries(vibechat_ble PUBLIC vibechat_ble_core)
set_target_properties(vibechat_ble PROPERTIES AUTOMOC OFF)
add_executable(test_ble_transport tests/test_ble_transport.c)
target_link_libraries(test_ble_transport PRIVATE vibechat_ble_core)
set_target_properties(test_ble_transport PROPERTIES AUTOMOC OFF)
add_test(NAME test_ble_transport COMMAND test_ble_transport)
set_tests_properties(test_ble_transport PROPERTIES TIMEOUT 60) # Включает настоящий 15s ACK watchdog.
add_executable(ble_probe tests/ble_probe.c)
target_link_libraries(ble_probe PRIVATE vibechat_ble)
set_target_properties(ble_probe PROPERTIES AUTOMOC OFF)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_program(BLE_DBUS_RUN_SESSION dbus-run-session REQUIRED)
add_executable(test_ble_bluez tests/test_ble_bluez.c)
target_link_libraries(test_ble_bluez PRIVATE vibechat_ble PkgConfig::BLE_SYSTEMD)
set_target_properties(test_ble_bluez PROPERTIES AUTOMOC OFF)
add_test(NAME test_ble_bluez COMMAND "${BLE_DBUS_RUN_SESSION}" -- $<TARGET_FILE:test_ble_bluez>)
set_tests_properties(test_ble_bluez PROPERTIES TIMEOUT 30)
endif()
add_executable(ble_peer_probe tests/ble_peer_probe.c)
target_link_libraries(ble_peer_probe PRIVATE vibechat_ble)
set_target_properties(ble_peer_probe PROPERTIES AUTOMOC OFF)
add_executable(test_ble_identity tests/test_ble_identity.c ble_identity.c)
target_link_libraries(test_ble_identity PRIVATE uasync)
target_include_directories(test_ble_identity PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
set_target_properties(test_ble_identity PROPERTIES AUTOMOC OFF)
add_test(NAME test_ble_identity COMMAND test_ble_identity)
+40
View File
@@ -0,0 +1,40 @@
/* Внутренний платформенный контракт. Все вызовы/события в uasync, send/close не вызывают события синхронно.
* ACCEPTED send копирует значение и даёт ровно один sent, либо closed отменяет все операции соединения.
* DATA sent означает локальное завершение; INVITE/ACK используют Request/Indication.
* close/destroy отменяют callbacks; transport создаёт идентификаторы, backend не хранит указатели core-link.
* Реализации не создают неограниченных очередей: максимум одна операция send на link. */
#ifndef VIBECHAT_BLE_BACKEND_H
#define VIBECHAT_BLE_BACKEND_H
#include "ble_transport.h"
#ifdef __cplusplus
extern "C" {
#endif
#define BLE_SERVICE_UUID "f61a9020-4abc-45ac-98e1-761df4c13811"
#define BLE_RX_UUID "f61a9021-4abc-45ac-98e1-761df4c13811"
#define BLE_TX_UUID "f61a9022-4abc-45ac-98e1-761df4c13811"
#define BLE_INVITE_UUID "f61a9023-4abc-45ac-98e1-761df4c13811"
#define BLE_ACK_UUID "f61a9024-4abc-45ac-98e1-761df4c13811"
#define BLE_VALUE_MAX 512
/* Wire-каналы backend; ACK не является прикладным каналом ble_channel. */
enum ble_wire_channel { BLE_WIRE_DATA, BLE_WIRE_INVITE, BLE_WIRE_ACK, BLE_WIRE_COUNT };
struct ble_backend;
struct ble_backend* ble_backend_create(struct ble_transport* owner, struct UASYNC* ua, const struct ble_config* config);
void ble_backend_destroy(struct ble_backend* backend);
int ble_backend_advertise(struct ble_backend* backend, const uint8_t* data, size_t len);
int ble_backend_connect(struct ble_backend* backend, ble_link_id id, const struct ble_endpoint* endpoint);
void ble_backend_close(struct ble_backend* backend, ble_link_id id);
int ble_backend_send(struct ble_backend* backend, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len);
/* События backend. incoming вызывается до любых других событий нового входящего link; 0 = лимит/отказ. */
ble_link_id ble_backend_incoming(struct ble_transport* owner, const struct ble_endpoint* endpoint);
void ble_backend_status(struct ble_transport* owner, int ready, const char* reason);
void ble_backend_advertising(struct ble_transport* owner, enum ble_state state, const char* reason);
void ble_backend_broadcast(struct ble_transport* owner, const struct ble_endpoint* endpoint,
const uint8_t* data, size_t len, int rssi);
void ble_backend_opened(struct ble_transport* owner, ble_link_id id, unsigned int mtu);
void ble_backend_received(struct ble_transport* owner, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len);
void ble_backend_sent(struct ble_transport* owner, ble_link_id id, int success);
void ble_backend_closed(struct ble_transport* owner, ble_link_id id, const char* reason);
#ifdef __cplusplus
}
#endif
#endif
+645
View File
@@ -0,0 +1,645 @@
/* BlueZ D-Bus через sd-bus (без Qt/GLib). Один uasync-поток, без блокирующих вызовов.
* Серверные DATA notifications и INVITE/ACK indications адресуются через AcquireNotify fd каждого клиента.
* DATA завершается после send(fd); INVITE/ACK ждут байт 1 от BlueZ (ATT completion). */
#include "ble_backend.h"
#include "../u_async.h"
#include "../mem.h"
#include "../debug_config.h"
#include <systemd/sd-bus.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <time.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#define CAT DEBUG_CATEGORY_DISCOVERY
#define ROOT "/org/vibechat/ble"
#define SERVICE ROOT "/service0"
#define RX SERVICE "/rx"
#define TX SERVICE "/tx"
#define INVITE SERVICE "/invite"
#define ACK SERVICE "/ack"
#define ADV ROOT "/advertisement"
#define BEACON ROOT "/beacon"
#define DEVICE_IF "org.bluez.Device1"
#define CHAR_IF "org.bluez.GattCharacteristic1"
struct bluez_link {
struct ble_backend* backend;
ble_link_id id;
char device[BLE_ENDPOINT_MAX], rx[BLE_ENDPOINT_MAX], tx[BLE_ENDPOINT_MAX], invite[BLE_ENDPOINT_MAX], ack[BLE_ENDPOINT_MAX];
sd_bus_slot *connect, *query, *subscribe, *write;
int fd[BLE_WIRE_COUNT], outgoing, opened, sending, buffered, subscribed, channel, connected, resolved, mtu_known;
void* io[BLE_WIRE_COUNT];
unsigned int mtu;
uint8_t value[BLE_VALUE_MAX];
size_t value_size;
};
struct characteristic { struct ble_backend* backend; int tx; };
struct advertisement { struct ble_backend* backend; int beacon; };
struct ble_backend {
struct ble_transport* owner;
struct UASYNC* ua;
sd_bus* bus;
sd_bus_slot *setup, *signals, *objects, *removed, *owner_watch, *service, *rx, *tx, *invite, *ack, *manager, *adv, *beacon, *beacon_op;
struct characteristic rx_context, tx_context, invite_context, ack_context;
struct advertisement adv_context, beacon_context;
struct bluez_link links[BLE_LINKS_MAX];
char adapter[BLE_ENDPOINT_MAX], adapter_name[64], daemon_owner[BLE_ENDPOINT_MAX];
uint16_t company;
uint8_t data[BLE_BROADCAST_MAX], next_data[BLE_BROADCAST_MAX];
size_t data_size, next_size;
int beacon_registered, beacon_desired, beacon_action, step, failed, processing;
void *io, *timer;
};
static void bus_update(struct ble_backend* b);
static void bus_timer(void* arg);
static void query_link(struct bluez_link* l);
static void beacon_update(struct ble_backend* b);
static void fail(struct ble_backend* b, const char* where, int error) {
if (b->failed) return;
b->failed = 1;
DEBUG_ERROR(CAT, "BLE BlueZ %s: %s (%d)", where, strerror(error < 0 ? -error : error), error);
ble_backend_status(b->owner, 0, where);
}
static int response_error(sd_bus_message* m, const char* where) {
const sd_bus_error* e = sd_bus_message_get_error(m);
if (!e) return 0;
DEBUG_ERROR(CAT, "BLE BlueZ %s: %s: %s", where, e->name, e->message ? e->message : ""); return 1;
}
static void bus_process(struct ble_backend* b) {
if (b->processing || b->failed) return;
b->processing = 1;
int r = 0;
for (int i = 0; i < 64; i++) { r = sd_bus_process(b->bus, NULL); if (r <= 0 || b->failed) break; }
b->processing = 0;
if (r < 0) { fail(b, "D-Bus disconnected", r); return; }
bus_update(b);
if (r > 0 && !b->failed) {
DEBUG_DEBUG(CAT, "BLE D-Bus drain budget exhausted; continuing buffered messages");
if (b->timer) uasync_cancel_timeout(b->ua, b->timer);
b->timer = uasync_set_timeout(b->ua, 0, b, bus_timer, "ble_dbus_drain");
if (!b->timer) fail(b, "D-Bus drain allocation", ENOMEM);
}
}
static void bus_io(int fd, void* arg) { (void)fd; bus_process(arg); }
static void bus_timer(void* arg) { struct ble_backend* b = arg; b->timer = NULL; bus_process(b); }
static void bus_update(struct ble_backend* b) {
if (b->processing || b->failed) return;
int events = sd_bus_get_events(b->bus);
uint64_t timeout = UINT64_MAX;
int r = sd_bus_get_timeout(b->bus, &timeout);
if (events < 0 || r < 0) { fail(b, "D-Bus polling failed", events < 0 ? events : r); return; }
uasync_set_socket_read(b->ua, b->io, !!(events & POLLIN));
uasync_set_socket_write(b->ua, b->io, !!(events & POLLOUT));
if (b->timer) { uasync_cancel_timeout(b->ua, b->timer); b->timer = NULL; }
if (timeout != UINT64_MAX) {
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t now = (uint64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
uint64_t delay = timeout > now ? (timeout - now + 99) / 100 : 0;
b->timer = uasync_set_timeout(b->ua, delay > INT_MAX ? INT_MAX : (int)delay, b, bus_timer, "ble_dbus");
if (!b->timer) fail(b, "D-Bus timer allocation", ENOMEM);
}
}
static struct bluez_link* find_link(struct ble_backend* b, ble_link_id id) {
for (int i = 0; i < BLE_LINKS_MAX; i++) if (b->links[i].id == id && id) return &b->links[i];
return NULL;
}
static struct bluez_link* find_device(struct ble_backend* b, const char* device) {
for (int i = 0; i < BLE_LINKS_MAX; i++) if (b->links[i].id && !strcmp(b->links[i].device, device)) return &b->links[i];
return NULL;
}
static struct bluez_link* alloc_link(struct ble_backend* b, ble_link_id id, const char* device, int outgoing) {
if (strlen(device) >= BLE_ENDPOINT_MAX || find_device(b, device)) return NULL;
for (int i = 0; i < BLE_LINKS_MAX; i++) if (!b->links[i].id) {
struct bluez_link* l = &b->links[i]; memset(l, 0, sizeof(*l));
l->backend = b; l->id = id;
for (int ch = 0; ch < BLE_WIRE_COUNT; ch++) l->fd[ch] = -1;
l->mtu = 23; l->outgoing = outgoing; strcpy(l->device, device); return l;
}
return NULL;
}
static struct bluez_link* incoming(struct ble_backend* b, const char* device) {
struct bluez_link* l = find_device(b, device);
if (l) return l->outgoing ? NULL : l;
struct ble_endpoint ep = {0};
if (strlen(device) >= sizeof(ep.id)) return NULL;
strcpy(ep.id, device);
ble_link_id id = ble_backend_incoming(b->owner, &ep);
if (!id) return NULL;
l = alloc_link(b, id, device, 0);
if (!l) ble_backend_closed(b->owner, id, "BlueZ link allocation failed");
return l;
}
static void link_error(struct bluez_link* l, const char* reason) {
DEBUG_WARN(CAT, "BLE BlueZ link=%llu failed: %s", (unsigned long long)l->id, reason);
ble_backend_closed(l->backend->owner, l->id, reason);
}
static void notify_read(int fd, void* arg) {
struct bluez_link* l = arg;
uint8_t data[8]; ssize_t n = recv(fd, data, sizeof(data), MSG_DONTWAIT);
if (n < 0 && (errno == EAGAIN || errno == EINTR)) return;
if (n < 0) {
DEBUG_ERROR(CAT, "BLE notification socket read failed link=%llu fd=%d errno=%d: %s",
(unsigned long long)l->id, fd, errno, strerror(errno));
link_error(l, "notification socket read failed"); return;
}
if (!n) { link_error(l, "notification subscription closed"); return; }
if (n != 1 || data[0] != 1 || l->channel == BLE_WIRE_DATA || fd != l->fd[l->channel] ||
!l->sending || l->buffered) {
DEBUG_WARN(CAT, "BLE indication completion invalid fd=%d expected_fd=%d bytes=%zd sending=%d buffered=%d",
fd, l->fd[l->channel], n, l->sending, l->buffered);
link_error(l, "invalid indication completion"); return;
}
l->sending = 0;
ble_backend_sent(l->backend->owner, l->id, 1);
}
static void notify_write(int fd, void* arg) {
struct bluez_link* l = arg;
if (!l->buffered) { uasync_set_socket_write(l->backend->ua, l->io[l->channel], 0); return; }
ssize_t n = send(fd, l->value, l->value_size, MSG_DONTWAIT | MSG_NOSIGNAL);
if (n < 0 && (errno == EAGAIN || errno == EINTR)) return;
if (n != (ssize_t)l->value_size) {
DEBUG_ERROR(CAT, "BLE notification socket write failed link=%llu channel=%d expected=%zu sent=%zd errno=%d",
(unsigned long long)l->id, l->channel, l->value_size, n, n < 0 ? errno : 0);
link_error(l, "notification socket write failed"); return;
}
l->buffered = 0; uasync_set_socket_write(l->backend->ua, l->io[l->channel], 0);
if (l->channel == BLE_WIRE_DATA) {
l->sending = 0;
ble_backend_sent(l->backend->owner, l->id, 1);
}
}
static int options(sd_bus_message* m, const char** device, uint16_t* mtu, uint16_t* offset) {
int r = sd_bus_message_enter_container(m, 'a', "{sv}");
if (r < 0) return r;
const char *key, *type;
while ((r = sd_bus_message_enter_container(m, 'e', "sv")) > 0) {
if ((r = sd_bus_message_read(m, "s", &key)) < 0 || (r = sd_bus_message_peek_type(m, NULL, &type)) < 0) return r;
if ((r = sd_bus_message_enter_container(m, 'v', type)) < 0) return r;
if (!strcmp(key, "device") && !strcmp(type, "o")) r = sd_bus_message_read(m, "o", device);
else if (!strcmp(key, "mtu") && !strcmp(type, "q")) r = sd_bus_message_read(m, "q", mtu);
else if (!strcmp(key, "offset") && !strcmp(type, "q")) r = sd_bus_message_read(m, "q", offset);
else r = sd_bus_message_skip(m, type);
if (r < 0) return r;
sd_bus_message_exit_container(m); sd_bus_message_exit_container(m);
}
if (r < 0) return r;
return sd_bus_message_exit_container(m);
}
static int reject(sd_bus_message* m, const char* reason) {
DEBUG_WARN(CAT, "BLE BlueZ request rejected: %s", reason);
int r = sd_bus_reply_method_errorf(m, "org.bluez.Error.Failed", "%s", reason);
if (r < 0) DEBUG_ERROR(CAT, "BLE BlueZ error reply failed: %s", strerror(-r));
return r;
}
static int write_value(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct characteristic* ctx = arg; struct ble_backend* b = ctx->backend;
const void* data; size_t len; const char* device = NULL; uint16_t mtu = 23, offset = 0;
if (!sd_bus_message_get_sender(m) || strcmp(sd_bus_message_get_sender(m), b->daemon_owner)) return reject(m, "sender is not BlueZ");
if (ctx->tx == 1 || sd_bus_message_read_array(m, 'y', &data, &len) < 0 || options(m, &device, &mtu, &offset) < 0 ||
!device || offset || !len || len > BLE_VALUE_MAX || mtu < 23) return reject(m, "invalid WriteValue");
struct bluez_link* l = incoming(b, device);
if (!l) return reject(m, "connection unavailable");
ble_link_id id = l->id;
enum ble_wire_channel channel = ctx->tx >= 2 ? (enum ble_wire_channel)(ctx->tx - 1) : BLE_WIRE_DATA;
ble_backend_received(b->owner, id, channel, data, len);
if (!find_link(b, id)) return reject(m, "session rejected");
int r = sd_bus_reply_method_return(m, "");
if (r < 0) DEBUG_ERROR(CAT, "BLE BlueZ WriteValue reply failed link=%llu: %s", (unsigned long long)id, strerror(-r));
return r;
}
static int acquire_notify(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct characteristic* ctx = arg; struct ble_backend* b = ctx->backend;
const char* device = NULL; uint16_t mtu = 23, offset = 0;
if (!sd_bus_message_get_sender(m) || strcmp(sd_bus_message_get_sender(m), b->daemon_owner)) return reject(m, "sender is not BlueZ");
if (!ctx->tx || options(m, &device, &mtu, &offset) < 0 || !device || offset || mtu < 23) return reject(m, "invalid AcquireNotify");
struct bluez_link* l = incoming(b, device);
int channel = ctx->tx >= 2 ? ctx->tx - 1 : BLE_WIRE_DATA;
if (!l || l->fd[channel] >= 0) return reject(m, "subscription unavailable");
int pair[2];
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) < 0) {
DEBUG_ERROR(CAT, "BLE notification socketpair failed: %s", strerror(errno)); return reject(m, "socketpair failed");
}
l->fd[channel] = pair[0]; l->mtu = mtu;
l->io[channel] = uasync_add_socket(b->ua, l->fd[channel], notify_read, notify_write, notify_read, "ble_notify", l);
if (!l->io[channel]) { close(pair[0]); close(pair[1]); l->fd[channel] = -1; return reject(m, "fd registration failed"); }
uasync_set_socket_write(b->ua, l->io[channel], 0);
int r = sd_bus_reply_method_return(m, "hq", pair[1], mtu); close(pair[1]);
if (r < 0) {
DEBUG_ERROR(CAT, "BLE BlueZ AcquireNotify reply failed link=%llu: %s", (unsigned long long)l->id, strerror(-r));
link_error(l, "AcquireNotify reply failed"); return r;
}
DEBUG_DEBUG(CAT, "BLE BlueZ subscription link=%llu channel=%d mode=%s mtu=%u",
(unsigned long long)l->id, channel, channel == BLE_WIRE_DATA ? "notification" : "indication", mtu);
if (l->fd[0] >= 0 && l->fd[1] >= 0 && l->fd[2] >= 0 && !l->opened) {
l->opened = 1; ble_backend_opened(b->owner, l->id, mtu);
}
bus_update(b); return r;
}
static int get_service(sd_bus* bus, const char* path, const char* interface, const char* property,
sd_bus_message* reply, void* arg, sd_bus_error* error) {
(void)bus; (void)path; (void)interface; (void)arg; (void)error;
return !strcmp(property, "UUID") ? sd_bus_message_append(reply, "s", BLE_SERVICE_UUID) : sd_bus_message_append(reply, "b", 1);
}
static int get_char(sd_bus* bus, const char* path, const char* interface, const char* property,
sd_bus_message* reply, void* arg, sd_bus_error* error) {
(void)bus; (void)path; (void)interface; (void)error; struct characteristic* ctx = arg;
if (!strcmp(property, "UUID")) return sd_bus_message_append(reply, "s",
ctx->tx == 3 ? BLE_ACK_UUID : ctx->tx == 2 ? BLE_INVITE_UUID : ctx->tx ? BLE_TX_UUID : BLE_RX_UUID);
if (!strcmp(property, "Service")) return sd_bus_message_append(reply, "o", SERVICE);
if (!strcmp(property, "Flags") && ctx->tx >= 2) return sd_bus_message_append(reply, "as", 2, "write", "indicate");
if (!strcmp(property, "Flags")) return sd_bus_message_append(reply, "as", 1, ctx->tx ? "notify" : "write-without-response");
return sd_bus_message_append(reply, "b", 0); /* NotifyAcquired объявляет поддержку адресного fd API. */
}
static int get_adv(sd_bus* bus, const char* path, const char* interface, const char* property,
sd_bus_message* reply, void* arg, sd_bus_error* error) {
(void)bus; (void)path; (void)interface; (void)error;
struct advertisement* a = arg; struct ble_backend* b = a->backend;
if (!strcmp(property, "Type")) return sd_bus_message_append(reply, "s", "peripheral");
if (!strcmp(property, "ServiceUUIDs")) return sd_bus_message_append(reply, "as", 1, BLE_SERVICE_UUID);
int r = sd_bus_message_open_container(reply, 'a', "{qv}");
if (r >= 0) r = sd_bus_message_open_container(reply, 'e', "qv");
if (r >= 0) r = sd_bus_message_append(reply, "q", b->company);
if (r >= 0) r = sd_bus_message_open_container(reply, 'v', "ay");
if (r >= 0) r = sd_bus_message_append_array(reply, 'y', b->data, b->data_size);
if (r >= 0) r = sd_bus_message_close_container(reply);
if (r >= 0) r = sd_bus_message_close_container(reply);
if (r >= 0) r = sd_bus_message_close_container(reply);
return r;
}
static int adv_release(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct advertisement* a = arg;
DEBUG_WARN(CAT, "BLE advertisement released by BlueZ beacon=%d", a->beacon);
if (a->beacon) {
a->backend->beacon_registered = 0;
ble_backend_advertising(a->backend->owner, BLE_CLOSED, "advertisement released");
}
else fail(a->backend, "connectable advertisement released", EIO);
return sd_bus_reply_method_return(m, "");
}
static const sd_bus_vtable service_table[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("UUID", "s", get_service, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Primary", "b", get_service, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_VTABLE_END
};
static const sd_bus_vtable rx_table[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("UUID", "s", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Service", "o", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Flags", "as", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_METHOD("WriteValue", "aya{sv}", "", write_value, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable tx_table[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("UUID", "s", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Service", "o", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Flags", "as", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("NotifyAcquired", "b", get_char, 0, 0),
SD_BUS_METHOD("AcquireNotify", "a{sv}", "hq", acquire_notify, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable invite_table[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("WriteValue", "aya{sv}", "", write_value, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_PROPERTY("UUID", "s", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Service", "o", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Flags", "as", get_char, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("NotifyAcquired", "b", get_char, 0, 0),
SD_BUS_METHOD("AcquireNotify", "a{sv}", "hq", acquire_notify, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable adv_table[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("Type", "s", get_adv, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ServiceUUIDs", "as", get_adv, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_METHOD("Release", "", "", adv_release, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable beacon_table[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("Type", "s", get_adv, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ManufacturerData", "a{qv}", get_adv, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_METHOD("Release", "", "", adv_release, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static int subscribed(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct bluez_link* l = arg; l->subscribe = sd_bus_slot_unref(l->subscribe);
if (response_error(m, "StartNotify")) { link_error(l, "subscription failed"); return 0; }
if (++l->subscribed < BLE_WIRE_COUNT) {
const char* path = l->subscribed == BLE_WIRE_INVITE ? l->invite : l->ack;
int r = sd_bus_call_method_async(l->backend->bus, &l->subscribe, "org.bluez", path, CHAR_IF,
"StartNotify", subscribed, l, "");
if (r < 0) link_error(l, "control subscription failed");
return 0;
}
DEBUG_INFO(CAT, "BLE BlueZ DATA configured link=%llu write=without_response receive=notification mtu=%u; PHY/interval managed by OS/peer",
(unsigned long long)l->id, l->mtu);
l->opened = 1; ble_backend_opened(l->backend->owner, l->id, l->mtu); return 0;
}
/* Snapshot содержит также старые объявления; только live-событие обновляет discovery/last_seen. */
static int properties(struct ble_backend* b, const char* path, const char* interface, sd_bus_message* m, int live) {
const char *key, *type, *uuid = NULL;
int r, resolved = -1, connected = -1, rssi = 127, have_data = 0;
uint16_t mtu = 0;
uint8_t data[BLE_BROADCAST_MAX]; size_t data_size = 0;
if ((r = sd_bus_message_enter_container(m, 'a', "{sv}")) < 0) return r;
while ((r = sd_bus_message_enter_container(m, 'e', "sv")) > 0) {
if ((r = sd_bus_message_read(m, "s", &key)) < 0 || (r = sd_bus_message_peek_type(m, NULL, &type)) < 0) return r;
if ((r = sd_bus_message_enter_container(m, 'v', type)) < 0) return r;
if (!strcmp(interface, DEVICE_IF) && !strcmp(key, "ManufacturerData") && !strcmp(type, "a{qv}")) {
sd_bus_message_enter_container(m, 'a', "{qv}");
while ((r = sd_bus_message_enter_container(m, 'e', "qv")) > 0) {
uint16_t company; const char* value_type;
sd_bus_message_read(m, "q", &company); sd_bus_message_peek_type(m, NULL, &value_type);
sd_bus_message_enter_container(m, 'v', value_type);
if (company == b->company && !strcmp(value_type, "ay")) {
const void* bytes; size_t size;
r = sd_bus_message_read_array(m, 'y', &bytes, &size);
if (r >= 0 && size <= sizeof(data)) { memcpy(data, bytes, size); data_size = size; have_data = 1; }
else DEBUG_WARN(CAT, "BLE rejected advertisement size=%zu", r < 0 ? 0 : size);
} else sd_bus_message_skip(m, value_type);
sd_bus_message_exit_container(m); sd_bus_message_exit_container(m);
}
sd_bus_message_exit_container(m);
} else if (!strcmp(interface, DEVICE_IF) && !strcmp(key, "RSSI") && !strcmp(type, "n")) {
int16_t value; r = sd_bus_message_read(m, "n", &value); rssi = value;
} else if (!strcmp(interface, DEVICE_IF) && !strcmp(type, "b") && (!strcmp(key, "Connected") || !strcmp(key, "ServicesResolved"))) {
int value; r = sd_bus_message_read(m, "b", &value);
if (!strcmp(key, "Connected")) connected = value; else resolved = value;
} else if (!strcmp(interface, CHAR_IF) && !strcmp(key, "UUID") && !strcmp(type, "s")) r = sd_bus_message_read(m, "s", &uuid);
else if (!strcmp(interface, CHAR_IF) && !strcmp(key, "MTU") && !strcmp(type, "q")) r = sd_bus_message_read(m, "q", &mtu);
else if (!strcmp(interface, CHAR_IF) && !strcmp(key, "Value") && !strcmp(type, "ay")) {
const void* bytes; size_t size; r = sd_bus_message_read_array(m, 'y', &bytes, &size);
if (r >= 0) for (int i = 0; i < BLE_LINKS_MAX; i++) {
struct bluez_link* l = &b->links[i];
if (live && size && l->id && l->opened && l->outgoing &&
(!strcmp(l->tx, path) || !strcmp(l->invite, path) || !strcmp(l->ack, path)))
ble_backend_received(b->owner, l->id, !strcmp(l->ack, path) ? BLE_WIRE_ACK :
!strcmp(l->invite, path) ? BLE_WIRE_INVITE : BLE_WIRE_DATA, bytes, size);
}
} else r = sd_bus_message_skip(m, type);
if (r < 0) return r;
sd_bus_message_exit_container(m); sd_bus_message_exit_container(m);
}
if (r < 0) return r;
sd_bus_message_exit_container(m);
if (!strcmp(interface, "org.bluez.Adapter1") && !b->adapter[0]) {
const char* name = strrchr(path, '/');
if (strlen(path) < sizeof(b->adapter) && (!b->adapter_name[0] || (name && !strcmp(name + 1, b->adapter_name)))) strcpy(b->adapter, path);
}
if (!strcmp(interface, DEVICE_IF)) {
struct bluez_link* l = find_device(b, path);
if (l && ((connected == 0 && (!l->outgoing || l->connected)) || (resolved == 0 && l->resolved)))
link_error(l, connected == 0 ? "device disconnected" : "GATT services invalidated");
else if (l && l->outgoing && (connected >= 0 || resolved >= 0)) {
if (connected >= 0) l->connected = connected;
if (resolved >= 0) l->resolved = resolved;
DEBUG_DEBUG(CAT, "BLE BlueZ readiness link=%llu connected=%d resolved=%d opened=%d",
(unsigned long long)l->id, l->connected, l->resolved, l->opened);
if (l->connected && l->resolved && !l->opened && !l->query) query_link(l);
}
if (live && have_data && strlen(path) < BLE_ENDPOINT_MAX) {
struct ble_endpoint ep = {0}; strcpy(ep.id, path); ble_backend_broadcast(b->owner, &ep, data, data_size, rssi);
}
}
if (uuid) for (int i = 0; i < BLE_LINKS_MAX; i++) {
struct bluez_link* l = &b->links[i]; size_t prefix = strlen(l->device);
if (!l->id || !l->outgoing || l->opened || strncmp(path, l->device, prefix) || path[prefix] != '/' || strlen(path) >= BLE_ENDPOINT_MAX) continue;
if (!strcmp(uuid, BLE_RX_UUID)) {
strcpy(l->rx, path);
if (mtu >= 23) { l->mtu = mtu; l->mtu_known = 1; }
}
if (!strcmp(uuid, BLE_TX_UUID)) strcpy(l->tx, path);
if (!strcmp(uuid, BLE_INVITE_UUID)) strcpy(l->invite, path);
if (!strcmp(uuid, BLE_ACK_UUID)) strcpy(l->ack, path);
}
return 0;
}
static int interfaces(struct ble_backend* b, const char* path, sd_bus_message* m, int live) {
int r = sd_bus_message_enter_container(m, 'a', "{sa{sv}}"); const char* iface;
if (r < 0) return r;
while ((r = sd_bus_message_enter_container(m, 'e', "sa{sv}")) > 0) {
if ((r = sd_bus_message_read(m, "s", &iface)) < 0 || (r = properties(b, path, iface, m, live)) < 0) return r;
sd_bus_message_exit_container(m);
}
if (r < 0) return r;
return sd_bus_message_exit_container(m);
}
static int managed_objects(struct ble_backend* b, sd_bus_message* m) {
int r = sd_bus_message_enter_container(m, 'a', "{oa{sa{sv}}}"); const char* path;
if (r < 0) return r;
while ((r = sd_bus_message_enter_container(m, 'e', "oa{sa{sv}}")) > 0) {
if ((r = sd_bus_message_read(m, "o", &path)) < 0 || (r = interfaces(b, path, m, 0)) < 0) return r;
sd_bus_message_exit_container(m);
}
if (r < 0) return r;
return sd_bus_message_exit_container(m);
}
static int queried(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct bluez_link* l = arg;
/* Оставляем slot ненулевым при разборе, чтобы ServicesResolved не породил рекурсивный запрос. */
int r = response_error(m, "GetManagedObjects") ? -EIO : managed_objects(l->backend, m);
l->query = sd_bus_slot_unref(l->query);
if (!l->id) return 0;
if (r < 0) { link_error(l, "service discovery failed"); return 0; }
DEBUG_DEBUG(CAT, "BLE BlueZ GATT snapshot link=%llu connected=%d resolved=%d RX=%d TX=%d INVITE=%d ACK=%d mtu=%u known=%d",
(unsigned long long)l->id, l->connected, l->resolved, !!l->rx[0], !!l->tx[0], !!l->invite[0], !!l->ack[0], l->mtu, l->mtu_known);
if (l->connected && l->resolved && l->rx[0] && l->tx[0] && l->invite[0] && l->ack[0] && !l->subscribe && !l->opened) {
if (!l->mtu_known) DEBUG_WARN(CAT, "BLE BlueZ MTU unavailable link=%llu: using ATT minimum 23", (unsigned long long)l->id);
r = sd_bus_call_method_async(l->backend->bus, &l->subscribe, "org.bluez", l->tx, CHAR_IF, "StartNotify", subscribed, l, "");
if (r < 0) link_error(l, "StartNotify request failed");
}
return 0;
}
static void query_link(struct bluez_link* l) {
if (l->query || l->subscribe || l->opened) return;
int r = sd_bus_call_method_async(l->backend->bus, &l->query, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager",
"GetManagedObjects", queried, l, "");
if (r < 0) link_error(l, "discovery request failed");
}
static int connected(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct bluez_link* l = arg; l->connect = sd_bus_slot_unref(l->connect);
if (response_error(m, "Connect")) { link_error(l, "connect failed"); return 0; }
query_link(l); return 0;
}
static int signal_changed(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct ble_backend* b = arg; const char* iface;
int r = sd_bus_message_read(m, "s", &iface);
if (r >= 0) r = properties(b, sd_bus_message_get_path(m), iface, m, 1);
if (r < 0) DEBUG_WARN(CAT, "BLE invalid BlueZ PropertiesChanged: %s", strerror(-r));
return 0;
}
static int object_added(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct ble_backend* b = arg; const char* path;
int r = sd_bus_message_read(m, "o", &path);
if (r >= 0) r = interfaces(b, path, m, 1);
if (r < 0) DEBUG_WARN(CAT, "BLE invalid BlueZ InterfacesAdded: %s", strerror(-r));
return 0;
}
static int object_removed(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct ble_backend* b = arg; const char* path;
if (sd_bus_message_read(m, "o", &path) < 0) { DEBUG_WARN(CAT, "BLE invalid InterfacesRemoved"); return 0; }
if (!strcmp(path, b->adapter)) { fail(b, "Bluetooth adapter removed", ENODEV); return 0; }
for (int i = 0; i < BLE_LINKS_MAX; i++) {
struct bluez_link* l = &b->links[i];
if (l->id && (!strcmp(path, l->device) || !strcmp(path, l->rx) || !strcmp(path, l->tx) ||
!strcmp(path, l->invite) || !strcmp(path, l->ack))) link_error(l, "BlueZ object removed");
}
return 0;
}
static int owner_changed(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; const char *name, *old_owner, *new_owner;
if (sd_bus_message_read(m, "sss", &name, &old_owner, &new_owner) >= 0 && !new_owner[0]) fail(arg, "BlueZ stopped", ECONNRESET);
return 0;
}
static int setup_reply(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct ble_backend* b = arg; b->setup = sd_bus_slot_unref(b->setup);
if (response_error(m, "startup")) { fail(b, "BlueZ startup failed", EIO); return 0; }
int r = 0;
switch (b->step++) {
case 0:
snprintf(b->daemon_owner, sizeof(b->daemon_owner), "%s", sd_bus_message_get_sender(m));
r = managed_objects(b, m);
if (r < 0 || !b->adapter[0]) { fail(b, "Bluetooth adapter unavailable", r < 0 ? r : ENODEV); return 0; }
r = sd_bus_call_method_async(b->bus, &b->setup, "org.bluez", b->adapter, "org.bluez.GattManager1",
"RegisterApplication", setup_reply, b, "oa{sv}", ROOT, 0); break;
case 1:
r = sd_bus_call_method_async(b->bus, &b->setup, "org.bluez", b->adapter, "org.bluez.LEAdvertisingManager1",
"RegisterAdvertisement", setup_reply, b, "oa{sv}", ADV, 0); break;
case 2:
r = sd_bus_call_method_async(b->bus, &b->setup, "org.bluez", b->adapter, "org.bluez.Adapter1",
"SetDiscoveryFilter", setup_reply, b, "a{sv}", 2, "Transport", "s", "le", "DuplicateData", "b", 1); break;
case 3:
r = sd_bus_call_method_async(b->bus, &b->setup, "org.bluez", b->adapter, "org.bluez.Adapter1", "StartDiscovery", setup_reply, b, ""); break;
case 4: ble_backend_status(b->owner, 1, "BlueZ scan/GATT ready"); break;
}
if (r < 0) fail(b, "BlueZ setup request failed", r);
return 0;
}
static int beacon_reply(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct ble_backend* b = arg; b->beacon_op = sd_bus_slot_unref(b->beacon_op);
if (response_error(m, "advertisement update")) { fail(b, "advertisement update failed", EIO); return 0; }
b->beacon_registered = b->beacon_action == 1; b->beacon_action = 0;
DEBUG_DEBUG(CAT, "BLE broadcast registered=%d bytes=%zu", b->beacon_registered, b->data_size);
ble_backend_advertising(b->owner, b->beacon_registered ? BLE_READY : BLE_CLOSED, "BlueZ advertisement registration changed");
beacon_update(b); return 0;
}
static void beacon_update(struct ble_backend* b) {
if (b->beacon_op || b->failed) return;
int r;
if (b->beacon_registered) {
if (b->beacon_desired && b->next_size == b->data_size && !memcmp(b->data, b->next_data, b->data_size)) return;
b->beacon_action = 2;
r = sd_bus_call_method_async(b->bus, &b->beacon_op, "org.bluez", b->adapter, "org.bluez.LEAdvertisingManager1",
"UnregisterAdvertisement", beacon_reply, b, "o", BEACON);
} else {
if (!b->beacon_desired) return;
b->beacon_action = 1;
b->data_size = b->next_size; memcpy(b->data, b->next_data, b->data_size);
r = sd_bus_call_method_async(b->bus, &b->beacon_op, "org.bluez", b->adapter, "org.bluez.LEAdvertisingManager1",
"RegisterAdvertisement", beacon_reply, b, "oa{sv}", BEACON, 0);
}
if (r < 0) fail(b, "advertisement request failed", r);
}
struct ble_backend* ble_backend_create(struct ble_transport* owner, struct UASYNC* ua, const struct ble_config* cfg) {
struct ble_backend* b = u_calloc(1, sizeof(*b));
if (!b) { DEBUG_ERROR(CAT, "BLE BlueZ allocation failed"); return NULL; }
b->owner = owner; b->ua = ua; b->company = cfg->company_id;
if (cfg->adapter) snprintf(b->adapter_name, sizeof(b->adapter_name), "%s", cfg->adapter);
b->rx_context.backend = b; b->tx_context.backend = b; b->tx_context.tx = 1;
b->invite_context.backend = b; b->invite_context.tx = 2;
b->ack_context.backend = b; b->ack_context.tx = 3;
b->adv_context.backend = b; b->beacon_context.backend = b; b->beacon_context.beacon = 1;
int r = sd_bus_open_system(&b->bus);
if (r >= 0) r = sd_bus_add_object_manager(b->bus, &b->manager, ROOT);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->service, SERVICE, "org.bluez.GattService1", service_table, b);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->rx, RX, CHAR_IF, rx_table, &b->rx_context);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->tx, TX, CHAR_IF, tx_table, &b->tx_context);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->invite, INVITE, CHAR_IF, invite_table, &b->invite_context);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->ack, ACK, CHAR_IF, invite_table, &b->ack_context);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->adv, ADV, "org.bluez.LEAdvertisement1", adv_table, &b->adv_context);
if (r >= 0) r = sd_bus_add_object_vtable(b->bus, &b->beacon, BEACON, "org.bluez.LEAdvertisement1", beacon_table, &b->beacon_context);
if (r >= 0) r = sd_bus_add_match_async(b->bus, &b->signals,
"type='signal',sender='org.bluez',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", signal_changed, NULL, b);
if (r >= 0) r = sd_bus_add_match_async(b->bus, &b->objects,
"type='signal',sender='org.bluez',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", object_added, NULL, b);
if (r >= 0) r = sd_bus_add_match_async(b->bus, &b->removed,
"type='signal',sender='org.bluez',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved'", object_removed, NULL, b);
if (r >= 0) r = sd_bus_add_match_async(b->bus, &b->owner_watch,
"type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.bluez'",
owner_changed, NULL, b);
if (r >= 0) r = sd_bus_call_method_async(b->bus, &b->setup, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager",
"GetManagedObjects", setup_reply, b, "");
if (r >= 0) { b->io = uasync_add_socket(ua, sd_bus_get_fd(b->bus), bus_io, bus_io, bus_io, "ble_dbus", b); if (!b->io) r = -ENOMEM; }
if (r < 0) { DEBUG_ERROR(CAT, "BLE BlueZ create: %s", strerror(-r)); ble_backend_destroy(b); return NULL; }
bus_update(b); return b;
}
void ble_backend_close(struct ble_backend* b, ble_link_id id) {
struct bluez_link* l = find_link(b, id); if (!l) return;
l->connect = sd_bus_slot_unref(l->connect); l->query = sd_bus_slot_unref(l->query);
l->subscribe = sd_bus_slot_unref(l->subscribe); l->write = sd_bus_slot_unref(l->write);
for (int channel = 0; channel < BLE_WIRE_COUNT; channel++) {
if (l->io[channel]) uasync_remove_socket(b->ua, l->io[channel]);
if (l->fd[channel] >= 0) close(l->fd[channel]);
}
/* Не разрываем общий Device1 ACL: он может принадлежать другим профилям/приложениям. */
if (l->outgoing && !b->failed) for (int channel = 0; channel < l->subscribed; channel++) {
const char* path = channel == BLE_WIRE_ACK ? l->ack : channel == BLE_WIRE_INVITE ? l->invite : l->tx;
int r = sd_bus_call_method_async(b->bus, NULL, "org.bluez", path, CHAR_IF, "StopNotify", NULL, NULL, "");
if (r < 0) DEBUG_WARN(CAT, "BLE StopNotify failed: %s", strerror(-r));
}
memset(l, 0, sizeof(*l)); bus_update(b);
}
void ble_backend_destroy(struct ble_backend* b) {
if (!b) return;
b->failed = 1;
for (int i = 0; i < BLE_LINKS_MAX; i++) if (b->links[i].id) ble_backend_close(b, b->links[i].id);
if (b->timer) uasync_cancel_timeout(b->ua, b->timer);
if (b->io) uasync_remove_socket(b->ua, b->io);
sd_bus_slot_unref(b->setup); sd_bus_slot_unref(b->signals); sd_bus_slot_unref(b->objects); sd_bus_slot_unref(b->removed);
sd_bus_slot_unref(b->owner_watch);
sd_bus_slot_unref(b->service); sd_bus_slot_unref(b->rx); sd_bus_slot_unref(b->tx); sd_bus_slot_unref(b->invite);
sd_bus_slot_unref(b->ack); sd_bus_slot_unref(b->manager);
sd_bus_slot_unref(b->adv); sd_bus_slot_unref(b->beacon); sd_bus_slot_unref(b->beacon_op);
/* Закрытие приватного bus освобождает регистрации/scan сессии в BlueZ по owner-disconnect. */
sd_bus_close_unref(b->bus); u_free(b);
}
int ble_backend_advertise(struct ble_backend* b, const uint8_t* data, size_t len) {
if (b->failed) return BLE_NOT_READY;
b->beacon_desired = data != NULL; b->next_size = len; if (len) memcpy(b->next_data, data, len);
if (!b->beacon_op && b->beacon_desired == b->beacon_registered &&
(!b->beacon_desired || (len == b->data_size && !memcmp(b->data, b->next_data, len)))) {
ble_backend_advertising(b->owner, b->beacon_registered ? BLE_READY : BLE_CLOSED, "advertisement unchanged"); return BLE_OK;
}
ble_backend_advertising(b->owner, BLE_STARTING, "updating advertisement");
beacon_update(b); bus_update(b); return b->failed ? BLE_ERROR : BLE_OK;
}
int ble_backend_connect(struct ble_backend* b, ble_link_id id, const struct ble_endpoint* ep) {
if (b->failed || strncmp(ep->id, b->adapter, strlen(b->adapter)) || !strstr(ep->id, "/dev_")) {
DEBUG_WARN(CAT, "BLE invalid BlueZ endpoint=%s", ep->id); return BLE_ERROR;
}
struct bluez_link* l = alloc_link(b, id, ep->id, 1);
if (!l) { DEBUG_WARN(CAT, "BLE BlueZ duplicate/limit endpoint=%s", ep->id); return BLE_ERROR; }
int r = sd_bus_call_method_async(b->bus, &l->connect, "org.bluez", l->device, DEVICE_IF, "Connect", connected, l, "");
if (r < 0) { DEBUG_ERROR(CAT, "BLE Connect request: %s", strerror(-r)); ble_backend_close(b, id); return BLE_ERROR; }
bus_update(b); return BLE_OK;
}
static int written(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct bluez_link* l = arg; l->write = sd_bus_slot_unref(l->write); l->sending = 0;
ble_backend_sent(l->backend->owner, l->id, !response_error(m, "WriteValue")); return 0;
}
int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len) {
struct bluez_link* l = find_link(b, id);
if ((unsigned)channel >= BLE_WIRE_COUNT || !l || !l->opened || l->sending || !len || len > BLE_VALUE_MAX) {
DEBUG_ERROR(CAT, "BLE invalid backend send link=%llu", (unsigned long long)id); return BLE_ERROR;
}
l->channel = channel;
if (!l->outgoing) {
memcpy(l->value, data, len); l->value_size = len; l->buffered = l->sending = 1;
uasync_set_socket_write(b->ua, l->io[channel], 1); return BLE_OK;
}
sd_bus_message* m = NULL;
const char* path = channel == BLE_WIRE_ACK ? l->ack : channel == BLE_WIRE_INVITE ? l->invite : l->rx;
int r = sd_bus_message_new_method_call(b->bus, &m, "org.bluez", path, CHAR_IF, "WriteValue");
if (r >= 0) r = sd_bus_message_append_array(m, 'y', data, len);
if (r >= 0) r = sd_bus_message_append(m, "a{sv}", 1, "type", "s", channel == BLE_WIRE_DATA ? "command" : "request");
if (r >= 0) r = sd_bus_call_async(b->bus, &l->write, m, written, l, 10000000);
sd_bus_message_unref(m);
if (r < 0) { DEBUG_ERROR(CAT, "BLE WriteValue request: %s", strerror(-r)); return BLE_ERROR; }
DEBUG_TRACE(CAT, "BLE WriteValue submitted link=%llu channel=%d type=%s bytes=%zu", (unsigned long long)id, channel,
channel == BLE_WIRE_DATA ? "command" : "request", len);
l->sending = 1; bus_update(b); return BLE_OK;
}
+42
View File
@@ -0,0 +1,42 @@
#include "ble_identity.h"
#include "../debug_config.h"
#include <string.h>
/* Строгая проверка scalar value: запрещены overlong, surrogate и > U+10FFFF. */
static int character(const unsigned char* p, size_t n) {
unsigned v; int k;
if (!n || !p[0]) return -1;
if (p[0] < 128) return 1;
if (p[0] >= 0xc2 && p[0] <= 0xdf) { k = 2; v = p[0] & 31; }
else if (p[0] >= 0xe0 && p[0] <= 0xef) { k = 3; v = p[0] & 15; }
else if (p[0] >= 0xf0 && p[0] <= 0xf4) { k = 4; v = p[0] & 7; }
else return -1;
if (n < (size_t)k) return -1;
for (int i = 1; i < k; i++) { if ((p[i] & 0xc0) != 0x80) return -1; v = (v << 6) | (p[i] & 63); }
if ((k == 3 && v < 0x800) || (k == 4 && v < 0x10000) || (v >= 0xd800 && v <= 0xdfff) || v > 0x10ffff) return -1;
return k;
}
int ble_identity_encode(uint8_t out[24], uint64_t id, const char* name) {
if (!out || !id || !name) goto invalid;
size_t length = strlen(name), offset = 0;
while (offset < length) {
int n = character((const unsigned char*)name + offset, length - offset);
if (n < 0) goto invalid;
if (offset + n > 13) break;
offset += n;
}
out[0] = 'V'; out[1] = 'C'; out[2] = BLE_IDENTITY_VERSION;
for (int i = 10; i >= 3; i--) { out[i] = id; id >>= 8; }
memcpy(out + 11, name, offset); return (int)(11 + offset);
invalid:
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "BLE identity encode: invalid ID/name"); return -1;
}
int ble_identity_decode(struct ble_identity* out, const uint8_t* data, size_t size) {
if (!out || !data || size < 11 || size > 24 || data[0] != 'V' || data[1] != 'C' || data[2] != BLE_IDENTITY_VERSION) return -1;
struct ble_identity result = {0};
for (int i = 3; i < 11; i++) result.node_id = (result.node_id << 8) | data[i];
if (!result.node_id) goto invalid;
for (size_t pos = 11; pos < size;) { int n = character(data + pos, size - pos); if (n < 0) goto invalid; pos += n; }
memcpy(result.name, data + 11, size - 11); *out = result; return 0;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "BLE identity decode: invalid ID/UTF-8"); return -1;
}
+18
View File
@@ -0,0 +1,18 @@
/* Объявление VibeChat: VC, версия 3, node_id BE64, имя UTF-8 до 13 байт.
* encode возвращает размер 11..24 либо -1; decode возвращает 0 либо -1.
* Имя сокращается по целому символу; decode отклоняет неверный UTF-8 и нулевой ID. */
#ifndef VIBECHAT_BLE_IDENTITY_H
#define VIBECHAT_BLE_IDENTITY_H
#include <stdint.h>
#include <stddef.h>
#define BLE_IDENTITY_VERSION 3 /* DATA с 16-bit flow ID и обязательной ACK-характеристикой. */
#ifdef __cplusplus
extern "C" {
#endif
struct ble_identity { uint64_t node_id; char name[14]; };
int ble_identity_encode(uint8_t output[24], uint64_t node_id, const char* name);
int ble_identity_decode(struct ble_identity* output, const uint8_t* data, size_t size);
#ifdef __cplusplus
}
#endif
#endif
+558
View File
@@ -0,0 +1,558 @@
#include "ble_backend.h"
#include "../u_async.h"
#include "../mem.h"
#include "../debug_config.h"
#include <string.h>
#define CAT DEBUG_CATEGORY_DISCOVERY
#define HELLO 1
#define ACCEPT 2
#define FRAME 3
#define HEADER 7
#define FLOW_HEADER 2
#define FLOW_MIN_VALUE (FLOW_HEADER + HEADER + 1)
#define FLOW_SLOTS (BLE_TX_WINDOW_BYTES / FLOW_MIN_VALUE)
#define DEADLINE_US 15000000ULL
#define STATS_US 5000000ULL
struct ble_stats {
uint64_t since, tx_bytes, rx_bytes, wait_us, max_wait_us;
unsigned tx_frames, rx_frames, fragments, dropped, missing, ack_tx, ack_rx, max_flight;
};
struct ble_channel_state {
struct ll_queue *tx, *rx;
struct ll_entry* current;
uint64_t rx_deadline;
uint16_t sequence, offset, rx_sequence, rx_size, rx_offset, dropped_sequence, rx_next;
int blocked, dropping, rx_seen;
uint8_t assembly[BLE_FRAME_MAX];
};
struct ble_connection {
struct ble_transport* owner;
ble_link_id id;
uint64_t peer, deadline, submitted;
struct ble_stats stats;
struct ble_endpoint endpoint;
struct ble_channel_state channels[BLE_CHANNEL_COUNT];
unsigned int capacity;
uint16_t pending_bytes;
/* ID монотонен по модулю 65536; кольцо хранит только размеры, payload остаётся у ETCP. */
uint16_t tx_next, tx_acked, rx_last, flight_head, flight_count, flight_bytes, flight_sizes[FLOW_SLOTS];
uint64_t flight_deadline, ack_due, ack_completed;
void* ack_timer;
int rx_flow_seen, ack_dirty, pending_ack;
enum ble_channel pending_channel, next_channel;
int outgoing, opened, ready, pending, control_pending;
uint8_t control[17];
};
struct ble_transport {
struct UASYNC* ua;
struct ble_config config;
char adapter[64];
struct ble_backend* backend;
struct ble_connection links[BLE_LINKS_MAX];
ble_link_id sequence;
void* timer;
int started, ready, reject_incoming;
};
static uint16_t read16(const uint8_t* p) { return (uint16_t)((p[0] << 8) | p[1]); }
static void write16(uint8_t* p, uint16_t v) { p[0] = v >> 8; p[1] = v; }
static uint64_t read64(const uint8_t* p) {
uint64_t v = 0;
for (int i = 0; i < 8; i++) v = (v << 8) | p[i];
return v;
}
static void write64(uint8_t* p, uint64_t v) { for (int i = 7; i >= 0; i--) { p[i] = v; v >>= 8; } }
/* Диагностический отпечаток зашифрованного кадра для сопоставления журналов двух узлов. */
static uint32_t frame_tag(const uint8_t* data, size_t len) {
uint32_t tag = 2166136261u;
for (size_t i = 0; i < len; i++) tag = (tag ^ data[i]) * 16777619u;
return tag;
}
static void release_entry(struct ll_entry* e) { if (e) { queue_dgram_free(e); queue_entry_free(e); } }
static void pump(struct ble_transport* t, struct ble_connection* c);
static struct ble_connection* lookup(struct ble_transport* t, ble_link_id id) {
if (t && id) for (int i = 0; i < BLE_LINKS_MAX; i++) if (t->links[i].id == id) return &t->links[i];
return NULL;
}
/* Сводка раз в пять секунд и при закрытии: payload кадров, очереди и локальное ожидание GATT. */
static void log_stats(struct ble_connection* c, uint64_t now) {
struct ble_stats* s = &c->stats;
uint64_t elapsed = now > s->since ? now - s->since : 1;
if (s->tx_frames || s->rx_frames || s->fragments || s->dropped || c->pending || c->flight_count) {
DEBUG_DEBUG(CAT, "BLE traffic link=%llu outgoing=%d elapsed_ms=%llu TX_Bps=%llu RX_Bps=%llu TX_frames=%u RX_frames=%u "
"fragments=%u drops=%u missing=%u wait_avg_us=%llu wait_max_us=%llu DATA_queue=%d INVITE_queue=%d pending_ms=%llu",
(unsigned long long)c->id, c->outgoing, (unsigned long long)(elapsed / 1000),
(unsigned long long)(s->tx_bytes * 1000000 / elapsed), (unsigned long long)(s->rx_bytes * 1000000 / elapsed),
s->tx_frames, s->rx_frames, s->fragments, s->dropped, s->missing,
(unsigned long long)(s->fragments ? s->wait_us / s->fragments : 0), (unsigned long long)s->max_wait_us,
queue_entry_count(c->channels[BLE_CHANNEL_DATA].tx) + !!c->channels[BLE_CHANNEL_DATA].current,
queue_entry_count(c->channels[BLE_CHANNEL_INVITE].tx) + !!c->channels[BLE_CHANNEL_INVITE].current,
(unsigned long long)(c->pending ? (now - c->submitted) / 1000 : 0));
DEBUG_DEBUG(CAT, "BLE flow link=%llu flight=%u/%u bytes=%u/%u peak_bytes=%u tx_next=%u acked=%u "
"rx_last=%u ACK_tx=%u ACK_rx=%u ack_dirty=%d ack_pending=%d",
(unsigned long long)c->id, c->flight_count, (unsigned)FLOW_SLOTS, c->flight_bytes,
BLE_TX_WINDOW_BYTES, s->max_flight, c->tx_next, c->tx_acked, c->rx_last,
s->ack_tx, s->ack_rx, c->ack_dirty, c->pending_ack && c->pending);
}
memset(s, 0, sizeof(*s)); s->since = now;
}
int ble_get_outgoing(struct ble_transport* t, ble_link_id id, int* outgoing) {
struct ble_connection* c = lookup(t, id);
if (!c || !outgoing) { DEBUG_ERROR(CAT, "BLE direction: invalid link=%llu/output", (unsigned long long)id); return -1; }
*outgoing = c->outgoing;
return 0;
}
static void close_link(struct ble_transport* t, struct ble_connection* c, const char* reason) {
ble_link_id id = c->id;
uint64_t peer = c->peer;
log_stats(c, get_time_us());
DEBUG_INFO(CAT, "BLE closed link=%llu peer=%016llx reason=%s", (unsigned long long)id, (unsigned long long)peer, reason);
if (t->backend) ble_backend_close(t->backend, id);
if (c->ack_timer) uasync_cancel_timeout(t->ua, c->ack_timer);
for (int i = 0; i < BLE_CHANNEL_COUNT; i++) {
struct ble_channel_state* ch = &c->channels[i];
release_entry(ch->current);
queue_clear(ch->tx); queue_free(ch->tx);
queue_clear(ch->rx); queue_free(ch->rx);
}
memset(c, 0, sizeof(*c));
if (t->config.callbacks.connection) t->config.callbacks.connection(t->config.arg, id, peer, BLE_CLOSED, reason);
}
static struct ble_connection* new_link(struct ble_transport* t, const struct ble_endpoint* ep, int outgoing, uint64_t peer) {
if (!t || !t->started || !ep || !memchr(ep->id, 0, sizeof(ep->id)) || !ep->id[0]) {
DEBUG_ERROR(CAT, "BLE invalid connection request"); return NULL;
}
for (int i = 0; i < BLE_LINKS_MAX; i++) if (t->links[i].id && !strcmp(t->links[i].endpoint.id, ep->id)) {
DEBUG_WARN(CAT, "BLE duplicate connection endpoint=%s", ep->id); return NULL;
}
for (int i = 0; i < BLE_LINKS_MAX; i++) if (!t->links[i].id) {
struct ble_connection* c = &t->links[i];
int allocated = 1;
for (int ch = 0; ch < BLE_CHANNEL_COUNT; ch++) {
c->channels[ch].tx = queue_new(t->ua, 0, 0, 0, "ble_tx");
c->channels[ch].rx = queue_new(t->ua, 0, 0, 0, "ble_rx");
if (!c->channels[ch].tx || !c->channels[ch].rx) allocated = 0;
}
if (!allocated) {
DEBUG_ERROR(CAT, "BLE queue allocation failed");
for (int ch = 0; ch < BLE_CHANNEL_COUNT; ch++) { queue_free(c->channels[ch].tx); queue_free(c->channels[ch].rx); }
memset(c, 0, sizeof(*c)); return NULL;
}
c->owner = t; c->id = ++t->sequence; c->peer = peer; c->endpoint = *ep; c->outgoing = outgoing; c->tx_next = 1;
c->capacity = 20; c->stats.since = get_time_us(); c->deadline = c->stats.since + DEADLINE_US;
DEBUG_INFO(CAT, "BLE connecting link=%llu endpoint=%s outgoing=%d", (unsigned long long)c->id, ep->id, outgoing);
if (t->config.callbacks.connection) t->config.callbacks.connection(t->config.arg, c->id, peer, BLE_STARTING, "connecting");
return c;
}
DEBUG_WARN(CAT, "BLE connection limit=%d", BLE_LINKS_MAX); return NULL;
}
/* writable отражает и свою очередь, и кредит удалённого приёмника. */
static void wake_writer(struct ble_transport* t, struct ble_connection* c, enum ble_channel channel) {
struct ble_channel_state* ch = &c->channels[channel];
if (!ch->blocked || !ble_channel_tx_ready(t, c->id, channel)) return;
ch->blocked = 0;
DEBUG_TRACE(CAT, "BLE TX resumed link=%llu channel=%d flight_bytes=%u", (unsigned long long)c->id, channel, c->flight_bytes);
void (*writable)(void*, ble_link_id) = channel == BLE_CHANNEL_DATA ? t->config.callbacks.writable : t->config.callbacks.invite_writable;
if (writable) writable(t->config.arg, c->id);
}
static void ack_timer(void* arg) {
struct ble_connection* c = arg; c->ack_timer = NULL;
pump(c->owner, c);
}
/* Один отложенный ACK на направление; новые RX заменяют ID, не отодвигая срок. */
static int schedule_ack(struct ble_transport* t, struct ble_connection* c) {
uint64_t now = get_time_us();
if (!c->ack_dirty) c->ack_due = c->ack_completed ? c->ack_completed + BLE_ACK_INTERVAL_US : now + BLE_ACK_INTERVAL_US;
c->ack_dirty = 1;
if (c->ack_timer || (c->pending && now >= c->ack_due)) return 1;
/* uasync использует усечённые TB: дополнительный TB исключает срабатывание раньше 15ms. */
uint64_t delay = c->ack_due > now ? (c->ack_due - now + 199) / 100 : 0;
c->ack_timer = uasync_set_timeout(t->ua, (int)delay, c, ack_timer, "ble_ack");
if (c->ack_timer) return 1;
DEBUG_ERROR(CAT, "BLE ACK timer allocation failed link=%llu rx_last=%u", (unsigned long long)c->id, c->rx_last);
close_link(t, c, "ACK timer allocation failed"); return 0;
}
static void pump(struct ble_transport* t, struct ble_connection* c) {
if (!c->opened || c->pending) return;
uint8_t value[BLE_VALUE_MAX];
size_t len;
c->pending_bytes = 0;
c->pending_ack = c->ack_dirty && get_time_us() >= c->ack_due;
c->pending_channel = BLE_CHANNEL_DATA;
if (c->pending_ack) { write16(value, c->rx_last); len = 2; }
else if (c->control_pending) {
if (BLE_TX_WINDOW_BYTES - c->flight_bytes < 17 + FLOW_HEADER || c->flight_count == FLOW_SLOTS) return;
memcpy(value + FLOW_HEADER, c->control, 17); len = 17 + FLOW_HEADER;
}
else {
if (!c->ready) return;
struct ble_channel_state* ch = NULL;
for (int i = 0; i < BLE_CHANNEL_COUNT; i++) {
enum ble_channel channel = (c->next_channel + i) % BLE_CHANNEL_COUNT;
struct ble_channel_state* candidate = &c->channels[channel];
if (channel == BLE_CHANNEL_DATA &&
(BLE_TX_WINDOW_BYTES - c->flight_bytes < FLOW_MIN_VALUE || c->flight_count == FLOW_SLOTS)) {
candidate->blocked = 1; continue;
}
if (!candidate->current) {
candidate->current = queue_data_get(candidate->tx); candidate->offset = 0;
if (candidate->current) ++candidate->sequence;
}
if (candidate->current) { ch = candidate; c->pending_channel = channel; break; }
}
if (!ch) return;
c->next_channel = (c->pending_channel + 1) % BLE_CHANNEL_COUNT;
unsigned prefix = c->pending_channel == BLE_CHANNEL_DATA ? FLOW_HEADER : 0;
unsigned capacity = c->capacity;
if (prefix && capacity > BLE_TX_WINDOW_BYTES - c->flight_bytes) capacity = BLE_TX_WINDOW_BYTES - c->flight_bytes;
size_t n = ch->current->len - ch->offset;
if (n > capacity - HEADER - prefix) n = capacity - HEADER - prefix;
uint8_t* body = value + prefix;
body[0] = FRAME; write16(body + 1, ch->sequence); write16(body + 3, ch->current->len); write16(body + 5, ch->offset);
memcpy(body + HEADER, ch->current->dgram + ch->offset, n); len = prefix + HEADER + n; c->pending_bytes = n;
}
enum ble_wire_channel wire = c->pending_ack ? BLE_WIRE_ACK :
c->pending_channel == BLE_CHANNEL_DATA ? BLE_WIRE_DATA : BLE_WIRE_INVITE;
if (wire == BLE_WIRE_DATA) write16(value, c->tx_next);
int result = ble_backend_send(t->backend, c->id, wire, value, len);
if (result == BLE_OK) {
c->pending = 1; c->submitted = get_time_us(); c->deadline = c->submitted + DEADLINE_US;
if (wire == BLE_WIRE_ACK) {
if (c->ack_timer) { uasync_cancel_timeout(t->ua, c->ack_timer); c->ack_timer = NULL; }
c->ack_dirty = 0; c->stats.ack_tx++;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK TX link=%llu id=%u", (unsigned long long)c->id, c->rx_last);
} else if (wire == BLE_WIRE_DATA) {
if (!c->flight_count) c->flight_deadline = c->submitted + DEADLINE_US;
c->flight_sizes[(c->flight_head + c->flight_count++) % FLOW_SLOTS] = len;
c->flight_bytes += len;
if (c->flight_bytes > c->stats.max_flight) c->stats.max_flight = c->flight_bytes;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE DATA TX link=%llu seq=%u type=%u bytes=%zu flight=%u/%u",
(unsigned long long)c->id, c->tx_next, value[FLOW_HEADER], len, c->flight_count, c->flight_bytes);
c->tx_next++;
if (!ble_tx_ready(t, c->id)) c->channels[BLE_CHANNEL_DATA].blocked = 1;
}
}
else { DEBUG_ERROR(CAT, "BLE backend send failed link=%llu result=%d", (unsigned long long)c->id, result);
close_link(t, c, "backend send failed"); }
}
static void tick(void* arg) {
struct ble_transport* t = arg; t->timer = NULL;
uint64_t now = get_time_us();
for (int i = 0; i < BLE_LINKS_MAX; i++) {
struct ble_connection* c = &t->links[i];
if (!c->id) continue;
if (now - c->stats.since >= STATS_US) log_stats(c, now);
if (c->flight_count && now >= c->flight_deadline) {
DEBUG_WARN(CAT, "BLE ACK timeout link=%llu acked=%u next=%u flight_packets=%u flight_bytes=%u",
(unsigned long long)c->id, c->tx_acked, c->tx_next, c->flight_count, c->flight_bytes);
close_link(t, c, "ACK progress timeout"); continue;
}
if ((!c->ready || c->pending) && now >= c->deadline) {
DEBUG_WARN(CAT, "BLE handshake/send timeout link=%llu pending=%d", (unsigned long long)c->id, c->pending);
close_link(t, c, "operation timeout"); continue;
}
for (int channel = 0; channel < BLE_CHANNEL_COUNT; channel++) {
struct ble_channel_state* ch = &c->channels[channel];
if (ch->rx_size && now >= ch->rx_deadline) {
DEBUG_WARN(CAT, "BLE frame timeout link=%llu received=%u expected=%u",
(unsigned long long)c->id, ch->rx_offset, ch->rx_size);
ch->dropping = 1; ch->dropped_sequence = ch->rx_sequence;
c->stats.dropped++;
ch->rx_size = ch->rx_offset = 0;
}
}
}
t->timer = uasync_set_timeout(t->ua, 1000, t, tick, "ble_tick");
if (!t->timer) { DEBUG_ERROR(CAT, "BLE timer allocation failed"); ble_backend_status(t, 0, "timer allocation failed"); }
}
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config) {
if (!ua || !config || !config->node_id || config->tx_limit > BLE_QUEUE_MAX ||
(config->adapter && strlen(config->adapter) >= 64)) {
DEBUG_ERROR(CAT, "BLE create invalid configuration"); return NULL;
}
struct ble_transport* t = u_calloc(1, sizeof(*t));
if (!t) { DEBUG_ERROR(CAT, "BLE create allocation failed"); return NULL; }
t->ua = ua; t->config = *config;
if (!t->config.tx_limit) t->config.tx_limit = BLE_QUEUE_MAX;
if (config->adapter) { strcpy(t->adapter, config->adapter); t->config.adapter = t->adapter; }
return t;
}
int ble_start(struct ble_transport* t) {
if (!t || t->started) { DEBUG_ERROR(CAT, "BLE invalid/repeated start"); return BLE_ERROR; }
t->started = 1;
t->backend = ble_backend_create(t, t->ua, &t->config);
if (!t->backend) { t->started = 0; DEBUG_ERROR(CAT, "BLE backend creation failed"); return BLE_ERROR; }
t->timer = uasync_set_timeout(t->ua, 1000, t, tick, "ble_tick");
if (!t->timer) { DEBUG_ERROR(CAT, "BLE timer allocation failed"); ble_stop(t); return BLE_ERROR; }
if (t->config.callbacks.status) t->config.callbacks.status(t->config.arg, BLE_STARTING, "registering BLE service");
return BLE_OK;
}
void ble_stop(struct ble_transport* t) {
if (!t || !t->started) return;
t->started = t->ready = 0;
if (t->timer) { uasync_cancel_timeout(t->ua, t->timer); t->timer = NULL; }
for (int i = 0; i < BLE_LINKS_MAX; i++) if (t->links[i].id) close_link(t, &t->links[i], "transport stopped");
ble_backend_destroy(t->backend); t->backend = NULL;
if (t->config.callbacks.status) t->config.callbacks.status(t->config.arg, BLE_CLOSED, "stopped");
}
void ble_set_incoming(struct ble_transport* t, int allowed) {
if (!t) { DEBUG_ERROR(CAT, "BLE incoming policy: null transport"); return; }
t->reject_incoming = !allowed;
DEBUG_INFO(CAT, "BLE incoming allowed=%d", !!allowed);
if (!allowed) for (int i = 0; i < BLE_LINKS_MAX; i++)
if (t->links[i].id && !t->links[i].outgoing) close_link(t, &t->links[i], "incoming disabled");
}
void ble_destroy(struct ble_transport* t) { if (t) { ble_stop(t); u_free(t); } }
int ble_advertise(struct ble_transport* t, const void* data, size_t len) {
if (!t || !t->ready) { DEBUG_WARN(CAT, "BLE advertise before ready"); return BLE_NOT_READY; }
if (len > BLE_BROADCAST_MAX || (len && !data)) { DEBUG_ERROR(CAT, "BLE invalid advertisement length=%zu", len); return BLE_ERROR; }
return ble_backend_advertise(t->backend, data, len);
}
ble_link_id ble_connect(struct ble_transport* t, const struct ble_endpoint* ep, uint64_t peer) {
if (!t || !t->ready) { DEBUG_WARN(CAT, "BLE connect before ready"); return 0; }
struct ble_connection* c = new_link(t, ep, 1, peer);
if (!c) return 0;
ble_link_id id = c->id;
if (ble_backend_connect(t->backend, id, ep) != BLE_OK) { close_link(t, c, "connect rejected"); return 0; }
return id;
}
void ble_disconnect(struct ble_transport* t, ble_link_id id) {
struct ble_connection* c = lookup(t, id);
if (c) close_link(t, c, "API disconnect");
else DEBUG_WARN(CAT, "BLE disconnect unknown link=%llu", (unsigned long long)id);
}
int ble_channel_tx_ready(struct ble_transport* t, ble_link_id id, enum ble_channel channel) {
if ((unsigned)channel >= BLE_CHANNEL_COUNT) { DEBUG_ERROR(CAT, "BLE invalid channel=%d", channel); return 0; }
struct ble_connection* c = lookup(t, id);
struct ble_channel_state* ch = c ? &c->channels[channel] : NULL;
return c && c->ready && (channel != BLE_CHANNEL_DATA ||
(BLE_TX_WINDOW_BYTES - c->flight_bytes >= FLOW_MIN_VALUE && c->flight_count < FLOW_SLOTS)) &&
(unsigned)queue_entry_count(ch->tx) + !!ch->current < (channel == BLE_CHANNEL_DATA ? t->config.tx_limit : 1);
}
int ble_channel_tx_put(struct ble_transport* t, ble_link_id id, enum ble_channel channel, struct ll_entry* e) {
if ((unsigned)channel >= BLE_CHANNEL_COUNT) { DEBUG_ERROR(CAT, "BLE invalid channel=%d", channel); return BLE_ERROR; }
struct ble_connection* c = lookup(t, id);
struct ble_channel_state* ch = c ? &c->channels[channel] : NULL;
if (!c || !c->ready) { DEBUG_WARN(CAT, "BLE TX link not ready=%llu", (unsigned long long)id); return BLE_NOT_READY; }
if (!e || e->owner || e->next || e->prev || !e->dgram || !e->len || e->len > BLE_FRAME_MAX || e->len > e->memlen) {
DEBUG_ERROR(CAT, "BLE invalid TX frame link=%llu", (unsigned long long)id); return BLE_ERROR;
}
if (!ble_channel_tx_ready(t, id, channel)) {
if (!ch->blocked) DEBUG_TRACE(CAT, "BLE TX paused link=%llu packets=%u", (unsigned long long)id,
(channel == BLE_CHANNEL_DATA ? t->config.tx_limit : 1));
ch->blocked = 1; return BLE_WOULD_BLOCK;
}
/* Без лимита/индекса/consumer put не имеет отказа после проверок и не вызывает чужой callback. */
if (queue_data_put(ch->tx, e) != 0) { DEBUG_ERROR(CAT, "BLE TX queue invariant failed"); return BLE_ERROR; }
if (!ble_channel_tx_ready(t, id, channel)) ch->blocked = 1;
pump(t, c); return BLE_OK;
}
struct ll_entry* ble_channel_rx_get(struct ble_transport* t, ble_link_id id, enum ble_channel channel) {
if ((unsigned)channel >= BLE_CHANNEL_COUNT) { DEBUG_ERROR(CAT, "BLE invalid channel=%d", channel); return 0; }
struct ble_connection* c = lookup(t, id);
struct ble_channel_state* ch = c ? &c->channels[channel] : NULL;
return c ? queue_data_get(ch->rx) : NULL;
}
int ble_tx_ready(struct ble_transport* t, ble_link_id id) { return ble_channel_tx_ready(t, id, BLE_CHANNEL_DATA); }
int ble_tx_put(struct ble_transport* t, ble_link_id id, struct ll_entry* e) { return ble_channel_tx_put(t, id, BLE_CHANNEL_DATA, e); }
struct ll_entry* ble_rx_get(struct ble_transport* t, ble_link_id id) { return ble_channel_rx_get(t, id, BLE_CHANNEL_DATA); }
ble_link_id ble_backend_incoming(struct ble_transport* t, const struct ble_endpoint* ep) {
if (t->reject_incoming) { DEBUG_INFO(CAT, "BLE incoming rejected by policy"); return 0; }
struct ble_connection* c = new_link(t, ep, 0, 0); return c ? c->id : 0;
}
void ble_backend_status(struct ble_transport* t, int ready, const char* reason) {
t->ready = ready;
if (!ready) {
DEBUG_ERROR(CAT, "BLE radio failed: %s", reason);
for (int i = 0; i < BLE_LINKS_MAX; i++) if (t->links[i].id) close_link(t, &t->links[i], reason);
} else DEBUG_INFO(CAT, "BLE radio ready node=%016llx", (unsigned long long)t->config.node_id);
if (t->config.callbacks.status) t->config.callbacks.status(t->config.arg, ready ? BLE_READY : BLE_CLOSED, reason);
}
void ble_backend_broadcast(struct ble_transport* t, const struct ble_endpoint* ep, const uint8_t* data, size_t len, int rssi) {
if (len > BLE_BROADCAST_MAX) { DEBUG_WARN(CAT, "BLE oversized broadcast bytes=%zu", len); return; }
if (t->config.callbacks.broadcast) t->config.callbacks.broadcast(t->config.arg, ep, data, len, rssi);
}
void ble_backend_advertising(struct ble_transport* t, enum ble_state state, const char* reason) {
DEBUG_INFO(CAT, "BLE advertising state=%d reason=%s", state, reason);
if (t->config.callbacks.advertising) t->config.callbacks.advertising(t->config.arg, state, reason);
}
void ble_backend_opened(struct ble_transport* t, ble_link_id id, unsigned int mtu) {
struct ble_connection* c = lookup(t, id);
if (!c) return;
if (mtu < 23) { DEBUG_ERROR(CAT, "BLE invalid MTU=%u", mtu); close_link(t, c, "invalid MTU"); return; }
c->capacity = mtu - 3; if (c->capacity > BLE_VALUE_MAX) c->capacity = BLE_VALUE_MAX;
c->opened = 1;
DEBUG_INFO(CAT, "BLE GATT open link=%llu mtu=%u fragment_payload=%u outgoing=%d DATA=without_response/notify INVITE=request/indicate tx_limit=%u",
(unsigned long long)id, mtu, c->capacity - HEADER - FLOW_HEADER, c->outgoing, t->config.tx_limit);
DEBUG_INFO(CAT, "BLE flow configured link=%llu rate_bits=%u window_ms=%u window_bytes=%u ACK_interval_us=%u id_bits=16",
(unsigned long long)id, BLE_TX_RATE_BITS, BLE_TX_WINDOW_MS, BLE_TX_WINDOW_BYTES, BLE_ACK_INTERVAL_US);
if (c->outgoing) {
c->control[0] = HELLO; write64(c->control + 1, t->config.node_id); write64(c->control + 9, c->peer);
c->control_pending = 1;
}
pump(t, c);
}
void ble_backend_received(struct ble_transport* t, ble_link_id id, enum ble_wire_channel wire, const uint8_t* data, size_t len) {
struct ble_connection* c = lookup(t, id);
if (!c) return;
if (!c->opened) {
DEBUG_WARN(CAT, "BLE value before mandatory subscriptions link=%llu channel=%d bytes=%zu", (unsigned long long)id, wire, len);
close_link(t, c, "value before subscriptions"); return;
}
if (!data || !len || len > c->capacity || (unsigned)wire >= BLE_WIRE_COUNT) {
DEBUG_WARN(CAT, "BLE invalid wire value link=%llu channel=%d bytes=%zu", (unsigned long long)id, wire, len);
close_link(t, c, "invalid wire value"); return;
}
if (wire == BLE_WIRE_ACK) {
if (len != 2) {
DEBUG_WARN(CAT, "BLE invalid ACK length link=%llu bytes=%zu", (unsigned long long)id, len);
close_link(t, c, "invalid ACK length"); return;
}
uint16_t ack = read16(data), advance = (uint16_t)(ack - c->tx_acked);
if (!advance || advance >= 0x8000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u acked=%u flight=%u/%u ignored=stale",
(unsigned long long)id, ack, c->tx_acked, c->flight_count, c->flight_bytes); return;
}
if (advance > c->flight_count) {
DEBUG_WARN(CAT, "BLE ACK outside flight link=%llu id=%u acked=%u next=%u count=%u",
(unsigned long long)id, ack, c->tx_acked, c->tx_next, c->flight_count);
close_link(t, c, "ACK outside flight"); return;
}
unsigned bytes = 0;
for (unsigned i = 0; i < advance; i++) {
bytes += c->flight_sizes[c->flight_head]; c->flight_head = (c->flight_head + 1) % FLOW_SLOTS;
}
c->flight_count -= advance; c->flight_bytes -= bytes; c->tx_acked = ack; c->stats.ack_rx++;
c->flight_deadline = c->flight_count ? get_time_us() + DEADLINE_US : 0;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u released=%u/%u flight=%u/%u", (unsigned long long)id,
ack, advance, bytes, c->flight_count, c->flight_bytes);
wake_writer(t, c, BLE_CHANNEL_DATA); pump(t, c); return;
}
enum ble_channel channel = wire == BLE_WIRE_DATA ? BLE_CHANNEL_DATA : BLE_CHANNEL_INVITE;
if (wire == BLE_WIRE_DATA) {
if (len <= FLOW_HEADER) {
DEBUG_WARN(CAT, "BLE missing DATA flow header link=%llu bytes=%zu", (unsigned long long)id, len);
close_link(t, c, "invalid DATA flow header"); return;
}
uint16_t seq = read16(data), advance = (uint16_t)(seq - c->rx_last);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE DATA RX link=%llu seq=%u type=%u bytes=%zu previous=%u advance=%u",
(unsigned long long)id, seq, data[FLOW_HEADER], len, c->rx_last, advance);
if (!advance || advance >= 0x8000) {
DEBUG_WARN(CAT, "BLE duplicate/stale DATA link=%llu id=%u last=%u", (unsigned long long)id, seq, c->rx_last);
if (c->rx_flow_seen) schedule_ack(t, c); return;
}
if (advance != 1) DEBUG_WARN(CAT, "BLE DATA flow gap link=%llu expected=%u received=%u missing=%u",
(unsigned long long)id, (uint16_t)(c->rx_last + 1), seq, advance - 1);
c->rx_last = seq; c->rx_flow_seen = 1;
if (!schedule_ack(t, c)) return;
data += FLOW_HEADER; len -= FLOW_HEADER;
}
struct ble_channel_state* ch = &c->channels[channel];
if (channel == BLE_CHANNEL_DATA && len == 17 && !c->ready && ((data[0] == HELLO && !c->outgoing) || (data[0] == ACCEPT && c->outgoing))) {
uint64_t peer = read64(data + 1), target = read64(data + 9);
if (!peer || peer == t->config.node_id || (target && target != t->config.node_id) || (c->peer && peer != c->peer)) {
DEBUG_WARN(CAT, "BLE invalid HELLO identity link=%llu", (unsigned long long)id); close_link(t, c, "invalid identity"); return;
}
if (t->config.callbacks.validate && !t->config.callbacks.validate(t->config.arg, id, peer)) {
DEBUG_WARN(CAT, "BLE validation rejected link=%llu", (unsigned long long)id); close_link(t, c, "validation rejected"); return;
}
c->peer = peer; c->ready = 1;
if (!c->outgoing) {
c->control[0] = ACCEPT; write64(c->control + 1, t->config.node_id); write64(c->control + 9, peer); c->control_pending = 1;
}
DEBUG_INFO(CAT, "BLE session ready link=%llu peer=%016llx", (unsigned long long)id, (unsigned long long)peer);
if (t->config.callbacks.connection) t->config.callbacks.connection(t->config.arg, id, peer, BLE_READY, "HELLO accepted");
pump(t, c); return;
}
if (!c->ready || len <= HEADER || len > c->capacity || data[0] != FRAME) {
DEBUG_WARN(CAT, "BLE invalid value link=%llu channel=%d bytes=%zu capacity=%u ready=%d type=%u",
(unsigned long long)id, channel, len, c->capacity, c->ready, len ? data[0] : 0);
c->stats.dropped++;
ch->rx_size = ch->rx_offset = 0; return;
}
uint16_t seq = read16(data + 1), total = read16(data + 3), offset = read16(data + 5);
size_t n = len - HEADER;
if (offset && ch->dropping && ch->dropped_sequence == seq) {
DEBUG_TRACE(CAT, "BLE discarded frame tail link=%llu seq=%u offset=%u", (unsigned long long)id, seq, offset); return;
}
if (!offset) ch->dropping = 0;
if (!total || total > BLE_FRAME_MAX || offset > total || n > (size_t)(total - offset)) goto invalid;
if (!offset) {
uint16_t expected = ch->rx_seen ? ch->rx_next : 1;
if (seq != expected) {
uint16_t gap = (uint16_t)(seq - expected);
DEBUG_WARN(CAT, "BLE RX sequence gap link=%llu channel=%d expected=%u received=%u missing=%u",
(unsigned long long)id, channel, expected, seq, gap < 0x8000 ? gap : 0);
if (gap < 0x8000) c->stats.missing += gap;
}
ch->rx_seen = 1; ch->rx_next = (uint16_t)(seq + 1);
if (ch->rx_size) {
DEBUG_WARN(CAT, "BLE incomplete frame replaced link=%llu channel=%d seq=%u received=%u total=%u next=%u",
(unsigned long long)id, channel, ch->rx_sequence, ch->rx_offset, ch->rx_size, seq);
c->stats.dropped++;
}
ch->rx_sequence = seq; ch->rx_size = total; ch->rx_offset = 0; ch->rx_deadline = get_time_us() + DEADLINE_US;
}
if (ch->rx_size != total || ch->rx_sequence != seq || ch->rx_offset != offset) goto invalid;
memcpy(ch->assembly + offset, data + HEADER, n); ch->rx_offset += n;
if (ch->rx_offset != total) return;
ch->rx_size = ch->rx_offset = 0;
if (queue_entry_count(ch->rx) >= BLE_QUEUE_MAX) {
DEBUG_WARN(CAT, "BLE RX full: frame dropped link=%llu channel=%d", (unsigned long long)id, channel);
c->stats.dropped++; return;
}
struct ll_entry* entry = ll_alloc_lldgram(total);
if (!entry) { DEBUG_ERROR(CAT, "BLE RX allocation failed bytes=%u", total); c->stats.dropped++; return; }
memcpy(entry->dgram, ch->assembly, total); entry->len = total;
if (queue_data_put(ch->rx, entry) != 0) { DEBUG_ERROR(CAT, "BLE RX queue invariant failed"); return; }
c->stats.rx_bytes += total; c->stats.rx_frames++;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE wire RX link=%llu channel=%d seq=%u bytes=%u tag=%08x",
(unsigned long long)id, channel, seq, total, frame_tag(entry->dgram, entry->len));
void (*received)(void*, ble_link_id) = channel == BLE_CHANNEL_DATA ? t->config.callbacks.received : t->config.callbacks.invite_received;
if (received) received(t->config.arg, id);
return;
invalid:
c->stats.dropped++;
DEBUG_WARN(CAT, "BLE fragment dropped link=%llu channel=%d seq=%u offset=%u expected=%u total=%u bytes=%zu",
(unsigned long long)id, channel, seq, offset, ch->rx_offset, total, n);
ch->rx_size = ch->rx_offset = 0;
ch->dropping = 1; ch->dropped_sequence = seq;
}
void ble_backend_sent(struct ble_transport* t, ble_link_id id, int success) {
struct ble_connection* c = lookup(t, id);
if (!c) return;
if (!success || !c->pending) { DEBUG_WARN(CAT, "BLE send completion failed link=%llu success=%d pending=%d channel=%d bytes=%u",
(unsigned long long)id, success, c->pending, c->pending_channel, c->pending_bytes);
close_link(t, c, "send failed"); return; }
enum ble_channel channel = c->pending_channel;
uint64_t waited = get_time_us() - c->submitted;
c->stats.fragments++; c->stats.wait_us += waited;
if (waited > c->stats.max_wait_us) c->stats.max_wait_us = waited;
c->pending = 0;
if (c->pending_ack) {
c->pending_ack = 0; c->ack_completed = get_time_us();
/* Даже при медленном ACK дать DATA/INVITE следующие 15ms, иначе непрерывный RX вытесняет ответы. */
if (c->ack_dirty) {
if (c->ack_timer) { uasync_cancel_timeout(t->ua, c->ack_timer); c->ack_timer = NULL; }
c->ack_due = c->ack_completed + BLE_ACK_INTERVAL_US;
if (!schedule_ack(t, c)) return;
}
pump(t, c); return;
}
struct ble_channel_state* ch = &c->channels[channel];
if (!c->pending_bytes) c->control_pending = 0;
else if (ch->current) {
ch->offset += c->pending_bytes;
if (ch->offset == ch->current->len) {
c->stats.tx_bytes += ch->current->len; c->stats.tx_frames++;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE wire TX link=%llu channel=%d seq=%u bytes=%u tag=%08x",
(unsigned long long)id, channel, ch->sequence, ch->current->len,
frame_tag(ch->current->dgram, ch->current->len));
release_entry(ch->current); ch->current = NULL;
}
}
wake_writer(t, c, channel);
pump(t, c);
}
void ble_backend_closed(struct ble_transport* t, ble_link_id id, const char* reason) {
struct ble_connection* c = lookup(t, id); if (c) close_link(t, c, reason);
}
+88
View File
@@ -0,0 +1,88 @@
/* BLE transport без Qt: create → start → advertise/connect → tx_put/rx_get → destroy.
* Все API и callbacks принадлежат переданному uasync-потоку; callbacks не вызывают stop/destroy/disconnect.
* Объявление — повторяемые ОС байты, не надёжная датаграмма. node_id в HELLO не аутентифицирован.
* Один сервис обслуживает независимые соединения; данные — непрозрачные кадры 1..1500 байт.
* Целые кадры находятся в ll_queue. DATA: Write Without Response/Notification; INVITE: Request/Indication.
* DATA имеет накопительный ACK GATT-фрагментов: ограничивает очередь ОС, без собственных ретрансляций.
* Доставку целых DATA подтверждает потребитель (ETCP); ACK не является подтверждением приложения.
* Диагностика: discovery (BLE-префикс). NULL validate разрешает запрос, как и начальная политика GUI. */
#ifndef VIBECHAT_BLE_TRANSPORT_H
#define VIBECHAT_BLE_TRANSPORT_H
#include <stddef.h>
#include <stdint.h>
#include "../ll_queue.h"
#ifdef __cplusplus
extern "C" {
#endif
#define BLE_FRAME_MAX 1500
#define BLE_BROADCAST_MAX 24
#define BLE_ENDPOINT_MAX 256
#define BLE_LINKS_MAX 16
#define BLE_QUEUE_MAX 64
/* Расчётная скорость фиксирована; окно включает DATA GATT-values с заголовками. */
#define BLE_TX_RATE_BITS 2000000u
#define BLE_TX_WINDOW_MS 50u
#define BLE_TX_WINDOW_BYTES (BLE_TX_RATE_BITS / 8u * BLE_TX_WINDOW_MS / 1000u)
#define BLE_ACK_INTERVAL_US 15000u
struct ble_transport;
struct UASYNC;
typedef uint64_t ble_link_id;
enum ble_result { BLE_OK = 0, BLE_ERROR = -1, BLE_WOULD_BLOCK = -2, BLE_NOT_READY = -3 };
enum ble_channel { BLE_CHANNEL_DATA, BLE_CHANNEL_INVITE, BLE_CHANNEL_COUNT };
enum ble_state { BLE_STARTING, BLE_READY, BLE_CLOSED };
struct ble_endpoint { char id[BLE_ENDPOINT_MAX]; }; /* Непрозрачный адрес backend, копируется целиком. */
struct ble_callbacks {
/* Объявление нужного company_id; буфер заимствован до возврата. RSSI=127 означает неизвестный. */
void (*broadcast)(void* arg, const struct ble_endpoint* endpoint, const uint8_t* data, size_t len, int rssi);
void (*status)(void* arg, enum ble_state state, const char* reason); /* Состояние радио, reason заимствован. */
void (*advertising)(void* arg, enum ble_state state, const char* reason); /* Публикация bytes: READY не подтверждает приём. */
int (*validate)(void* arg, ble_link_id link, uint64_t node_id); /* Ненулевой результат разрешает HELLO. */
void (*connection)(void* arg, ble_link_id link, uint64_t node_id, enum ble_state state, const char* reason);
void (*received)(void* arg, ble_link_id link); /* rx_get до NULL; допустим синхронный drain. */
void (*writable)(void* arg, ble_link_id link); /* После заполнения TX снова можно вызвать tx_put. */
void (*invite_received)(void* arg, ble_link_id link); /* drain INVITE через channel_rx_get. */
void (*invite_writable)(void* arg, ble_link_id link); /* Освободилось место в отдельной TX очереди. */
};
struct ble_config {
uint64_t node_id; /* Ненулевой ID для HELLO; не криптографическое доказательство. */
uint16_t company_id; /* Manufacturer ID, одинаковый у участников. 0xffff только для разработки. */
const char* adapter; /* Linux: hci0 либо NULL (первый адаптер); Windows: NULL. Копируется. */
struct ble_callbacks callbacks;
void* arg; /* Заимствованный контекст callbacks, живёт до destroy. */
unsigned tx_limit; /* Целых TX-пакетов, включая фрагментируемый: 0=64, 1..64. */
};
/* Создать остановленный transport. ua/config заимствованы только ua; возвращает handle или NULL с логом. */
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config);
/* Начать регистрацию сервиса/scan; 0 означает запуск, готовность сообщает status. Повторный start — ошибка. */
int ble_start(struct ble_transport* transport);
/* Остановить свои объявления/scan/сессии, завершить pending и освободить очереди. Идемпотентно, вне callbacks. */
void ble_stop(struct ble_transport* transport);
/* Разрешить/запретить новые входящие; запрет закрывает текущие входящие. По умолчанию разрешены. */
void ble_set_incoming(struct ble_transport* transport, int allowed);
void ble_destroy(struct ble_transport* transport); /* stop + free; NULL допустим. */
/* Заменить повторяемое manufacturer-объявление (0..24 байта); копирует данные. NULL,0 выключает его.
* Регистрация не гарантирует выход в эфир/получение. Собственное connectable-объявление сервиса независимо. */
int ble_advertise(struct ble_transport* transport, const void* data, size_t len);
/* Самостоятельно выполнить connect/discovery/subscribe/HELLO. expected_node=0 не проверяет ID ответа.
* Handle либо 0 при отказе. Готовность/ошибка — connection; физическое ACL может обслуживать другие сервисы. */
ble_link_id ble_connect(struct ble_transport* transport, const struct ble_endpoint* endpoint, uint64_t expected_node);
/* Копировать направление существующего GATT в outgoing (0/1). 0 — успех, -1 — неверный handle/аргументы с логом. */
int ble_get_outgoing(struct ble_transport* transport, ble_link_id link, int* outgoing);
void ble_disconnect(struct ble_transport* transport, ble_link_id link);
/* Принять целый ll_entry в TX. При 0 владение передано; при любом отказе entry остаётся вызывающему.
* entry должен быть вне очереди, dgram!=NULL, len=1..1500. Заполнение: WOULD_BLOCK, затем writable. */
int ble_tx_put(struct ble_transport* transport, ble_link_id link, struct ll_entry* entry);
/* Снимок готовности принять пакет; заполнение учитывает текущую фрагментацию.
* После заполнения writable вызывается при освобождении места, без обязательного отказа tx_put. */
int ble_tx_ready(struct ble_transport* transport, ble_link_id link);
/* Извлечь целый кадр из RX или NULL; вызывающий освобождает queue_dgram_free + queue_entry_free. */
struct ll_entry* ble_rx_get(struct ble_transport* transport, ble_link_id link);
/* Каналы имеют независимые очереди/сборку, но один общий GATT send. INVITE TX ограничен одним пакетом.
* Контракт владения и возвратов как у DATA API выше. Планировщик чередует фрагменты каналов. */
int ble_channel_tx_ready(struct ble_transport* transport, ble_link_id link, enum ble_channel channel);
int ble_channel_tx_put(struct ble_transport* transport, ble_link_id link, enum ble_channel channel, struct ll_entry* entry);
struct ll_entry* ble_channel_rx_get(struct ble_transport* transport, ble_link_id link, enum ble_channel channel);
#ifdef __cplusplus
}
#endif
#endif
+765
View File
@@ -0,0 +1,765 @@
/* WinRT backend: события Windows сериализуются в uasync через ограниченный mailbox.
* Каждая отправка адресована link/subscribedClient; одна незавершённая операция на link.
* Слабые ссылки в WinRT handlers и отменяемый posted_task исключают обращения к уничтоженному core. */
#include "ble_backend.h"
#include "../u_async.h"
#include "../mem.h"
#include "../debug_config.h"
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Foundation.Metadata.h>
#include <winrt/Windows.System.Profile.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
#include <winrt/Windows.Storage.Streams.h>
#include <array>
#include <algorithm>
#include <vector>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <cstring>
#include <cstdio>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Devices::Bluetooth;
using namespace Windows::Devices::Bluetooth::Advertisement;
using namespace Windows::Devices::Bluetooth::GenericAttributeProfile;
using namespace Windows::Storage::Streams;
static constexpr auto CAT = DEBUG_CATEGORY_DISCOVERY;
static const char* bluetooth_error(BluetoothError error) {
switch (error) {
case BluetoothError::Success: return "Success";
case BluetoothError::RadioNotAvailable: return "RadioNotAvailable";
case BluetoothError::ResourceInUse: return "ResourceInUse";
case BluetoothError::DeviceNotConnected: return "DeviceNotConnected";
case BluetoothError::DisabledByPolicy: return "DisabledByPolicy";
case BluetoothError::NotSupported: return "NotSupported";
case BluetoothError::DisabledByUser: return "DisabledByUser";
case BluetoothError::ConsentRequired: return "ConsentRequired";
case BluetoothError::TransportNotSupported: return "TransportNotSupported";
default: return "OtherError";
}
}
/* Вызывать только из catch; сохраняем контекст и HRESULT вместо немого catch(...). */
static std::string exception_reason(const char* phase, ble_link_id id = 0) {
char prefix[192];
try { throw; }
catch (const hresult_error& e) {
std::snprintf(prefix, sizeof(prefix), "BLE WinRT phase=%s link=%llu HRESULT=0x%08x: ",
phase, (unsigned long long)id, (unsigned)e.code().value);
return prefix + to_string(e.message());
} catch (const std::exception& e) {
return std::string("BLE WinRT phase=") + phase + ": " + e.what();
} catch (...) { return std::string("BLE WinRT phase=") + phase + ": unknown exception"; }
}
struct WinState;
struct WinLink {
ble_link_id id = 0;
std::string endpoint;
hstring device_id;
bool outgoing = false, opened = false, sending = false;
BluetoothLEDevice device{nullptr};
GattDeviceService service{nullptr};
GattCharacteristic rx{nullptr}, tx{nullptr}, invite{nullptr}, ack{nullptr};
GattSession session{nullptr};
BluetoothLEPreferredConnectionParametersRequest speed_request{nullptr}; /* Живёт до закрытия link. */
GattSubscribedClient subscriber{nullptr}, invite_subscriber{nullptr}, ack_subscriber{nullptr};
IAsyncInfo operation{nullptr};
GattCharacteristic::ValueChanged_revoker value_changed, invite_changed, ack_changed;
GattSession::SessionStatusChanged_revoker session_changed;
GattSession::MaxPduSizeChanged_revoker pdu_changed;
BluetoothLEDevice::ConnectionParametersChanged_revoker parameters_changed;
BluetoothLEDevice::ConnectionPhyChanged_revoker phy_changed;
};
struct WriteDeferral {
Deferral value;
explicit WriteDeferral(Deferral v) : value(v) {}
~WriteDeferral() { try { value.Complete(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("write deferral").c_str()); } }
};
struct WinState : std::enable_shared_from_this<WinState> {
/* Имена имеют статическое время жизни; контекст ошибки сохраняется независимо от уровня лога. */
struct Event {
const char* name;
ble_link_id id;
std::function<void(WinState&)> run;
};
struct UASYNC* ua = nullptr;
struct ble_transport* owner = nullptr;
uint16_t company = 0;
std::mutex mutex;
std::deque<Event> events;
const char* operation = "idle"; /* Только uasync; Windows callbacks передают контекст через Event. */
ble_link_id operation_link = 0;
struct posted_task* posted = nullptr;
bool stopped = false, failed = false, overflow = false, apartment = false, scan_ready = false, service_ready = false;
IAsyncInfo startup{nullptr};
BluetoothLEAdvertisementWatcher watcher{nullptr};
BluetoothLEAdvertisementPublisher publisher{nullptr};
GattServiceProvider provider{nullptr};
GattLocalCharacteristic local_rx{nullptr}, local_tx{nullptr}, local_invite{nullptr}, local_ack{nullptr};
BluetoothLEAdvertisementWatcher::Received_revoker received;
BluetoothLEAdvertisementWatcher::Stopped_revoker scan_stopped;
BluetoothLEAdvertisementPublisher::StatusChanged_revoker publish_changed;
GattServiceProvider::AdvertisementStatusChanged_revoker service_changed;
GattLocalCharacteristic::WriteRequested_revoker write_requested, invite_write_requested, ack_write_requested;
GattLocalCharacteristic::SubscribedClientsChanged_revoker subscriptions_changed, invite_subscriptions_changed, ack_subscriptions_changed;
std::map<ble_link_id, std::unique_ptr<WinLink>> links;
std::array<uint8_t, BLE_BROADCAST_MAX> broadcast{};
size_t broadcast_size = 0;
bool broadcast_enabled = false;
uint64_t publish_generation = 0;
/* Отмечает конкретный вызов до обращения к WinRT, включая синхронные исключения. */
void step(const char* name, ble_link_id id = 0) {
operation = name; operation_link = id;
DEBUG_TRACE(CAT, "BLE WinRT operation=%s link=%llu", name, (unsigned long long)id);
}
static void dispatch(void* arg) {
auto& s = *static_cast<WinState*>(arg);
std::deque<Event> batch;
bool overloaded;
{ std::lock_guard guard(s.mutex); s.posted = nullptr; batch.swap(s.events); overloaded = s.overflow; s.overflow = false; }
if (overloaded) { s.fail("WinRT event queue overflow"); return; }
DEBUG_TRACE(CAT, "BLE WinRT dispatch events=%zu links=%zu", batch.size(), s.links.size());
for (auto& event : batch) {
if (s.stopped || s.failed) break;
s.step(event.name, event.id);
try { event.run(s); }
catch (...) {
auto reason = exception_reason(s.operation, s.operation_link);
DEBUG_ERROR(CAT, "BLE WinRT event=%s event_link=%llu: %s", event.name,
(unsigned long long)event.id, reason.c_str());
if (event.id) s.error(event.id, reason.c_str());
else s.fail(reason.c_str());
}
}
}
void post(const char* name, ble_link_id id, std::function<void(WinState&)> event) {
std::lock_guard guard(mutex);
if (stopped) return;
if (events.size() >= 256) {
if (!overflow) DEBUG_ERROR(CAT, "BLE WinRT event mailbox full queued=%zu limit=256", events.size());
overflow = true;
}
else events.push_back({name, id, std::move(event)});
if (posted) return;
posted = static_cast<struct posted_task*>(u_calloc(1, sizeof(struct posted_task)));
if (!posted) { overflow = true; DEBUG_ERROR(CAT, "BLE WinRT event allocation failed"); return; }
posted->callback = dispatch; posted->arg = this; uasync_post_reserved(ua, posted);
}
WinLink* link(ble_link_id id) { auto it = links.find(id); return it == links.end() ? nullptr : it->second.get(); }
void fail(const char* reason) {
if (failed) return;
DEBUG_ERROR(CAT, "BLE WinRT radio failure scan_ready=%d service_ready=%d links=%zu publisher_generation=%llu",
scan_ready, service_ready, links.size(), (unsigned long long)publish_generation);
failed = true; DEBUG_ERROR(CAT, "BLE WinRT failed: %s", reason); ble_backend_status(owner, 0, reason);
}
void error(ble_link_id id, const char* reason) {
DEBUG_WARN(CAT, "BLE WinRT link=%llu failed: %s", (unsigned long long)id, reason);
ble_backend_closed(owner, id, reason);
}
void radio_error(const char* phase, BluetoothError error) {
char reason[192];
std::snprintf(reason, sizeof(reason), "%s: BluetoothError=%d (%s)", phase, (int)error, bluetooth_error(error));
fail(reason);
}
template<typename Result, typename Callback>
void await(const char* phase, const IAsyncOperation<Result>& operation, ble_link_id id, Callback callback) {
step(phase, id);
const bool fragment = !std::strcmp(phase, "write frame") || !std::strcmp(phase, "notify frame");
if (!fragment) DEBUG_DEBUG(CAT, "BLE WinRT async begin phase=%s link=%llu", phase, (unsigned long long)id);
if (id) { auto* l = link(id); if (!l) { operation.Cancel(); return; } l->operation = operation; }
else startup = operation;
auto weak = weak_from_this();
operation.Completed([weak, id, phase, fragment, callback](const IAsyncOperation<Result>& op, AsyncStatus status) {
if (auto s = weak.lock()) s->post(phase, id, [id, op, status, phase, fragment, callback](WinState& self) {
auto* l = id ? self.link(id) : nullptr;
if (id && !l) {
DEBUG_DEBUG(CAT, "BLE WinRT stale completion phase=%s link=%llu status=%d", phase, (unsigned long long)id, (int)status);
return;
}
if (l) l->operation = nullptr; else self.startup = nullptr;
if (status != AsyncStatus::Completed) {
char reason[192];
std::snprintf(reason, sizeof(reason), "phase=%s async_status=%d HRESULT=0x%08x", phase, (int)status,
(unsigned)op.ErrorCode().value);
if (id) self.error(id, reason); else self.fail(reason);
return;
}
if (!fragment) DEBUG_DEBUG(CAT, "BLE WinRT async complete phase=%s link=%llu", phase, (unsigned long long)id);
self.step("IAsyncOperation.GetResults", id);
auto result = op.GetResults();
self.step(phase, id);
callback(self, result);
});
});
}
static IBuffer buffer(const uint8_t* data, size_t len) {
DataWriter writer; writer.WriteBytes(array_view<const uint8_t>(data, data + len)); return writer.DetachBuffer();
}
static std::vector<uint8_t> bytes(const IBuffer& buffer) {
if (buffer.Length() > BLE_VALUE_MAX) throw hresult_invalid_argument();
std::vector<uint8_t> result(buffer.Length()); DataReader::FromBuffer(buffer).ReadBytes(result); return result;
}
void ready() {
DEBUG_DEBUG(CAT, "BLE WinRT readiness scan=%d service=%d", scan_ready, service_ready);
if (scan_ready && service_ready) ble_backend_status(owner, 1, "WinRT scan/GATT ready");
}
/* Только доступные Win11 API: фактические параметры отделены от запрошенного профиля. */
void log_connection(ble_link_id id) {
auto* l = link(id);
if (!l || !l->device) return;
try {
auto api = l->device.try_as<IBluetoothLEDevice6>();
if (!api) return;
auto parameters = api.GetConnectionParameters(); auto phy = api.GetConnectionPhy();
DEBUG_INFO(CAT, "BLE WinRT negotiated link=%llu connected=%d interval_ms=%.2f latency=%u timeout_ms=%u TX_2M=%d RX_2M=%d TX_coded=%d RX_coded=%d",
(unsigned long long)id, l->device.ConnectionStatus() == BluetoothConnectionStatus::Connected,
parameters.ConnectionInterval() * 1.25, parameters.ConnectionLatency(), parameters.LinkTimeout() * 10,
phy.TransmitInfo().IsUncoded2MPhy(), phy.ReceiveInfo().IsUncoded2MPhy(),
phy.TransmitInfo().IsCodedPhy(), phy.ReceiveInfo().IsCodedPhy());
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("read connection parameters/PHY", id).c_str()); }
}
void prefer_speed(WinLink& l) {
try {
auto api = l.device.try_as<IBluetoothLEDevice6>();
if (!api) { DEBUG_INFO(CAT, "BLE WinRT speed API unavailable link=%llu; using OS parameters", (unsigned long long)l.id); return; }
auto weak = weak_from_this(); auto id = l.id;
auto changed = [weak, id](auto const&, auto const&) {
if (auto s = weak.lock()) s->post("connection parameters/PHY changed", id, [id](WinState& s) { s.log_connection(id); });
};
l.parameters_changed = api.ConnectionParametersChanged(auto_revoke, changed);
l.phy_changed = api.ConnectionPhyChanged(auto_revoke, changed);
l.speed_request = api.RequestPreferredConnectionParameters(BluetoothLEPreferredConnectionParameters::ThroughputOptimized());
auto status = l.speed_request.Status();
if (status == BluetoothLEPreferredConnectionParametersRequestStatus::Success)
DEBUG_INFO(CAT, "BLE WinRT ThroughputOptimized requested link=%llu; PHY selected by OS/peer", (unsigned long long)id);
else DEBUG_WARN(CAT, "BLE WinRT ThroughputOptimized request link=%llu status=%d", (unsigned long long)id, (int)status);
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("request throughput profile", l.id).c_str()); }
}
void watch_session(WinLink& l) {
auto weak = weak_from_this(); auto id = l.id;
step("GattSession.SessionStatusChanged registration (auto_revoke)", id);
l.session_changed = l.session.SessionStatusChanged(auto_revoke, [weak, id](auto const&, GattSessionStatusChangedEventArgs const& a) {
if (auto s = weak.lock()) s->post("GattSession.SessionStatusChanged", id, [id, a](WinState& s) {
auto status = a.Status(); auto error = a.Error();
DEBUG_DEBUG(CAT, "BLE WinRT session link=%llu status=%d error=%d (%s)",
(unsigned long long)id, (int)status, (int)error, bluetooth_error(error));
if (status == GattSessionStatus::Closed && s.link(id)) s.error(id, "GATT session closed");
});
});
step("GattSession.MaxPduSizeChanged registration (auto_revoke)", id);
l.pdu_changed = l.session.MaxPduSizeChanged(auto_revoke, [weak, id](auto const& session, auto const&) {
if (auto s = weak.lock()) s->post("GattSession.MaxPduSizeChanged", id, [id, session](WinState& s) {
if (!s.link(id)) return;
auto mtu = session.MaxPduSize();
DEBUG_DEBUG(CAT, "BLE WinRT negotiated PDU changed link=%llu mtu=%u", (unsigned long long)id, (unsigned)mtu);
});
});
}
WinLink* incoming(const GattSession& session) {
step("incoming GattSession.DeviceId");
hstring key = session.DeviceId().Id();
for (auto& [id, l] : links) if (l->device_id == key) return l->outgoing ? nullptr : l.get();
std::string endpoint = to_string(key);
if (endpoint.size() >= BLE_ENDPOINT_MAX || links.size() >= BLE_LINKS_MAX) {
DEBUG_WARN(CAT, "BLE WinRT incoming endpoint/connection limit"); return nullptr;
}
struct ble_endpoint ep{}; std::memcpy(ep.id, endpoint.c_str(), endpoint.size() + 1);
ble_link_id id = ble_backend_incoming(owner, &ep); if (!id) return nullptr;
auto l = std::make_unique<WinLink>(); l->id = id; l->endpoint = endpoint; l->device_id = key; l->session = session;
DEBUG_DEBUG(CAT, "BLE WinRT incoming link=%llu endpoint=%s mtu=%u", (unsigned long long)id, endpoint.c_str(), session.MaxPduSize());
auto* result = l.get(); links.emplace(id, std::move(l)); watch_session(*result); return result;
}
void subscriptions() {
std::vector<ble_link_id> active;
step("GattLocalCharacteristic.SubscribedClients");
auto invitations = local_invite.SubscribedClients(), acknowledgements = local_ack.SubscribedClients();
auto clients = local_tx.SubscribedClients();
DEBUG_DEBUG(CAT, "BLE WinRT subscriptions DATA=%u INVITE=%u ACK=%u links=%zu",
clients.Size(), invitations.Size(), acknowledgements.Size(), links.size());
for (auto const& client : clients) {
step("GattSubscribedClient.Session/DeviceId");
auto same_device = [&client](auto const& other) { return client.Session().DeviceId().Id() == other.Session().DeviceId().Id(); };
auto invitation = std::find_if(invitations.begin(), invitations.end(), same_device);
auto acknowledgement = std::find_if(acknowledgements.begin(), acknowledgements.end(), same_device);
if (invitation == invitations.end() || acknowledgement == acknowledgements.end()) {
DEBUG_DEBUG(CAT, "BLE WinRT subscriptions incomplete device=%s DATA=1 INVITE=%d ACK=%d",
to_string(client.Session().DeviceId().Id()).c_str(),
invitation != invitations.end(), acknowledgement != acknowledgements.end());
continue;
}
auto* l = incoming(client.Session()); if (!l) continue;
l->subscriber = client; l->invite_subscriber = *invitation; l->ack_subscriber = *acknowledgement; active.push_back(l->id);
if (!l->opened) {
step("GattSubscribedClient.MaxNotificationSize", l->id);
auto capacity = std::min({client.MaxNotificationSize(), (*invitation).MaxNotificationSize(), (*acknowledgement).MaxNotificationSize()});
DEBUG_DEBUG(CAT, "BLE WinRT subscribed DATA/INVITE/ACK link=%llu capacity=%u", (unsigned long long)l->id, capacity);
l->opened = true; ble_backend_opened(owner, l->id, capacity + 3);
}
}
std::vector<ble_link_id> gone;
for (auto& [id, l] : links) if (!l->outgoing && l->opened && std::find(active.begin(), active.end(), id) == active.end()) gone.push_back(id);
for (auto id : gone) error(id, "notification subscription removed");
}
void make_server() {
await("create GATT service", GattServiceProvider::CreateAsync(guid(BLE_SERVICE_UUID)), 0, [](WinState& s, GattServiceProviderResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create GATT service", result.Error()); return; }
s.provider = result.ServiceProvider();
GattLocalCharacteristicParameters parameters;
parameters.CharacteristicProperties(GattCharacteristicProperties::WriteWithoutResponse);
parameters.WriteProtectionLevel(GattProtectionLevel::Plain);
s.await("create RX characteristic", s.provider.Service().CreateCharacteristicAsync(guid(BLE_RX_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create RX characteristic", result.Error()); return; }
s.local_rx = result.Characteristic(); s.make_tx();
});
});
}
void make_tx() {
GattLocalCharacteristicParameters parameters; parameters.CharacteristicProperties(GattCharacteristicProperties::Notify);
parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
await("create TX characteristic", provider.Service().CreateCharacteristicAsync(guid(BLE_TX_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create TX characteristic", result.Error()); return; }
s.local_tx = result.Characteristic(); s.make_invite();
});
}
void make_invite() {
GattLocalCharacteristicParameters parameters;
parameters.CharacteristicProperties(GattCharacteristicProperties::Write | GattCharacteristicProperties::Indicate);
parameters.WriteProtectionLevel(GattProtectionLevel::Plain); parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
await("create invitation characteristic", provider.Service().CreateCharacteristicAsync(guid(BLE_INVITE_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create invitation characteristic", result.Error()); return; }
s.local_invite = result.Characteristic(); s.make_ack();
});
}
void make_ack() {
GattLocalCharacteristicParameters parameters;
parameters.CharacteristicProperties(GattCharacteristicProperties::Write | GattCharacteristicProperties::Indicate);
parameters.WriteProtectionLevel(GattProtectionLevel::Plain); parameters.ReadProtectionLevel(GattProtectionLevel::Plain);
await("create ACK characteristic", provider.Service().CreateCharacteristicAsync(guid(BLE_ACK_UUID), parameters), 0,
[](WinState& s, GattLocalCharacteristicResult const& result) {
if (result.Error() != BluetoothError::Success) { s.radio_error("create ACK characteristic", result.Error()); return; }
s.local_ack = result.Characteristic(); s.start_server();
});
}
GattLocalCharacteristic::WriteRequested_revoker watch_write(GattLocalCharacteristic const& characteristic, enum ble_wire_channel channel) {
auto weak = weak_from_this();
step("GattLocalCharacteristic.WriteRequested registration (auto_revoke)");
DEBUG_DEBUG(CAT, "BLE WinRT register WriteRequested channel=%d", channel);
return characteristic.WriteRequested(auto_revoke, [weak, channel](auto const&, GattWriteRequestedEventArgs const& args) {
const char* phase = "WriteRequested.GetDeferral";
try {
auto deferral = std::make_shared<WriteDeferral>(args.GetDeferral());
phase = "WriteRequested.GetRequestAsync"; auto request = args.GetRequestAsync();
phase = "WriteRequested.Session"; auto session = args.Session();
phase = "WriteRequested.GetRequestAsync.Completed registration";
request.Completed([weak, deferral, session, channel](auto const& operation, AsyncStatus status) {
if (auto s = weak.lock()) s->post("GattLocalCharacteristic.WriteRequested", 0,
[deferral, session, operation, status, channel](WinState& s) {
if (status != AsyncStatus::Completed) {
DEBUG_WARN(CAT, "BLE WinRT GetRequest channel=%d status=%d HRESULT=0x%08x",
channel, (int)status, (unsigned)operation.ErrorCode().value);
return;
}
s.step("GattWriteRequestedEventArgs.GetRequestAsync.GetResults");
auto request = operation.GetResults(); if (!request) { DEBUG_WARN(CAT, "BLE WinRT expired write request"); return; }
s.step("GattWriteRequest.Option/Offset/Value");
bool response = request.Option() == GattWriteOption::WriteWithResponse;
auto expected = channel == BLE_WIRE_DATA ? GattWriteOption::WriteWithoutResponse : GattWriteOption::WriteWithResponse;
if (request.Offset() || request.Option() != expected || !request.Value().Length() || request.Value().Length() > BLE_VALUE_MAX) {
DEBUG_WARN(CAT, "BLE WinRT invalid write offset=%u option=%d bytes=%u", request.Offset(),
(int)request.Option(), request.Value().Length());
if (response) request.RespondWithProtocolError(0x06);
return;
}
auto* l = s.incoming(session);
if (!l) { if (response) request.RespondWithProtocolError(0x08); return; }
if (!l->opened) {
/* WriteRequested может обогнать posted SubscribedClientsChanged: перечитать реальные CCC. */
auto id = l->id; s.subscriptions(); l = s.link(id);
if (!l || !l->opened) {
DEBUG_WARN(CAT, "BLE WinRT write before mandatory subscriptions link=%llu", (unsigned long long)id);
if (response) request.RespondWithProtocolError(0x08);
return;
}
}
auto id = l->id;
s.step("GattWriteRequest.Value/DataReader", id); auto data = bytes(request.Value());
DEBUG_TRACE(CAT, "BLE WinRT RX write link=%llu channel=%d bytes=%zu response=%d",
(unsigned long long)id, channel, data.size(), response);
ble_backend_received(s.owner, id, channel, data.data(), data.size());
s.step("GattWriteRequest.Respond", id);
if (response) { if (s.link(id)) request.Respond(); else request.RespondWithProtocolError(0x08); }
});
});
} catch (...) {
auto reason = exception_reason(phase);
if (auto s = weak.lock()) s->post("WriteRequested failure", 0, [reason](WinState& s) { s.fail(reason.c_str()); });
}
});
}
void start_server() {
auto weak = weak_from_this();
write_requested = watch_write(local_rx, BLE_WIRE_DATA);
invite_write_requested = watch_write(local_invite, BLE_WIRE_INVITE);
ack_write_requested = watch_write(local_ack, BLE_WIRE_ACK);
step("ACK.SubscribedClientsChanged registration (auto_revoke)");
ack_subscriptions_changed = local_ack.SubscribedClientsChanged(auto_revoke, [weak](auto const&, auto const&) {
if (auto s = weak.lock()) s->post("ACK.SubscribedClientsChanged", 0, [](WinState& s) { s.subscriptions(); });
});
step("INVITE.SubscribedClientsChanged registration (auto_revoke)");
invite_subscriptions_changed = local_invite.SubscribedClientsChanged(auto_revoke, [weak](auto const&, auto const&) {
if (auto s = weak.lock()) s->post("INVITE.SubscribedClientsChanged", 0, [](WinState& s) { s.subscriptions(); });
});
step("DATA.SubscribedClientsChanged registration (auto_revoke)");
subscriptions_changed = local_tx.SubscribedClientsChanged(auto_revoke, [weak](auto const&, auto const&) {
if (auto s = weak.lock()) s->post("DATA.SubscribedClientsChanged", 0, [](WinState& s) { s.subscriptions(); });
});
step("GattServiceProvider.AdvertisementStatusChanged registration (auto_revoke)");
service_changed = provider.AdvertisementStatusChanged(auto_revoke, [weak](auto const&, auto const& args) {
if (auto s = weak.lock()) s->post("GattServiceProvider.AdvertisementStatusChanged", 0, [args](WinState& s) {
auto status = args.Status(); auto error = args.Error();
DEBUG_DEBUG(CAT, "BLE WinRT service advertising state=%d error=%d (%s)", (int)status, (int)error, bluetooth_error(error));
if (status == GattServiceProviderAdvertisementStatus::Started) { s.service_ready = true; s.ready(); }
else if (status == GattServiceProviderAdvertisementStatus::Aborted) s.radio_error("GATT advertising aborted", error);
else DEBUG_INFO(CAT, "BLE WinRT service advertising state=%d", (int)status);
});
});
GattServiceProviderAdvertisingParameters parameters; parameters.IsConnectable(true); parameters.IsDiscoverable(true);
DEBUG_DEBUG(CAT, "BLE WinRT start server service=%s connectable=1 discoverable=1", BLE_SERVICE_UUID);
step("GattServiceProvider.StartAdvertising"); provider.StartAdvertising(parameters);
step("BluetoothLEAdvertisementWatcher construction/ScanningMode");
watcher = BluetoothLEAdvertisementWatcher(); watcher.ScanningMode(BluetoothLEScanningMode::Active);
step("Watcher.Received registration (auto_revoke)");
received = watcher.Received(auto_revoke, [weak](auto const&, BluetoothLEAdvertisementReceivedEventArgs const& args) {
auto state = weak.lock(); if (!state) return;
try {
bool matching = false;
for (auto const& value : args.Advertisement().ManufacturerData()) if (value.CompanyId() == state->company) matching = true;
if (!matching) return;
} catch (...) {
auto reason = exception_reason("Received manufacturer filter");
state->post("Watcher.Received filter failure", 0, [reason](WinState& s) { s.fail(reason.c_str()); });
return;
}
state->post("Watcher.Received", 0, [args](WinState& s) {
s.step("Received.Advertisement.ManufacturerData");
for (auto const& value : args.Advertisement().ManufacturerData()) {
if (value.CompanyId() != s.company || value.Data().Length() > BLE_BROADCAST_MAX) continue;
struct ble_endpoint ep{};
s.step("Received.QueryInterface(IBluetoothLEAdvertisementReceivedEventArgs2)");
auto address_api = args.try_as<IBluetoothLEAdvertisementReceivedEventArgs2>();
if (!address_api) {
s.fail("Received: IBluetoothLEAdvertisementReceivedEventArgs2 unavailable; Windows 10 2004+ required"); return;
}
s.step("Received.BluetoothAddressType"); auto type = address_api.BluetoothAddressType();
s.step("Received.BluetoothAddress"); auto address = args.BluetoothAddress();
std::snprintf(ep.id, sizeof(ep.id), "%012llx/%d", (unsigned long long)address, (int)type);
s.step("Received manufacturer buffer/DataReader");
auto data = bytes(value.Data());
s.step("Received.RawSignalStrengthInDBm"); auto rssi = args.RawSignalStrengthInDBm();
DEBUG_TRACE(CAT, "BLE WinRT broadcast endpoint=%s company=%04x bytes=%zu rssi=%d", ep.id, s.company,
data.size(), rssi);
s.step("ble_backend_broadcast");
ble_backend_broadcast(s.owner, &ep, data.data(), data.size(), rssi);
}
});
});
step("Watcher.Stopped registration (auto_revoke)");
scan_stopped = watcher.Stopped(auto_revoke, [weak](auto const&, auto const& args) {
if (auto s = weak.lock()) s->post("Watcher.Stopped", 0, [args](WinState& s) {
s.radio_error("scan stopped", args.Error());
});
});
step("Watcher.Start"); watcher.Start();
DEBUG_DEBUG(CAT, "BLE WinRT scan requested mode=active status=%d company=%04x", (int)watcher.Status(), company);
scan_ready = true; ready();
}
void publish() {
step("Publisher.Status");
DEBUG_DEBUG(CAT, "BLE WinRT publish requested enabled=%d bytes=%zu generation=%llu", broadcast_enabled,
broadcast_size, (unsigned long long)publish_generation);
if (publisher) {
auto status = publisher.Status();
if (status == BluetoothLEAdvertisementPublisherStatus::Started || status == BluetoothLEAdvertisementPublisherStatus::Waiting) {
publisher.Stop(); return;
}
if (status == BluetoothLEAdvertisementPublisherStatus::Stopping) return;
publish_changed.revoke(); publisher = nullptr;
}
if (!broadcast_enabled) return;
step("Publisher construction/manufacturer data");
BluetoothLEAdvertisement advertisement;
BluetoothLEManufacturerData data; data.CompanyId(company); data.Data(buffer(broadcast.data(), broadcast_size));
advertisement.ManufacturerData().Append(data); publisher = BluetoothLEAdvertisementPublisher(advertisement);
auto weak = weak_from_this(); auto generation = ++publish_generation;
step("Publisher.StatusChanged registration (auto_revoke)");
publish_changed = publisher.StatusChanged(auto_revoke, [weak, generation](auto const&, auto const& args) {
if (auto s = weak.lock()) s->post("Publisher.StatusChanged", 0, [args, generation](WinState& s) {
if (generation != s.publish_generation) {
DEBUG_DEBUG(CAT, "BLE WinRT stale publisher event generation=%llu current=%llu",
(unsigned long long)generation, (unsigned long long)s.publish_generation);
return;
}
auto status = args.Status(); auto error = args.Error();
DEBUG_DEBUG(CAT, "BLE WinRT broadcast state=%d error=%d (%s) generation=%llu", (int)status, (int)error,
bluetooth_error(error), (unsigned long long)generation);
if (status == BluetoothLEAdvertisementPublisherStatus::Aborted) s.radio_error("broadcast publication aborted", error);
else if (status == BluetoothLEAdvertisementPublisherStatus::Stopped) {
ble_backend_advertising(s.owner, BLE_CLOSED, "publisher stopped"); s.publish();
} else if (status == BluetoothLEAdvertisementPublisherStatus::Started) ble_backend_advertising(s.owner, BLE_READY, "publisher started");
else if (status == BluetoothLEAdvertisementPublisherStatus::Waiting)
ble_backend_advertising(s.owner, BLE_STARTING, "publisher waiting for radio resources");
else if (status == BluetoothLEAdvertisementPublisherStatus::Stopping)
ble_backend_advertising(s.owner, BLE_STARTING, "publisher stopping");
else ble_backend_advertising(s.owner, BLE_STARTING, "publisher created");
});
});
step("Publisher.Start"); publisher.Start();
}
void connect(ble_link_id id, uint64_t address, BluetoothAddressType type) {
DEBUG_DEBUG(CAT, "BLE WinRT connect link=%llu address=%012llx type=%d", (unsigned long long)id, (unsigned long long)address, (int)type);
await("open BLE device", BluetoothLEDevice::FromBluetoothAddressAsync(address, type), id, [id](WinState& s, BluetoothLEDevice const& device) {
auto* l = s.link(id);
if (!device) { s.error(id, "BLE device unavailable"); return; }
l->device = device; l->device_id = device.BluetoothDeviceId().Id();
DEBUG_DEBUG(CAT, "BLE WinRT device link=%llu endpoint=%s status=%d", (unsigned long long)id,
to_string(l->device_id).c_str(), (int)device.ConnectionStatus());
s.await("discover GATT service", device.GetGattServicesForUuidAsync(guid(BLE_SERVICE_UUID), BluetoothCacheMode::Uncached), id,
[id](WinState& s, GattDeviceServicesResult const& result) {
auto protocol = result.ProtocolError();
DEBUG_DEBUG(CAT, "BLE WinRT services link=%llu status=%d ATT_error=%d count=%u", (unsigned long long)id,
(int)result.Status(), protocol ? int(protocol.Value()) : -1,
result.Status() == GattCommunicationStatus::Success ? result.Services().Size() : 0);
if (result.Status() != GattCommunicationStatus::Success || result.Services().Size() != 1) {
DEBUG_WARN(CAT, "BLE WinRT service unavailable link=%llu status=%d ATT_error=%d",
(unsigned long long)id, (int)result.Status(), protocol ? int(protocol.Value()) : -1);
s.error(id, "VibeChat GATT service unavailable/ambiguous"); return;
}
auto* l = s.link(id); l->service = result.Services().GetAt(0);
s.await("discover GATT characteristics", l->service.GetCharacteristicsAsync(BluetoothCacheMode::Uncached), id,
[id](WinState& s, GattCharacteristicsResult const& result) {
auto protocol = result.ProtocolError();
DEBUG_DEBUG(CAT, "BLE WinRT characteristics link=%llu status=%d ATT_error=%d count=%u", (unsigned long long)id,
(int)result.Status(), protocol ? int(protocol.Value()) : -1,
result.Status() == GattCommunicationStatus::Success ? result.Characteristics().Size() : 0);
if (result.Status() != GattCommunicationStatus::Success) {
DEBUG_WARN(CAT, "BLE WinRT discovery failed link=%llu status=%d ATT_error=%d",
(unsigned long long)id, (int)result.Status(), protocol ? int(protocol.Value()) : -1);
s.error(id, "GATT discovery failed"); return;
}
auto* l = s.link(id);
for (auto const& ch : result.Characteristics()) {
DEBUG_DEBUG(CAT, "BLE WinRT characteristic link=%llu uuid=%s properties=0x%x", (unsigned long long)id,
to_string(to_hstring(ch.Uuid())).c_str(), (unsigned)ch.CharacteristicProperties());
if (ch.Uuid() == guid(BLE_RX_UUID)) l->rx = ch;
if (ch.Uuid() == guid(BLE_TX_UUID)) l->tx = ch;
if (ch.Uuid() == guid(BLE_INVITE_UUID)) l->invite = ch;
if (ch.Uuid() == guid(BLE_ACK_UUID)) l->ack = ch;
}
if (!l->rx || !l->tx || !l->invite || !l->ack) { s.error(id, "RX/TX/INVITE/ACK characteristics missing"); return; }
s.subscribe(id);
});
});
});
}
void subscribe(ble_link_id id) {
auto* l = link(id);
await("open GATT session", GattSession::FromDeviceIdAsync(l->device.BluetoothDeviceId()), id, [id](WinState& s, GattSession const& session) {
auto* l = s.link(id);
if (!session) { s.error(id, "GATT session unavailable"); return; }
l->session = session; session.MaintainConnection(true); s.watch_session(*l);
s.prefer_speed(*l);
DEBUG_DEBUG(CAT, "BLE WinRT session opened link=%llu mtu=%u maintain_connection=1", (unsigned long long)id, session.MaxPduSize());
s.log_connection(id);
l->value_changed = s.watch_value(id, l->tx, BLE_WIRE_DATA);
l->invite_changed = s.watch_value(id, l->invite, BLE_WIRE_INVITE);
l->ack_changed = s.watch_value(id, l->ack, BLE_WIRE_ACK);
s.subscribe_channel(id, BLE_WIRE_DATA);
});
}
GattCharacteristic::ValueChanged_revoker watch_value(ble_link_id id, GattCharacteristic const& characteristic, enum ble_wire_channel channel) {
auto weak = weak_from_this();
step("GattCharacteristic.ValueChanged registration (auto_revoke)", id);
DEBUG_DEBUG(CAT, "BLE WinRT register ValueChanged link=%llu channel=%d", (unsigned long long)id, channel);
return characteristic.ValueChanged(auto_revoke, [weak, id, channel](auto const&, GattValueChangedEventArgs const& args) {
if (auto s = weak.lock()) s->post("GattCharacteristic.ValueChanged", id, [id, channel, args](WinState& s) {
if (!s.link(id)) return;
s.step("ValueChanged.CharacteristicValue/DataReader", id);
auto data = bytes(args.CharacteristicValue());
DEBUG_TRACE(CAT, "BLE WinRT RX value link=%llu channel=%d bytes=%zu", (unsigned long long)id, channel, data.size());
ble_backend_received(s.owner, id, channel, data.data(), data.size());
});
});
}
void subscribe_channel(ble_link_id id, enum ble_wire_channel channel) {
auto* l = link(id);
auto characteristic = channel == BLE_WIRE_ACK ? l->ack : channel == BLE_WIRE_DATA ? l->tx : l->invite;
auto mode = channel == BLE_WIRE_DATA ? GattClientCharacteristicConfigurationDescriptorValue::Notify :
GattClientCharacteristicConfigurationDescriptorValue::Indicate;
step("GattCharacteristic.WriteCCCDWithResultAsync", id);
DEBUG_DEBUG(CAT, "BLE WinRT subscribe request link=%llu channel=%d mode=%d", (unsigned long long)id, channel, (int)mode);
await("subscribe channel", characteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(mode), id,
[id, channel](WinState& s, GattWriteResult const& result) {
auto status = result.Status(); auto protocol = result.ProtocolError();
DEBUG_DEBUG(CAT, "BLE WinRT subscribe link=%llu channel=%d status=%d", (unsigned long long)id, channel, (int)status);
if (status != GattCommunicationStatus::Success) {
DEBUG_WARN(CAT, "BLE WinRT subscription failed link=%llu channel=%d status=%d ATT_error=%d",
(unsigned long long)id, channel, (int)status, protocol ? int(protocol.Value()) : -1);
s.error(id, "GATT subscription failed"); return;
}
if (channel == BLE_WIRE_DATA) { s.subscribe_channel(id, BLE_WIRE_INVITE); return; }
if (channel == BLE_WIRE_INVITE) { s.subscribe_channel(id, BLE_WIRE_ACK); return; }
auto* l = s.link(id); l->opened = true; ble_backend_opened(s.owner, id, l->session.MaxPduSize());
});
}
void close(ble_link_id id) {
auto it = links.find(id); if (it == links.end()) return;
auto l = std::move(it->second); links.erase(it);
DEBUG_DEBUG(CAT, "BLE WinRT close link=%llu outgoing=%d opened=%d sending=%d pending=%d", (unsigned long long)id,
l->outgoing, l->opened, l->sending, bool(l->operation));
l->value_changed.revoke(); l->invite_changed.revoke(); l->ack_changed.revoke(); l->session_changed.revoke(); l->pdu_changed.revoke();
l->parameters_changed.revoke(); l->phy_changed.revoke();
try { if (l->speed_request) l->speed_request.Close(); }
catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("close throughput request", id).c_str()); }
try {
if (l->operation) l->operation.Cancel();
if (l->outgoing) {
if (l->session) { l->session.MaintainConnection(false); l->session.Close(); }
if (l->service) l->service.Close();
if (l->device) l->device.Close();
}
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("close", id).c_str()); }
}
void shutdown() {
struct posted_task* task;
std::deque<Event> discarded;
{ std::lock_guard guard(mutex); stopped = true; task = posted; posted = nullptr; discarded.swap(events); }
DEBUG_DEBUG(CAT, "BLE WinRT shutdown links=%zu discarded_events=%zu pending_dispatch=%d", links.size(), discarded.size(), task != nullptr);
discarded.clear();
if (task) uasync_cancel_post(ua, task);
received.revoke(); scan_stopped.revoke(); publish_changed.revoke(); service_changed.revoke();
write_requested.revoke(); subscriptions_changed.revoke(); invite_write_requested.revoke(); invite_subscriptions_changed.revoke();
ack_write_requested.revoke(); ack_subscriptions_changed.revoke();
while (!links.empty()) close(links.begin()->first);
try { if (startup) startup.Cancel(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("cancel startup").c_str()); }
try { if (watcher) watcher.Stop(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop scan").c_str()); }
try { if (publisher) publisher.Stop(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop publisher").c_str()); }
try { if (provider) provider.StopAdvertising(); } catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("stop server").c_str()); }
watcher = nullptr; publisher = nullptr; provider = nullptr;
local_rx = nullptr; local_tx = nullptr; local_invite = nullptr; local_ack = nullptr; startup = nullptr;
if (apartment) { uninit_apartment(); apartment = false; }
}
};
struct ble_backend { std::shared_ptr<WinState> state; };
extern "C" struct ble_backend* ble_backend_create(struct ble_transport* owner, struct UASYNC* ua, const struct ble_config* cfg) {
if (cfg->adapter) { DEBUG_ERROR(CAT, "BLE WinRT explicit adapter selection unsupported"); return nullptr; }
auto state = std::make_shared<WinState>(); state->owner = owner; state->ua = ua; state->company = cfg->company_id;
try {
DEBUG_DEBUG(CAT, "BLE WinRT initialize company=%04x node=%016llx apartment=MTA", cfg->company_id, (unsigned long long)cfg->node_id);
init_apartment(apartment_type::multi_threaded); state->apartment = true;
state->step("ApiInformation.IsPropertyPresent(BluetoothAddressType)");
bool address_type = Windows::Foundation::Metadata::ApiInformation::IsPropertyPresent(
L"Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs", L"BluetoothAddressType");
DEBUG_INFO(CAT, "BLE WinRT API BluetoothAddressType=%d required=Windows10-2004 wire=3 channels=DATA/INVITE/ACK", address_type);
if (!address_type) throw hresult_error(E_NOINTERFACE, L"BLE requires BluetoothAddressType (Windows 10 2004 / build 19041 or newer)");
try {
auto version = Windows::System::Profile::AnalyticsInfo::VersionInfo();
DEBUG_INFO(CAT, "BLE WinRT OS family=%s version=%s", to_string(version.DeviceFamily()).c_str(),
to_string(version.DeviceFamilyVersion()).c_str());
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("OS version diagnostics").c_str()); }
state->await("get default adapter", BluetoothAdapter::GetDefaultAsync(), 0, [](WinState& s, BluetoothAdapter const& adapter) {
if (!adapter) { s.fail("no default Bluetooth adapter"); return; }
DEBUG_DEBUG(CAT, "BLE WinRT adapter id=%s address=%012llx BLE=%d central=%d peripheral=%d", to_string(adapter.DeviceId()).c_str(),
(unsigned long long)adapter.BluetoothAddress(), adapter.IsLowEnergySupported(),
adapter.IsCentralRoleSupported(), adapter.IsPeripheralRoleSupported());
try {
auto advertising = adapter.try_as<IBluetoothAdapter3>();
auto phy = adapter.try_as<IBluetoothAdapter4>();
DEBUG_DEBUG(CAT, "BLE WinRT adapter extended_advertising=%d advertisement_max=%d PHY_2M_supported=%d PHY_coded_supported=%d (-1=API unavailable)",
advertising ? int(advertising.IsExtendedAdvertisingSupported()) : -1,
advertising ? int(advertising.MaxAdvertisementDataLength()) : -1,
phy ? int(phy.IsLowEnergyUncoded2MPhySupported()) : -1, phy ? int(phy.IsLowEnergyCodedPhySupported()) : -1);
} catch (...) { DEBUG_WARN(CAT, "%s", exception_reason("optional adapter capabilities").c_str()); }
if (!adapter.IsLowEnergySupported() || !adapter.IsCentralRoleSupported() || !adapter.IsPeripheralRoleSupported()) {
s.fail("adapter lacks BLE central/peripheral support"); return;
}
s.make_server();
});
void* memory = u_malloc(sizeof(ble_backend));
if (!memory) { DEBUG_ERROR(CAT, "BLE WinRT allocation failed"); state->shutdown(); return nullptr; }
return new(memory) ble_backend{state};
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason(state->operation).c_str()); state->shutdown(); return nullptr; }
}
extern "C" void ble_backend_destroy(struct ble_backend* b) {
if (!b) return;
b->state->shutdown(); b->~ble_backend(); u_free(b);
}
extern "C" int ble_backend_advertise(struct ble_backend* b, const uint8_t* data, size_t len) {
try {
auto& s = *b->state; s.broadcast_enabled = data != nullptr; s.broadcast_size = len;
if (len) std::memcpy(s.broadcast.data(), data, len);
s.publish(); return BLE_OK;
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason(b->state->operation).c_str()); return BLE_ERROR; }
}
extern "C" int ble_backend_connect(struct ble_backend* b, ble_link_id id, const struct ble_endpoint* ep) {
unsigned long long address; int type, consumed = 0;
if (std::sscanf(ep->id, "%llx/%d%n", &address, &type, &consumed) != 2 || ep->id[consumed] || address >> 48 || type < 0 || type > 2) {
DEBUG_ERROR(CAT, "BLE invalid WinRT endpoint=%s", ep->id); return BLE_ERROR;
}
try {
auto& s = *b->state; auto l = std::make_unique<WinLink>(); l->id = id; l->endpoint = ep->id; l->outgoing = true;
s.links.emplace(id, std::move(l)); s.connect(id, address, static_cast<BluetoothAddressType>(type)); return BLE_OK;
} catch (...) { DEBUG_ERROR(CAT, "%s", exception_reason("connect", id).c_str()); b->state->close(id); return BLE_ERROR; }
}
extern "C" void ble_backend_close(struct ble_backend* b, ble_link_id id) { b->state->close(id); }
extern "C" int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len) {
auto& s = *b->state; auto* l = s.link(id);
if ((unsigned)channel >= BLE_WIRE_COUNT || !l || !l->opened || l->sending) {
DEBUG_ERROR(CAT, "BLE invalid WinRT send link=%llu exists=%d opened=%d sending=%d bytes=%zu", (unsigned long long)id,
l != nullptr, l && l->opened, l && l->sending, len);
return BLE_ERROR;
}
try {
DEBUG_TRACE(CAT, "BLE WinRT TX fragment link=%llu outgoing=%d channel=%d bytes=%zu",
(unsigned long long)id, l->outgoing, channel, len);
s.step("TX DataWriter buffer", id);
l->sending = true; auto buffer = WinState::buffer(data, len);
auto option = channel == BLE_WIRE_DATA ? GattWriteOption::WriteWithoutResponse : GattWriteOption::WriteWithResponse;
uint64_t started = get_time_us();
s.step(l->outgoing ? "GattCharacteristic.WriteValueWithResultAsync" : "GattLocalCharacteristic.NotifyValueAsync", id);
if (l->outgoing) s.await("write frame", (channel == BLE_WIRE_ACK ? l->ack : channel == BLE_WIRE_INVITE ? l->invite : l->rx)
.WriteValueWithResultAsync(buffer, option), id,
[id, len, channel, started](WinState& s, GattWriteResult const& result) {
auto* l = s.link(id); l->sending = false;
auto protocol = result.ProtocolError();
if (result.Status() != GattCommunicationStatus::Success)
DEBUG_WARN(CAT, "BLE WinRT write failed link=%llu channel=%d bytes=%zu status=%d ATT_error=%d", (unsigned long long)id,
channel, len, (int)result.Status(), protocol ? int(protocol.Value()) : -1);
else DEBUG_TRACE(CAT, "BLE WinRT write complete link=%llu channel=%d bytes=%zu elapsed_us=%llu",
(unsigned long long)id, channel, len, (unsigned long long)(get_time_us() - started));
ble_backend_sent(s.owner, id, result.Status() == GattCommunicationStatus::Success);
});
else s.await("notify frame", (channel == BLE_WIRE_ACK ? s.local_ack : channel == BLE_WIRE_INVITE ? s.local_invite : s.local_tx)
.NotifyValueAsync(buffer, channel == BLE_WIRE_ACK ? l->ack_subscriber :
channel == BLE_WIRE_INVITE ? l->invite_subscriber : l->subscriber), id,
[id, len, channel, started](WinState& s, GattClientNotificationResult const& result) {
auto* l = s.link(id); l->sending = false;
s.step("GattClientNotificationResult.Status/BytesSent", id);
if (result.Status() != GattCommunicationStatus::Success || result.BytesSent() != len)
DEBUG_WARN(CAT, "BLE WinRT notification failed link=%llu channel=%d expected=%zu sent=%u status=%d", (unsigned long long)id,
channel, len, result.BytesSent(), (int)result.Status());
else DEBUG_TRACE(CAT, "BLE WinRT notification complete link=%llu channel=%d bytes=%zu elapsed_us=%llu",
(unsigned long long)id, channel, len, (unsigned long long)(get_time_us() - started));
ble_backend_sent(s.owner, id, result.Status() == GattCommunicationStatus::Success && result.BytesSent() == len);
});
return BLE_OK;
} catch (...) { l->sending = false; DEBUG_ERROR(CAT, "%s", exception_reason(s.operation, id).c_str()); return BLE_ERROR; }
}
+103
View File
@@ -0,0 +1,103 @@
/* Реальное Android↔desktop радио. Android instrumentation включает только тестовый echo.
* ble_peer_probe [connect|listen] [seconds] [peer_hex]; 96 пакетов, TX saturation, побайтовая сверка. */
#include "ble_transport.h"
#include "ble_identity.h"
#include "u_async.h"
#include "debug_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static struct ble_transport* transport;
static ble_link_id session;
static unsigned sent, received_count, blocked;
static int failed, dial, seen, radio_ready;
static uint64_t peer, expected_peer, started, verified_bytes, retry;
static const unsigned lengths[] = {1,13,14,20,244,512,1499,1500};
static unsigned length(unsigned n) { return lengths[n % 8]; }
static uint8_t probe_byte(unsigned n, unsigned i) { return (uint8_t)(n + i * 37); }
static void send_more(void* arg, ble_link_id id) {
(void)arg;
while (sent < 96) {
struct ll_entry* e = ll_alloc_lldgram(length(sent));
if (!e) { failed = 1; return; }
e->len = length(sent);
for (unsigned i = 0; i < e->len; i++) e->dgram[i] = probe_byte(sent, i);
int result = ble_tx_put(transport, id, e);
if (result != BLE_OK) {
queue_dgram_free(e); queue_entry_free(e);
if (result == BLE_WOULD_BLOCK) blocked++; else failed = 1;
return;
}
sent++;
}
}
static void received(void* arg, ble_link_id id) {
(void)arg; struct ll_entry* e;
while ((e = ble_rx_get(transport, id))) {
unsigned n = received_count;
if (n >= 96 || e->len != length(n)) failed = 1;
for (unsigned i = 0; i < e->len; i++) if (e->dgram[i] != probe_byte(n, i)) failed = 1;
printf("VERIFY packet=%u bytes=%u ok=%d\n", n, e->len, !failed); fflush(stdout);
verified_bytes += e->len; received_count++; queue_dgram_free(e); queue_entry_free(e);
}
}
static void connected(void* arg, ble_link_id id, uint64_t node, enum ble_state state, const char* reason) {
(void)arg;
printf("CONNECTION link=%llu peer=%016llx state=%d reason=%s\n", (unsigned long long)id, (unsigned long long)node, state, reason);
if (state == BLE_READY) { session = id; peer = node; started = get_time_us(); send_more(NULL, id); }
if (state == BLE_CLOSED && received_count < 96) {
if (started) failed = 1;
else {
session = 0; retry = get_time_us() + 1000000;
DEBUG_INFO(DEBUG_CATEGORY_DISCOVERY, "BLE probe closed before transfer; retrying discovery");
}
}
}
static void status(void* arg, enum ble_state state, const char* reason) {
(void)arg; printf("STATUS state=%d reason=%s\n", state, reason);
radio_ready = state == BLE_READY;
if (state == BLE_READY) {
uint8_t data[24]; int n = ble_identity_encode(data, 0x5643424c45544553ULL, "Desktop-test");
if (ble_advertise(transport, data, n) != BLE_OK) failed = 1;
}
}
static void broadcast(void* arg, const struct ble_endpoint* ep, const uint8_t* data, size_t len, int rssi) {
(void)arg; struct ble_identity identity;
if (ble_identity_decode(&identity, data, len) < 0) return;
if (expected_peer && identity.node_id != expected_peer) return;
if (!seen) printf("DISCOVER node=%016llx name=%s rssi=%d endpoint=%s\n",
(unsigned long long)identity.node_id, identity.name, rssi, ep->id);
seen = 1;
if (dial && radio_ready && !session && get_time_us() >= retry) {
session = ble_connect(transport, ep, identity.node_id);
if (!session) { DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "BLE probe connect refused; retrying discovery"); retry = get_time_us() + 1000000; }
}
}
int main(int argc, char** argv) {
dial = argc < 2 || !strcmp(argv[1], "connect");
int seconds = argc > 2 ? atoi(argv[2]) : 90;
if (argc > 3) {
char* end; errno = 0; expected_peer = strtoull(argv[3], &end, 16);
if (errno || !expected_peer || !argv[3][0] || *end) { fprintf(stderr, "Invalid peer_hex\n"); return 2; }
}
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_DISCOVERY, DEBUG_LEVEL_DEBUG);
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_TRACE);
struct UASYNC* ua = uasync_create(); struct ble_config cfg = {0};
cfg.node_id = 0x5643424c45544553ULL; cfg.company_id = 0xffff;
cfg.callbacks.status = status; cfg.callbacks.broadcast = broadcast; cfg.callbacks.connection = connected;
cfg.callbacks.received = received; cfg.callbacks.writable = send_more;
transport = ble_create(ua, &cfg);
if (!transport || ble_start(transport) != BLE_OK) failed = 1;
uint64_t end = get_time_us() + (uint64_t)seconds * 1000000;
while (!failed && received_count < 96 && get_time_us() < end) uasync_poll(ua, 100);
int ok = !failed && received_count == 96 && seen && blocked;
printf("RESULT ok=%d sent=%u verified=%u blocked=%u discovered=%d peer=%016llx\n",
ok, sent, received_count, blocked, seen, (unsigned long long)peer);
if (started) {
double seconds = (get_time_us() - started) / 1000000.0;
printf("THROUGHPUT verified_bytes_each_direction=%llu seconds=%.3f aggregate_bytes_per_second=%.0f\n",
(unsigned long long)verified_bytes, seconds, seconds > 0 ? verified_bytes * 2 / seconds : 0);
}
ble_destroy(transport); uasync_destroy(ua, 0); return !ok;
}
+49
View File
@@ -0,0 +1,49 @@
/* Реальное радио, запуск вручную: ble_probe [seconds]. Публикует только тестовое объявление. */
#include "ble_transport.h"
#include "u_async.h"
#include "debug_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int ready, failed, published, changed, republished;
static enum ble_state advertising = BLE_STARTING;
static struct ble_transport* transport;
static void status(void* arg, enum ble_state state, const char* reason) {
(void)arg; DEBUG_INFO(DEBUG_CATEGORY_DISCOVERY, "BLE probe status=%d reason=%s", state, reason);
if (state == BLE_READY) { ready = 1; if (ble_advertise(transport, "vibechat-probe", 14) != BLE_OK) failed = 1; }
if (state == BLE_CLOSED && strcmp(reason, "stopped")) failed = 1;
}
static void advertising_status(void* arg, enum ble_state state, const char* reason) {
(void)arg; advertising = state;
DEBUG_INFO(DEBUG_CATEGORY_DISCOVERY, "BLE probe advertising=%d updated=%d reason=%s", state, changed, reason);
if (state == BLE_READY) { if (changed) republished = 1; else published = 1; }
}
static void broadcast(void* arg, const struct ble_endpoint* ep, const uint8_t* data, size_t len, int rssi) {
(void)arg; (void)data;
DEBUG_DEBUG(DEBUG_CATEGORY_DISCOVERY, "BLE probe broadcast endpoint=%s bytes=%zu rssi=%d", ep->id, len, rssi);
}
int main(int argc, char** argv) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_DISCOVERY, DEBUG_LEVEL_DEBUG);
int seconds = argc > 1 ? atoi(argv[1]) : 5;
if (seconds < 1 || seconds > 60) { DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "BLE probe seconds must be 1..60"); return 2; }
struct UASYNC* ua = uasync_create();
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "BLE probe cannot create uasync"); return 1; }
struct ble_config cfg = {0}; cfg.node_id = 0x5643424c45544553ULL; cfg.company_id = 0xffff;
cfg.callbacks.status = status; cfg.callbacks.broadcast = broadcast;
cfg.callbacks.advertising = advertising_status;
transport = ble_create(ua, &cfg);
if (!transport || ble_start(transport) != BLE_OK) failed = 1;
uint64_t start = get_time_us(), end = start + (uint64_t)seconds * 1000000;
while (!failed && get_time_us() < end) {
uasync_poll(ua, 100);
if (published && !changed && get_time_us() >= start + 1000000) {
changed = 1; if (ble_advertise(transport, "vibechat-updated", 16) != BLE_OK) failed = 1;
}
}
int ok = ready && published && republished && advertising == BLE_READY && !failed;
if (ok) DEBUG_INFO(DEBUG_CATEGORY_DISCOVERY, "BLE probe PASS: radio ready, advertisement published and updated");
else DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "BLE probe FAIL: ready=%d published=%d updated=%d advertising=%d failed=%d seconds=%d",
ready, published, republished, advertising, failed, seconds);
ble_destroy(transport); uasync_destroy(ua, 0);
return !ok;
}
+245
View File
@@ -0,0 +1,245 @@
/* Private D-Bus + настоящий backend: DATA без ATT ACK, адресные INVITE/ACK indications, два клиента. */
#include "ble_backend.h"
#include "u_async.h"
#include "mem.h"
#include "debug_config.h"
#include <systemd/sd-bus.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define CHECK(v) do { if (!(v)) { fprintf(stderr, "FAIL line=%d: %s errno=%d\n", __LINE__, #v, errno); exit(1); } } while (0)
#define ROOT "/org/vibechat/ble"
#define ADAPTER "/org/bluez/hci0"
#define REMOTE ADAPTER "/dev_C"
static struct UASYNC* ua;
static sd_bus* bus;
static struct ble_transport* transport;
static char application[256];
static int ready, broadcasts, ready_links, advertisement_count;
static ble_link_id links[4];
static unsigned commands, requests, ack_requests, ack_values;
static int ack_fds[2] = {-1, -1};
static int remote_resolved;
struct reply { sd_bus_slot* slot; int done, fd, failed; };
static void poll_all(void) {
for (int i = 0; i < 500; i++) {
CHECK(sd_bus_process(bus, NULL) >= 0); uasync_poll(ua, 10);
for (int j = 0; j < 2; j++) if (ack_fds[j] >= 0) {
uint8_t ack[8]; ssize_t n = recv(ack_fds[j], ack, sizeof(ack), MSG_DONTWAIT);
if (n < 0) { CHECK(errno == EAGAIN); continue; }
CHECK(n == 2); ack_values++; CHECK(send(ack_fds[j], "\1", 1, MSG_NOSIGNAL) == 1);
}
}
}
static int register_app(sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)arg; (void)e; snprintf(application, sizeof(application), "%s", sd_bus_message_get_sender(m));
return sd_bus_reply_method_return(m, "");
}
static int simple(sd_bus_message* m, void* arg, sd_bus_error* e) { (void)arg; (void)e; return sd_bus_reply_method_return(m, ""); }
static int advertise(sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)arg; (void)e; advertisement_count++;
return sd_bus_reply_method_return(m, "");
}
static int property(sd_bus* b, const char* p, const char* i, const char* name, sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)b; (void)p; (void)i; (void)name; (void)arg; (void)e; return sd_bus_message_append(m, "b", 1);
}
static const sd_bus_vtable adapter_vtable[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("Powered", "b", property, 0, 0),
SD_BUS_METHOD("SetDiscoveryFilter", "a{sv}", "", simple, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("StartDiscovery", "", "", simple, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable gatt_vtable[] = {
SD_BUS_VTABLE_START(0), SD_BUS_METHOD("RegisterApplication", "oa{sv}", "", register_app, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static const sd_bus_vtable adv_vtable[] = {
SD_BUS_VTABLE_START(0), SD_BUS_METHOD("RegisterAdvertisement", "oa{sv}", "", advertise, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("UnregisterAdvertisement", "o", "", simple, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static int remote_property(sd_bus* b, const char* path, const char* i, const char* name,
sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)b; (void)i; (void)arg; (void)e;
if (!strcmp(name, "MTU")) return sd_bus_message_append(m, "q", remote_resolved ? 517 : 23);
const char* uuid = strstr(path, "/rx") ? BLE_RX_UUID : strstr(path, "/tx") ? BLE_TX_UUID :
strstr(path, "/ack") ? BLE_ACK_UUID : BLE_INVITE_UUID;
return sd_bus_message_append(m, "s", uuid);
}
static int remote_write(sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)arg; (void)e;
const void* data; size_t len; const char *key, *type;
CHECK(sd_bus_message_read_array(m, 'y', &data, &len) > 0 && len > 0);
CHECK(sd_bus_message_enter_container(m, 'a', "{sv}") > 0);
CHECK(sd_bus_message_enter_container(m, 'e', "sv") > 0);
CHECK(sd_bus_message_read(m, "s", &key) > 0 && !strcmp(key, "type"));
CHECK(sd_bus_message_enter_container(m, 'v', "s") > 0);
CHECK(sd_bus_message_read(m, "s", &type) > 0);
int invitation = strstr(sd_bus_message_get_path(m), "/invite") != NULL;
int ack = strstr(sd_bus_message_get_path(m), "/ack") != NULL;
CHECK(!strcmp(type, invitation || ack ? "request" : "command"));
if (ack) { CHECK(len == 2); ack_requests++; } else if (invitation) requests++; else commands++;
int r = sd_bus_reply_method_return(m, "");
if (!invitation && !ack && len == 19 && ((const uint8_t*)data)[2] == 1) {
sd_bus_message* signal = NULL; uint8_t accept[19] = {0, 1, 2}; accept[10] = 3; accept[18] = 200;
CHECK(sd_bus_message_new_signal(bus, &signal, REMOTE "/tx", "org.freedesktop.DBus.Properties", "PropertiesChanged") >= 0);
CHECK(sd_bus_message_append(signal, "sa{sv}as", "org.bluez.GattCharacteristic1", 1, "Value", "ay", 19,
0,1,2,0,0,0,0,0,0,0,accept[10],0,0,0,0,0,0,0,accept[18], 0) >= 0);
CHECK(sd_bus_send(bus, signal, NULL) >= 0); sd_bus_message_unref(signal);
}
return r;
}
static int remote_state(sd_bus* b, const char* path, const char* i, const char* name,
sd_bus_message* m, void* arg, sd_bus_error* e) {
(void)b; (void)path; (void)i; (void)arg; (void)e;
if (!strcmp(name, "ManufacturerData")) return sd_bus_message_append(m, "a{qv}", 1, 0xffff, "ay", 3, 'a', 'b', 'c');
return sd_bus_message_append(m, "b", !strcmp(name, "Connected") || remote_resolved);
}
static const sd_bus_vtable device_vtable[] = {
SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Connect", "", "", simple, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_PROPERTY("Connected", "b", remote_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("ServicesResolved", "b", remote_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("ManufacturerData", "a{qv}", remote_state, 0, 0),
SD_BUS_VTABLE_END
};
static const sd_bus_vtable remote_vtable[] = {
SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("UUID", "s", remote_property, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("MTU", "q", remote_property, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_METHOD("StartNotify", "", "", simple, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("StopNotify", "", "", simple, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("WriteValue", "aya{sv}", "", remote_write, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END
};
static void status(void* arg, enum ble_state state, const char* reason) {
(void)arg; (void)reason; if (state == BLE_READY) ready++;
}
static void connection(void* arg, ble_link_id id, uint64_t node, enum ble_state state, const char* reason) {
(void)arg; (void)reason;
if (state == BLE_READY) { CHECK(node < 4); links[node] = id; ready_links++; }
}
static void broadcast(void* arg, const struct ble_endpoint* ep, const uint8_t* data, size_t len, int rssi) {
(void)arg; CHECK(strstr(ep->id, "dev_A")); CHECK(len == 3 && !memcmp(data, "abc", 3)); CHECK(rssi == -42); broadcasts++;
}
static int replied(sd_bus_message* m, void* arg, sd_bus_error* error) {
(void)error; struct reply* reply = arg;
reply->failed = sd_bus_message_is_method_error(m, NULL);
if (reply->failed) { const sd_bus_error* e = sd_bus_message_get_error(m); fprintf(stderr, "reply error %s: %s\n", e->name, e->message); }
if (!reply->failed && !strcmp(sd_bus_message_get_signature(m, 1), "hq")) {
int fd; uint16_t mtu; CHECK(sd_bus_message_read(m, "hq", &fd, &mtu) > 0); CHECK(mtu == 23);
reply->fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); CHECK(reply->fd >= 0);
}
reply->done = 1; return 0;
}
static int acquire(const char* device, int channel) {
struct reply reply = {.fd=-1};
const char* path = channel == 2 ? ROOT "/service0/ack" : channel ? ROOT "/service0/invite" : ROOT "/service0/tx";
CHECK(sd_bus_call_method_async(bus, &reply.slot, application, path, "org.bluez.GattCharacteristic1",
"AcquireNotify", replied, &reply, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
poll_all(); CHECK(reply.done && !reply.failed && reply.fd >= 0); sd_bus_slot_unref(reply.slot); return reply.fd;
}
static void write_value(const char* device, const uint8_t* data, size_t len, int invitation) {
struct reply reply = {0}; sd_bus_message* m = NULL;
CHECK(sd_bus_message_new_method_call(bus, &m, application, invitation ? ROOT "/service0/invite" : ROOT "/service0/rx", "org.bluez.GattCharacteristic1", "WriteValue") >= 0);
CHECK(sd_bus_message_append_array(m, 'y', data, len) >= 0);
CHECK(sd_bus_message_append(m, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
CHECK(sd_bus_call_async(bus, &reply.slot, m, replied, &reply, 1000000) >= 0); sd_bus_message_unref(m);
poll_all(); CHECK(reply.done && !reply.failed); sd_bus_slot_unref(reply.slot);
}
static void announce(void) {
sd_bus_message* m = NULL;
CHECK(sd_bus_message_new_signal(bus, &m, ADAPTER "/dev_A", "org.freedesktop.DBus.Properties", "PropertiesChanged") >= 0);
CHECK(sd_bus_message_append(m, "sa{sv}as", "org.bluez.Device1", 2,
"ManufacturerData", "a{qv}", 1, 0xffff, "ay", 3, 'a', 'b', 'c', "RSSI", "n", -42, 0) >= 0);
CHECK(sd_bus_send(bus, m, NULL) >= 0); sd_bus_message_unref(m); poll_all(); CHECK(broadcasts == 1);
}
static void hello(const char* device, int fd, uint8_t node) {
uint8_t data[32] = {0, 1, 1}; data[10] = node; data[18] = 200;
write_value(device, data, 19, 0);
CHECK(recv(fd, data, sizeof(data), MSG_DONTWAIT) == 19); CHECK(data[2] == 2 && data[10] == 200 && data[18] == node);
poll_all(); /* ACCEPT отправляется Notification: подтверждения от клиента нет. */
}
static struct ll_entry* frame(int value) {
struct ll_entry* e = ll_alloc_lldgram(31); CHECK(e); e->len = 31; memset(e->dgram, value, e->len); return e;
}
int main(void) {
CHECK(getenv("DBUS_SESSION_BUS_ADDRESS")); CHECK(!setenv("DBUS_SYSTEM_BUS_ADDRESS", getenv("DBUS_SESSION_BUS_ADDRESS"), 1));
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
size_t before = u_get_allocated_count();
CHECK(sd_bus_open_user(&bus) >= 0); CHECK(sd_bus_request_name(bus, "org.bluez", 0) >= 0);
sd_bus_slot *manager = NULL, *adapter = NULL, *gatt = NULL, *adv = NULL, *remote[5] = {0};
CHECK(sd_bus_add_object_manager(bus, &manager, "/") >= 0);
CHECK(sd_bus_add_object_vtable(bus, &adapter, ADAPTER, "org.bluez.Adapter1", adapter_vtable, NULL) >= 0);
CHECK(sd_bus_add_object_vtable(bus, &gatt, ADAPTER, "org.bluez.GattManager1", gatt_vtable, NULL) >= 0);
CHECK(sd_bus_add_object_vtable(bus, &adv, ADAPTER, "org.bluez.LEAdvertisingManager1", adv_vtable, NULL) >= 0);
CHECK(sd_bus_add_object_vtable(bus, &remote[0], REMOTE, "org.bluez.Device1", device_vtable, NULL) >= 0);
const char* paths[] = {REMOTE "/rx", REMOTE "/tx", REMOTE "/invite", REMOTE "/ack"};
for (int i = 0; i < 4; i++)
CHECK(sd_bus_add_object_vtable(bus, &remote[i+1], paths[i], "org.bluez.GattCharacteristic1", remote_vtable, NULL) >= 0);
ua = uasync_create(); CHECK(ua);
struct ble_config config = {0}; config.node_id = 200; config.company_id = 0xffff;
config.callbacks.status = status; config.callbacks.connection = connection; config.callbacks.broadcast = broadcast;
transport = ble_create(ua, &config); CHECK(transport && !ble_start(transport)); poll_all(); CHECK(ready == 1 && broadcasts == 0);
CHECK(!ble_advertise(transport, "abc", 3)); poll_all(); CHECK(advertisement_count == 2);
CHECK(!ble_advertise(transport, "def", 3)); poll_all(); CHECK(advertisement_count == 3);
announce();
int fd1 = acquire(ADAPTER "/dev_A", 0), fd2 = acquire(ADAPTER "/dev_B", 0);
int invite1 = acquire(ADAPTER "/dev_A", 1), invite2 = acquire(ADAPTER "/dev_B", 1);
CHECK(ready_links == 0); /* Без обязательной ACK-подписки opened не наступает. */
ack_fds[0] = acquire(ADAPTER "/dev_A", 2); ack_fds[1] = acquire(ADAPTER "/dev_B", 2);
hello(ADAPTER "/dev_A", fd1, 1); hello(ADAPTER "/dev_B", fd2, 2); CHECK(ready_links == 2);
CHECK(!ble_tx_put(transport, links[1], frame(0xa1))); CHECK(!ble_tx_put(transport, links[2], frame(0xb2))); poll_all();
uint8_t data[64];
/* Все DATA-фрагменты обоим клиентам проходят без единого ATT ACK. */
for (int offset = 0; offset < 31; offset += 11) {
CHECK(recv(fd1, data, sizeof(data), MSG_DONTWAIT) == (offset == 22 ? 18 : 20));
CHECK(data[8] == offset && data[9] == 0xa1);
CHECK(recv(fd2, data, sizeof(data), MSG_DONTWAIT) == (offset == 22 ? 18 : 20));
CHECK(data[8] == offset && data[9] == 0xb2);
}
CHECK(recv(fd1, data, sizeof(data), MSG_DONTWAIT) < 0 && errno == EAGAIN);
CHECK(recv(fd2, data, sizeof(data), MSG_DONTWAIT) < 0 && errno == EAGAIN);
uint8_t incoming[] = {0, 2, 3, 0, 1, 0, 3, 0, 0, 0x11, 0x22, 0x33};
write_value(ADAPTER "/dev_B", incoming, sizeof(incoming), 0);
CHECK(!ble_rx_get(transport, links[1])); struct ll_entry* e = ble_rx_get(transport, links[2]); CHECK(e && e->len == 3);
CHECK(!memcmp(e->dgram, incoming + 9, 3)); queue_dgram_free(e); queue_entry_free(e);
write_value(ADAPTER "/dev_A", incoming + 2, sizeof(incoming) - 2, 1);
CHECK(!ble_rx_get(transport, links[1])); CHECK(!ble_channel_rx_get(transport, links[2], BLE_CHANNEL_INVITE));
e = ble_channel_rx_get(transport, links[1], BLE_CHANNEL_INVITE); CHECK(e && e->len == 3);
queue_dgram_free(e); queue_entry_free(e);
CHECK(!ble_channel_tx_put(transport, links[2], BLE_CHANNEL_INVITE, frame(0xc3))); poll_all();
CHECK(recv(invite1, data, sizeof(data), MSG_DONTWAIT) < 0 && errno == EAGAIN);
CHECK(recv(fd2, data, sizeof(data), MSG_DONTWAIT) < 0 && errno == EAGAIN);
for (int offset = 0; offset < 31; offset += 13) {
CHECK(recv(invite2, data, sizeof(data), MSG_DONTWAIT) == (offset == 26 ? 12 : 20));
CHECK(data[6] == offset && data[7] == 0xc3);
poll_all(); /* INVITE не продвигается без подтверждения; другой клиент свободен. */
CHECK(recv(invite2, data, sizeof(data), MSG_DONTWAIT) < 0 && errno == EAGAIN);
CHECK(!ble_tx_put(transport, links[1], frame(0xd4))); poll_all();
for (int part = 0; part < 31; part += 11) {
CHECK(recv(fd1, data, sizeof(data), MSG_DONTWAIT) == (part == 22 ? 18 : 20));
CHECK(data[8] == part && data[9] == 0xd4);
}
CHECK(send(invite2, "\1", 1, MSG_NOSIGNAL) == 1); poll_all();
}
CHECK(ack_values >= 2);
close(ack_fds[0]); close(ack_fds[1]); ack_fds[0] = ack_fds[1] = -1;
close(invite1); close(invite2);
close(fd1); poll_all(); close(fd2); poll_all();
/* Настоящий клиент backend: HELLO/DATA используют command, INVITE использует request. */
struct ble_endpoint ep = {REMOTE};
CHECK(ble_connect(transport, &ep, 3)); poll_all();
/* Кеш UUID доступен сразу, но до ServicesResolved нельзя объявлять GATT готовым с прежним MTU23. */
CHECK(ready_links == 2 && commands == 0);
remote_resolved = 1;
CHECK(sd_bus_emit_properties_changed(bus, REMOTE, "org.bluez.Device1", "ServicesResolved", NULL) >= 0);
poll_all(); CHECK(ready_links == 3 && commands == 1);
CHECK(!ble_tx_put(transport, links[3], frame(0xe5)));
CHECK(!ble_channel_tx_put(transport, links[3], BLE_CHANNEL_INVITE, frame(0xe6)));
poll_all(); CHECK(commands == 2 && requests == 1 && ack_requests >= 1); /* MTU517 и ACK Write Request. */
ble_destroy(transport); uasync_destroy(ua, 0);
for (int i = 0; i < 5; i++) sd_bus_slot_unref(remote[i]);
sd_bus_slot_unref(manager); sd_bus_slot_unref(adapter); sd_bus_slot_unref(gatt); sd_bus_slot_unref(adv); sd_bus_close_unref(bus);
CHECK(u_get_allocated_count() == before);
puts("BlueZ private-bus tests PASS (DATA commands, addressed INVITE/ACK indications, mandatory ACK, two clients, teardown)");
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
#include "ble_identity.h"
#include "debug_config.h"
#include <assert.h>
#include <string.h>
#include <stdio.h>
int main(void) {
debug_config_init();
uint8_t bytes[24]; struct ble_identity peer;
int n = ble_identity_encode(bytes, 0x0102030405060708ULL, "АБВГДЕЖ");
assert(n == 23 && bytes[0] == 'V' && bytes[1] == 'C' && bytes[2] == BLE_IDENTITY_VERSION && bytes[3] == 1 && bytes[10] == 8);
assert(ble_identity_decode(&peer, bytes, n) == 0);
assert(peer.node_id == 0x0102030405060708ULL && !strcmp(peer.name, "АБВГДЕ"));
assert(ble_identity_encode(bytes, 1, "123456789012😀") == 23);
assert(ble_identity_encode(bytes, 1, "1234567890123tail") == 24);
assert(ble_identity_decode(&peer, bytes, 24) == 0 && !strcmp(peer.name, "1234567890123"));
assert(ble_identity_decode(&peer, bytes, 25) < 0);
bytes[2] = 1; assert(ble_identity_decode(&peer, bytes, 24) < 0);
bytes[2] = 2; assert(ble_identity_decode(&peer, bytes, 24) < 0);
n = ble_identity_encode(bytes, 1, ""); assert(n == 11 && ble_identity_decode(&peer, bytes, n) == 0);
bytes[11] = 0xc0; bytes[12] = 0x80; assert(ble_identity_decode(&peer, bytes, 13) < 0);
assert(ble_identity_encode(bytes, 0, "name") < 0);
assert(ble_identity_encode(bytes, 1, "\xed\xa0\x80") < 0);
puts("BLE identity PASS"); return 0;
}
+272
View File
@@ -0,0 +1,272 @@
/* Два/три настоящих common transport с управляемым асинхронным GATT backend: проверяется весь lifecycle. */
#include "ble_backend.h"
#include "u_async.h"
#include "mem.h"
#include "debug_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(v) do { if (!(v)) { fprintf(stderr, "FAIL line=%d: %s\n", __LINE__, #v); exit(1); } } while (0)
struct fake_link {
ble_link_id id, other; struct ble_backend* peer; uint8_t data[512]; size_t len; int pending;
enum ble_wire_channel channel; uint64_t ack_at, complete_at; uint16_t ack_id, ack_delivered; int ack_held, delivered;
};
struct ble_backend { struct ble_transport* owner; struct UASYNC* ua; void* task; int index, started; struct fake_link links[16]; };
static struct ble_backend* backends[3];
static unsigned int mtu = 23;
static int hold, drop, reject_peer, ready_count, writable_count;
static int hold_ack;
static unsigned ack_delay_us;
static unsigned ack_timeouts;
static unsigned closed_count;
static uint64_t data_wire_bytes[3];
static ble_link_id incoming_ids[3];
static void run(void* arg);
static void schedule(struct ble_backend* b) { if (!b->task) b->task = uasync_call_soon(b->ua, b, run); CHECK(b->task); }
struct ble_backend* ble_backend_create(struct ble_transport* owner, struct UASYNC* ua, const struct ble_config* cfg) {
struct ble_backend* b = u_calloc(1, sizeof(*b)); CHECK(b); b->owner = owner; b->ua = ua; b->index = (int)cfg->node_id - 1;
backends[b->index] = b; schedule(b); return b;
}
void ble_backend_destroy(struct ble_backend* b) {
if (b->task) uasync_call_soon_cancel(b->ua, b->task);
backends[b->index] = NULL; u_free(b);
}
int ble_backend_advertise(struct ble_backend* b, const uint8_t* data, size_t len) { (void)b; (void)data; (void)len; return 0; }
static struct fake_link* slot(struct ble_backend* b, ble_link_id id) {
for (int i = 0; i < 16; i++) if (b->links[i].id == id) return &b->links[i];
return NULL;
}
int ble_backend_connect(struct ble_backend* b, ble_link_id id, const struct ble_endpoint* ep) {
struct ble_backend* p = backends[atoi(ep->id)]; CHECK(p);
struct fake_link *a = slot(b, 0), *c = slot(p, 0); CHECK(a && c);
struct ble_endpoint remote = {0}; snprintf(remote.id, sizeof(remote.id), "%d", b->index);
ble_link_id other = ble_backend_incoming(p->owner, &remote); CHECK(other);
*a = (struct fake_link){.id=id, .other=other, .peer=p};
*c = (struct fake_link){.id=other, .other=id, .peer=b};
incoming_ids[p->index] = other;
schedule(b); schedule(p); return 0;
}
void ble_backend_close(struct ble_backend* b, ble_link_id id) {
struct fake_link* f = slot(b, id); if (f) memset(f, 0, sizeof(*f));
}
int ble_backend_send(struct ble_backend* b, ble_link_id id, enum ble_wire_channel channel, const uint8_t* data, size_t len) {
struct fake_link* f = slot(b, id); CHECK(f && !f->pending && len <= sizeof(f->data));
CHECK(len <= mtu - 3);
if (channel == BLE_WIRE_DATA) data_wire_bytes[b->index] += len;
if (channel == BLE_WIRE_ACK) {
CHECK(len == 2);
uint64_t now = get_time_us();
CHECK(!f->ack_at || now - f->ack_at >= BLE_ACK_INTERVAL_US);
f->ack_at = now; f->ack_id = (uint16_t)((data[0] << 8) | data[1]);
}
f->channel = channel; memcpy(f->data, data, len); f->len = len; f->pending = 1; f->delivered = 0;
f->complete_at = channel == BLE_WIRE_ACK ? get_time_us() + ack_delay_us : 0;
schedule(b); return 0;
}
static void run(void* arg) {
struct ble_backend* b = arg; b->task = NULL;
if (!b->started) { b->started = 1; ble_backend_status(b->owner, 1, "fake ready"); }
for (int i = 0; i < 16; i++) {
struct fake_link* f = &b->links[i]; if (!f->id) continue;
if (!hold_ack && f->ack_held) {
uint8_t ack[2] = {f->ack_id >> 8, f->ack_id}; f->ack_held = 0; f->ack_delivered = f->ack_id;
if (f->peer && slot(f->peer, f->other)) ble_backend_received(f->peer->owner, f->other, BLE_WIRE_ACK, ack, 2);
}
if (!f->len) { f->len = 1; ble_backend_opened(b->owner, f->id, mtu); }
if (!f->pending || hold) continue;
struct fake_link* target = f->peer ? slot(f->peer, f->other) : NULL;
if (target && !target->len) { schedule(b); continue; } /* Удалённая подписка должна завершиться до доставки HELLO. */
ble_link_id id = f->id, other = f->other;
struct ble_backend* peer = f->peer;
if (!f->delivered) {
uint8_t data[512]; size_t len = f->len; memcpy(data, f->data, len); f->delivered = 1;
int dropped = f->channel != BLE_WIRE_ACK && data[f->channel == BLE_WIRE_DATA ? 2 : 0] == 3 && drop && --drop == 0;
if (hold_ack && f->channel == BLE_WIRE_ACK) f->ack_held = 1;
else if (!dropped && peer && slot(peer, other)) {
if (f->channel == BLE_WIRE_ACK) f->ack_delivered = f->ack_id;
ble_backend_received(peer->owner, other, f->channel, data, len);
}
}
if (slot(b, id)) {
if (get_time_us() < f->complete_at) { schedule(b); continue; }
f->pending = 0; ble_backend_sent(b->owner, id, 1);
}
}
}
static int validate(void* arg, ble_link_id id, uint64_t node) { (void)arg; (void)id; (void)node; return !reject_peer; }
static void connection(void* arg, ble_link_id id, uint64_t node, enum ble_state s, const char* reason) {
(void)arg; (void)id; (void)node; if (s == BLE_READY) ready_count++;
if (s == BLE_CLOSED) closed_count++;
if (s == BLE_CLOSED && !strcmp(reason, "ACK progress timeout")) ack_timeouts++;
}
static void writable(void* arg, ble_link_id id) { (void)arg; (void)id; writable_count++; }
static void drain(struct UASYNC* ua) {
uint64_t end = get_time_us() + 2 * BLE_ACK_INTERVAL_US;
do { uasync_poll(ua, 10); } while (get_time_us() < end);
}
static struct ll_entry* frame(unsigned int size, uint8_t value) {
struct ll_entry* e = ll_alloc_lldgram(size); CHECK(e); e->len = size; memset(e->dgram, value, size); return e;
}
static void release(struct ll_entry* e) { queue_dgram_free(e); queue_entry_free(e); }
/* Непрерывный RX при ACK completion >15ms не должен вытеснять встречный DATA. */
static void exercise_ack_fairness(void) {
mtu = 517; hold = drop = reject_peer = hold_ack = 0; ack_delay_us = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_config cfg = {.node_id = 1}; struct ble_transport* a = ble_create(ua, &cfg);
cfg.node_id = 2; struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
ble_link_id other = incoming_ids[1]; ack_delay_us = 20000;
int queued = 0, replied = 0; unsigned received = 0;
uint64_t started = get_time_us();
while (!replied && get_time_us() - started < 250000) {
while (ble_tx_ready(a, id)) CHECK(!ble_tx_put(a, id, frame(500, 0x31)));
uasync_poll(ua, 0);
struct fake_link* receiver = slot(backends[1], other); CHECK(receiver);
if (!queued && receiver->pending && receiver->channel == BLE_WIRE_ACK) {
CHECK(!ble_tx_put(b, other, frame(1, 0xa7))); queued = 1;
}
struct ll_entry* e;
while ((e = ble_rx_get(b, other))) { received++; release(e); }
while ((e = ble_rx_get(a, id))) { CHECK(e->len == 1 && e->dgram[0] == 0xa7); replied++; release(e); }
}
CHECK(queued && replied == 1 && received > 1);
printf("BLE ACK fairness PASS (completion=20ms, continuous RX, DATA returned in %lluus)\n",
(unsigned long long)(get_time_us() - started));
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0); ack_delay_us = 0;
}
static void exercise(unsigned int test_mtu) {
mtu = test_mtu; hold = drop = reject_peer = ready_count = writable_count = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_transport* t[3];
for (int i = 0; i < 3; i++) {
struct ble_config cfg = {0}; cfg.node_id = i + 1; cfg.company_id = 0xffff;
cfg.callbacks.validate = validate; cfg.callbacks.connection = connection; cfg.callbacks.writable = writable;
t[i] = ble_create(ua, &cfg); CHECK(t[i] && !ble_start(t[i]));
}
drain(ua);
struct ble_endpoint ep = {"1"};
ble_link_id a = ble_connect(t[0], &ep, 2); CHECK(a); drain(ua); CHECK(ready_count == 2);
ble_link_id b = incoming_ids[1];
CHECK(!ble_tx_put(t[0], a, frame(1500, 0x31))); CHECK(!ble_tx_put(t[1], b, frame(1499, 0x71))); drain(ua);
struct ll_entry* e = ble_rx_get(t[1], b); CHECK(e && e->len == 1500);
for (int i = 0; i < e->len; i++) CHECK(e->dgram[i] == 0x31);
release(e);
e = ble_rx_get(t[0], a); CHECK(e && e->len == 1499 && e->dgram[1498] == 0x71); release(e);
/* Два независимых канала одновременно, включая отказ TX и потерю фрагмента только INVITE. */
CHECK(ble_channel_tx_ready(t[0], a, BLE_CHANNEL_INVITE));
CHECK(!ble_tx_put(t[0], a, frame(1500, 0x61)));
CHECK(!ble_channel_tx_put(t[0], a, BLE_CHANNEL_INVITE, frame(1100, 0x62)));
e = frame(1, 0x63);
CHECK(ble_channel_tx_put(t[0], a, BLE_CHANNEL_INVITE, e) == BLE_WOULD_BLOCK); release(e);
CHECK(!ble_channel_tx_put(t[1], b, BLE_CHANNEL_INVITE, frame(1100, 0x64)));
drain(ua);
e = ble_rx_get(t[1], b); CHECK(e && e->len == 1500 && e->dgram[1499] == 0x61); release(e);
e = ble_channel_rx_get(t[1], b, BLE_CHANNEL_INVITE); CHECK(e && e->len == 1100 && e->dgram[1099] == 0x62); release(e);
e = ble_channel_rx_get(t[0], a, BLE_CHANNEL_INVITE); CHECK(e && e->len == 1100 && e->dgram[1099] == 0x64); release(e);
CHECK(!ble_rx_get(t[0], a) && !ble_rx_get(t[1], b));
drop = 2; CHECK(!ble_channel_tx_put(t[0], a, BLE_CHANNEL_INVITE, frame(1100, 0x65))); drain(ua);
CHECK(!ble_channel_rx_get(t[1], b, BLE_CHANNEL_INVITE));
CHECK(!ble_channel_tx_put(t[0], a, BLE_CHANNEL_INVITE, frame(20, 0x66))); drain(ua);
e = ble_channel_rx_get(t[1], b, BLE_CHANNEL_INVITE); CHECK(e && e->len == 20); release(e);
drop = 2; CHECK(!ble_tx_put(t[0], a, frame(1500, 0x44))); drain(ua); CHECK(!ble_rx_get(t[1], b));
CHECK(!ble_tx_put(t[0], a, frame(5, 0x55))); drain(ua); e = ble_rx_get(t[1], b); CHECK(e && e->len == 5); release(e);
unsigned before_writable = writable_count;
hold = 1;
for (int i = 0; i < BLE_QUEUE_MAX; i++) CHECK(!ble_tx_put(t[0], a, frame(1, i)));
e = frame(1, 99); CHECK(ble_tx_put(t[0], a, e) == BLE_WOULD_BLOCK); CHECK(!e->owner && e->dgram[0] == 99); release(e);
hold = 0; schedule(backends[0]); drain(ua); CHECK(writable_count > before_writable);
for (int i = 0; i < BLE_QUEUE_MAX; i++) { e = ble_rx_get(t[1], b); CHECK(e && e->dgram[0] == i); release(e); }
CHECK(!ble_rx_get(t[1], b));
for (int i = 0; i < BLE_QUEUE_MAX + 1; i++) { CHECK(!ble_tx_put(t[0], a, frame(2, i))); drain(ua); }
for (int i = 0; i < BLE_QUEUE_MAX; i++) { e = ble_rx_get(t[1], b); CHECK(e && e->dgram[0] == i); release(e); }
CHECK(!ble_rx_get(t[1], b));
strcpy(ep.id, "2"); ble_link_id c = ble_connect(t[0], &ep, 3); CHECK(c); drain(ua); CHECK(ready_count == 4);
hold = 1; CHECK(!ble_tx_put(t[0], a, frame(1500, 8))); CHECK(!ble_tx_put(t[0], c, frame(1500, 9)));
ble_disconnect(t[0], a); hold = 0; schedule(backends[0]); drain(ua);
e = ble_rx_get(t[2], incoming_ids[2]); CHECK(e && e->len == 1500 && e->dgram[1499] == 9); release(e);
e = frame(1, 0); CHECK(ble_tx_put(t[0], a, e) == BLE_NOT_READY); release(e);
/* Отказ политики на третьем независимом направлении не затрагивает существующее соединение. */
reject_peer = 1; strcpy(ep.id, "2"); ble_link_id rejected = ble_connect(t[1], &ep, 3); CHECK(rejected); drain(ua);
CHECK(ready_count == 4);
e = frame(1, 0); CHECK(ble_tx_put(t[1], rejected, e) == BLE_NOT_READY); release(e);
ble_set_incoming(t[2], 0);
struct ble_endpoint denied = {"policy-test"};
CHECK(!ble_backend_incoming(t[2], &denied));
CHECK(!ble_rx_get(t[2], incoming_ids[2]));
ble_set_incoming(t[2], 1);
ble_link_id early = ble_backend_incoming(t[2], &denied); CHECK(early);
uint8_t early_hello[19] = {0, 1, 1}; early_hello[10] = 1; early_hello[18] = 3;
unsigned before_closed = closed_count;
ble_backend_received(t[2], early, BLE_WIRE_DATA, early_hello, sizeof(early_hello));
CHECK(closed_count == before_closed + 1 && ready_count == 4);
for (int i = 0; i < 3; i++) ble_destroy(t[i]);
drain(ua); uasync_destroy(ua, 0);
}
/* Локальный completion мгновенный, удалённые ACK задержаны: предел относится именно к очереди ниже backend. */
static void exercise_flow(void) {
mtu = 517; hold = drop = reject_peer = hold_ack = 0;
struct UASYNC* ua = uasync_create(); CHECK(ua);
struct ble_config cfg = {.node_id = 1}; cfg.callbacks.writable = writable; cfg.callbacks.connection = connection;
struct ble_transport* a = ble_create(ua, &cfg); cfg.node_id = 2;
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
ble_link_id other = incoming_ids[1];
uint64_t base[2] = {data_wire_bytes[0], data_wire_bytes[1]};
unsigned received[2] = {0}; hold_ack = 1;
for (unsigned i = 0; i < BLE_QUEUE_MAX; i++) {
CHECK(!ble_tx_put(a, id, frame(1500, i))); CHECK(!ble_tx_put(b, other, frame(1500, i)));
}
for (unsigned i = 0; i < 10000; i++) uasync_poll(ua, 0);
for (unsigned i = 0; i < 2; i++) {
CHECK(data_wire_bytes[i] - base[i] <= BLE_TX_WINDOW_BYTES);
CHECK(data_wire_bytes[i] - base[i] >= BLE_TX_WINDOW_BYTES - BLE_VALUE_MAX);
}
CHECK(!ble_tx_ready(a, id) && !ble_tx_ready(b, other));
uint64_t paused = data_wire_bytes[0];
struct fake_link* receiver = slot(backends[1], other); CHECK(receiver);
uint8_t duplicate[2] = {receiver->ack_delivered >> 8, receiver->ack_delivered};
/* Повтор уже доставленного ACK не подтверждает новые данные даже при зависшем ACK в backend. */
ble_backend_received(a, id, BLE_WIRE_ACK, duplicate, 2);
for (unsigned i = 0; i < 1000; i++) uasync_poll(ua, 0);
CHECK(data_wire_bytes[0] == paused && !ble_tx_ready(a, id));
hold_ack = 0; schedule(backends[0]); schedule(backends[1]);
uint64_t deadline = get_time_us() + 5000000;
while ((received[0] < BLE_QUEUE_MAX || received[1] < BLE_QUEUE_MAX) && get_time_us() < deadline) {
uasync_poll(ua, 10);
for (unsigned i = 0; i < 2; i++) {
struct ll_entry* e;
while ((e = ble_rx_get(i ? b : a, i ? other : id))) {
CHECK(e->len == 1500 && e->dgram[0] == received[i] && e->dgram[1499] == received[i]);
received[i]++; release(e);
}
}
}
CHECK(received[0] == BLE_QUEUE_MAX && received[1] == BLE_QUEUE_MAX); drain(ua);
/* Больше полного оборота wire ID; новые данные действительно приходят, старые ACK не открывают окно. */
unsigned sent = 0, delivered = 0; deadline = get_time_us() + 10000000;
while (delivered < 70000 && get_time_us() < deadline) {
while (sent < 70000 && ble_tx_ready(a, id)) { CHECK(!ble_tx_put(a, id, frame(1, sent))); sent++; }
uasync_poll(ua, 0);
struct ll_entry* e;
while ((e = ble_rx_get(b, other))) { CHECK(e->len == 1 && e->dgram[0] == (uint8_t)delivered); delivered++; release(e); }
}
CHECK(sent == 70000 && delivered == sent); drain(ua);
struct fake_link* sender = slot(backends[0], id); CHECK(sender);
uint8_t future[2] = {(uint8_t)((receiver->ack_id + 1) >> 8), (uint8_t)(receiver->ack_id + 1)};
ble_backend_received(a, id, BLE_WIRE_ACK, future, 2); CHECK(!ble_tx_ready(a, id));
/* Локальный send завершается, но peer исчез: независимый от pending watchdog закрывает линк через 15s. */
ack_timeouts = 0;
uint64_t started = get_time_us(); CHECK(!ble_tx_put(b, other, frame(1, 0)));
while (!ack_timeouts && get_time_us() - started < 17000000) uasync_poll(ua, 1000);
CHECK(ack_timeouts == 1 && get_time_us() - started >= 15000000 && !ble_tx_ready(b, other));
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
puts("BLE flow PASS (12500-byte duplex bound, ACK >=15ms, duplicate ACK, resume, 70000 IDs, invalid ACK, timeout, teardown)");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
size_t before = u_get_allocated_count();
exercise_ack_fairness(); CHECK(u_get_allocated_count() == before);
exercise(23); exercise(247); exercise(517);
exercise_flow();
CHECK(u_get_allocated_count() == before); puts("BLE transport tests PASS (MTU 23/247/517, duplex, loss, backpressure, peers, teardown)"); return 0;
}
+98
View File
@@ -0,0 +1,98 @@
/* Настоящий WinRT mailbox без радио: исключения, изоляция link и отмена поздних callbacks.
* Включаем backend для доступа к внутренним событиям; общий transport заменён наблюдателем. */
#include "../ble_winrt.cpp"
#include <cstdlib>
#define CHECK(value) do { if (!(value)) { DEBUG_ERROR(CAT, "CHECK failed line=%d: %s", __LINE__, #value); std::abort(); } } while (0)
struct ble_transport {
WinState* state = nullptr;
unsigned radio_failures = 0, link_failures = 0;
ble_link_id closed = 0;
std::string reason;
};
extern "C" void ble_backend_closed(ble_transport* t, ble_link_id id, const char* reason) {
++t->link_failures; t->closed = id; t->reason = reason; t->state->close(id);
}
extern "C" void ble_backend_status(ble_transport* t, int ready, const char* reason) {
CHECK(!ready); ++t->radio_failures; t->reason = reason;
while (!t->state->links.empty()) t->state->close(t->state->links.begin()->first);
}
/* Эти события не должны возникать в проверке mailbox. */
extern "C" ble_link_id ble_backend_incoming(ble_transport*, const ble_endpoint*) { CHECK(false); return 0; }
extern "C" void ble_backend_advertising(ble_transport*, ble_state, const char*) { CHECK(false); }
extern "C" void ble_backend_broadcast(ble_transport*, const ble_endpoint*, const uint8_t*, size_t, int) { CHECK(false); }
extern "C" void ble_backend_opened(ble_transport*, ble_link_id, unsigned) { CHECK(false); }
extern "C" void ble_backend_received(ble_transport*, ble_link_id, ble_wire_channel, const uint8_t*, size_t) { CHECK(false); }
extern "C" void ble_backend_sent(ble_transport*, ble_link_id, int) { CHECK(false); }
static std::shared_ptr<WinState> make_state(UASYNC* ua, ble_transport& owner) {
auto s = std::make_shared<WinState>(); s->ua = ua; s->owner = &owner; owner.state = s.get();
for (ble_link_id id : {7, 9}) {
auto l = std::make_unique<WinLink>(); l->id = id; s->links.emplace(id, std::move(l));
}
return s;
}
/* Ошибка GATT одного пира не переводит radio в failed и не теряет событие другого пира. */
static void link_exception(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
bool other_processed = false;
s->post("test.ValueChanged", 7, [](WinState& state) {
state.step("test.CharacteristicValue", 7);
throw hresult_error(E_NOINTERFACE, L"test interface absent");
});
s->post("test.other_peer", 9, [&other_processed](WinState&) { other_processed = true; });
uasync_poll(ua, 0);
CHECK(owner.link_failures == 1 && owner.closed == 7 && owner.radio_failures == 0);
CHECK(!s->failed && !s->link(7) && s->link(9) && other_processed);
CHECK(owner.reason.find("phase=test.CharacteristicValue link=7 HRESULT=0x80004002") != std::string::npos);
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: link exception preserves other peer and exact HRESULT context");
}
/* Общая ошибка сохраняет точную операцию даже при выключенном TRACE и останавливает batch. */
static void radio_exception(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
bool processed = false;
s->post("test.Received", 0, [](WinState& state) {
state.step("test.BluetoothAddressType");
throw hresult_error(E_NOINTERFACE, L"test interface absent");
});
s->post("test.after_failure", 9, [&processed](WinState&) { processed = true; });
uasync_poll(ua, 0);
CHECK(owner.radio_failures == 1 && owner.link_failures == 0 && s->failed);
CHECK(s->links.empty() && !processed);
CHECK(owner.reason.find("phase=test.BluetoothAddressType link=0 HRESULT=0x80004002") != std::string::npos);
s->shutdown();
DEBUG_INFO(CAT, "WinRT test PASS: radio exception stops queued delivery with TRACE disabled");
}
/* shutdown отменяет posted_task, освобождает captures и отвергает позднее событие Windows. */
static void pending_shutdown(UASYNC* ua) {
ble_transport owner;
auto s = make_state(ua, owner);
auto payload = std::make_shared<int>(0);
std::weak_ptr<int> weak_payload = payload;
bool processed = false;
s->post("test.pending", 9, [payload, &processed](WinState&) { processed = true; });
payload.reset(); CHECK(!weak_payload.expired());
s->shutdown(); CHECK(weak_payload.expired());
s->post("test.late", 9, [&processed](WinState&) { processed = true; });
CHECK(s->events.empty() && !s->posted);
std::weak_ptr<WinState> weak = s; s.reset();
uasync_poll(ua, 0);
CHECK(weak.expired() && !processed && owner.radio_failures == 0 && owner.link_failures == 0);
DEBUG_INFO(CAT, "WinRT test PASS: shutdown cancels queued work and rejects late callbacks");
}
int main() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
init_apartment(apartment_type::multi_threaded);
UASYNC* ua = uasync_create(); CHECK(ua);
link_exception(ua); radio_exception(ua); pending_shutdown(ua);
uasync_destroy(ua, 0); uninit_apartment();
return 0;
}
+204 -21
View File
@@ -12,6 +12,10 @@
#include <stdarg.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <pthread.h>
#endif
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len) {
if (!debug_should_output(level, category)) return;
@@ -219,8 +223,151 @@ const char* debug_get_category_name(debug_category_t category_idx) {
debug_config_t g_debug_config;
static debug_log_hook_fn g_log_hook = NULL;
/* Запись/смена FILE и ротация сериализованы: fclose не пересекается с чужим fwrite.
* Файловый код logger-а не вызывает file_io/DEBUG_* под lock: они сами логируют ошибки. */
#ifdef _WIN32
static SRWLOCK log_mutex = SRWLOCK_INIT;
#define LOG_LOCK() AcquireSRWLockExclusive(&log_mutex)
#define LOG_UNLOCK() ReleaseSRWLockExclusive(&log_mutex)
#else
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
#define LOG_LOCK() pthread_mutex_lock(&log_mutex)
#define LOG_UNLOCK() pthread_mutex_unlock(&log_mutex)
#endif
#define LOG_PATH_MAX 4096
static uint64_t log_bytes, log_limit, log_rotate_at;
static unsigned log_files;
#ifdef _WIN32
static int log_wide_path(const char* path, wchar_t wide[LOG_PATH_MAX]) {
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, LOG_PATH_MAX)) return 0;
errno = EINVAL;
return -1;
}
#endif
static FILE* log_open(const char* path, int truncate, uint64_t* bytes) {
if (!path || !*path || strlen(path) >= LOG_PATH_MAX - 4) { errno = ENAMETOOLONG; return NULL; }
#ifdef _WIN32
wchar_t wide[LOG_PATH_MAX];
if (log_wide_path(path, wide)) return NULL;
FILE* file = _wfopen(wide, truncate ? L"wb" : L"ab");
struct _stat64 info;
int result = file ? _fstat64(_fileno(file), &info) : -1;
#else
FILE* file = fopen(path, truncate ? "wb" : "ab");
struct stat info;
int result = file ? fstat(fileno(file), &info) : -1;
#endif
if (!file) return NULL;
if (result) { int error = errno; fclose(file); errno = error; return NULL; }
*bytes = info.st_size > 0 ? (uint64_t)info.st_size : 0;
return file;
}
static int log_remove(const char* path) {
#ifdef _WIN32
wchar_t wide[LOG_PATH_MAX];
if (log_wide_path(path, wide)) return -1;
return _wremove(wide);
#else
return remove(path);
#endif
}
static int log_rename(const char* from, const char* to) {
#ifdef _WIN32
wchar_t source[LOG_PATH_MAX], destination[LOG_PATH_MAX];
if (log_wide_path(from, source) || log_wide_path(to, destination)) return -1;
return _wrename(source, destination);
#else
return rename(from, to);
#endif
}
/* Ошибку logger-а пишем напрямую, без рекурсии, в stderr и доступный текущий лог. */
static void log_io_error(const char* phase, const char* path, int error) {
char message[1024];
snprintf(message, sizeof(message), "[ERROR] [sys] log %s failed: path=%s errno=%d (%s)\n", phase, path, error, strerror(error));
fputs(message, stderr); fflush(stderr);
if (g_debug_config.file_output) {
log_bytes += fwrite(message, 1, strlen(message), g_debug_config.file_output);
fflush(g_debug_config.file_output);
}
}
/* Под lock, без проверок размера на диске. Пустые файлы не занимают поколения. */
static int log_rotate(const char* trigger) {
if (!log_bytes) return 0;
uint64_t previous_size = log_bytes;
const char* path = g_debug_config.output_file;
char from[LOG_PATH_MAX], to[LOG_PATH_MAX];
const char* phase = "rotate close";
int error = 0;
FILE* old = g_debug_config.file_output;
g_debug_config.file_output = NULL;
if (fclose(old)) { error = errno; goto reopen; }
if (log_files > 1) {
snprintf(to, sizeof(to), "%s.%u", path, log_files - 1);
phase = "rotate remove oldest";
if (log_remove(to) && errno != ENOENT) { error = errno; goto reopen; }
for (unsigned i = log_files - 1; i > 1; --i) {
snprintf(from, sizeof(from), "%s.%u", path, i - 1);
snprintf(to, sizeof(to), "%s.%u", path, i);
phase = "rotate rename generation";
if (log_rename(from, to) && errno != ENOENT) { error = errno; goto reopen; }
}
snprintf(to, sizeof(to), "%s.1", path);
phase = "rotate rename current";
if (log_rename(path, to)) { error = errno; goto reopen; }
}
reopen:
g_debug_config.file_output = log_open(path, !error && log_files == 1, &log_bytes);
if (!g_debug_config.file_output) {
log_io_error("rotate reopen", path, errno);
g_debug_config.console_enabled = 1;
return -1;
}
if (error) log_io_error(phase, path, error);
else if (debug_should_output(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_SYS)) {
char message[192];
int size = snprintf(message, sizeof(message), "[INFO] [sys] Log rotated: trigger=%s previous_bytes=%llu files=%u\n",
trigger, (unsigned long long)previous_size, log_files);
log_bytes += fwrite(message, 1, (size_t)size, g_debug_config.file_output);
if (fflush(g_debug_config.file_output)) log_io_error("flush rotation notice", path, errno);
}
/* При ошибке не повторяем файловые операции на каждой строке: ещё limit байтов до попытки. */
log_rotate_at = error ? log_bytes + log_limit : log_limit;
return error ? -1 : 0;
}
int debug_set_file_rotation(uint64_t max_bytes, unsigned file_count) {
if (max_bytes && (max_bytes < 4096 || max_bytes > UINT64_MAX / 2 || !file_count || file_count > 99)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid log rotation: bytes=%llu files=%u", (unsigned long long)max_bytes, file_count);
return -1;
}
LOG_LOCK();
log_limit = max_bytes; log_files = file_count; log_rotate_at = max_bytes;
LOG_UNLOCK();
return 0;
}
int debug_rotate_log(void) {
LOG_LOCK();
if (!log_limit || !g_debug_config.file_output) {
LOG_UNLOCK();
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot rotate log: file output or rotation is disabled");
return -1;
}
int result = log_rotate("startup/manual");
LOG_UNLOCK();
return result;
}
/* Initialize debug system with default settings */
void debug_config_init(void) {
LOG_LOCK();
log_bytes = log_limit = log_rotate_at = 0; log_files = 0;
g_debug_config.level = DEBUG_LEVEL_ERROR; // Global level: errors only by default
for (int i = 0; i < DEBUG_CATEGORY_COUNT; i++) {
g_debug_config.category_levels[i] = DEBUG_LEVEL_NONE; // 0 = disabled per category
@@ -234,6 +381,7 @@ void debug_config_init(void) {
g_debug_config.file_output = NULL; // No file by default
g_debug_config.console_enabled = 1; // Console enabled
g_debug_config.thread_marker = 0; // No thread marker by default
LOG_UNLOCK();
}
/* Set debug level */
@@ -285,12 +433,18 @@ void debug_enable_file_line(int enable) {
}
void debug_set_output_file(const char* file_path) {
if (g_debug_config.file_output) return;// уже установлен, кто первый того и тапки
FILE* new_file = fopen(file_path, "a");
if (!file_path) return;
LOG_LOCK();
if (g_debug_config.file_output) { LOG_UNLOCK(); return; }
FILE* new_file = log_open(file_path, 0, &log_bytes);
if (new_file) {
g_debug_config.file_output = new_file;
g_debug_config.output_file = file_path;
log_rotate_at = log_limit;
}
int error = errno;
LOG_UNLOCK();
if (!new_file) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot open log '%s': %s", file_path, strerror(error));
}
/* Set console log level */
@@ -301,50 +455,61 @@ void debug_enable_console(int enable) {
/* Enable file output with optional truncation */
void debug_enable_file_output(const char* file_path, int truncate) {
if (!file_path) return;
// Close existing file if open
if (g_debug_config.file_output) {
fclose(g_debug_config.file_output);
g_debug_config.file_output = NULL;
}
// Open new file
const char* mode = truncate ? "w" : "a";
FILE* new_file = fopen(file_path, mode);
LOG_LOCK();
uint64_t bytes;
FILE* new_file = log_open(file_path, truncate, &bytes);
int error = errno;
if (new_file) {
FILE* old = g_debug_config.file_output;
g_debug_config.file_output = new_file;
g_debug_config.output_file = file_path;
g_debug_config.console_enabled = 0;
log_bytes = bytes; log_rotate_at = log_limit;
if (old && fclose(old)) log_io_error("close previous", file_path, errno);
}
LOG_UNLOCK();
if (!new_file) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot open log '%s': %s", file_path, strerror(error));
}
/* Disable file output */
void debug_disable_file_output(void) {
LOG_LOCK();
if (g_debug_config.file_output) {
fclose(g_debug_config.file_output);
FILE* old = g_debug_config.file_output;
g_debug_config.file_output = NULL;
g_debug_config.output_file = NULL;
if (fclose(old)) log_io_error("close", g_debug_config.output_file, errno);
}
g_debug_config.output_file = NULL; log_bytes = 0;
LOG_UNLOCK();
}
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
int debug_reopen_log(void) {
LOG_LOCK();
if (!g_debug_config.output_file) {
LOG_UNLOCK();
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
return -1;
}
const char* path = g_debug_config.output_file;
FILE* new_file = fopen(path, "a");
uint64_t bytes;
FILE* new_file = log_open(path, 0, &bytes);
if (!new_file) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
int error = errno;
LOG_UNLOCK();
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(error));
return -1;
}
FILE* old_file = g_debug_config.file_output;
g_debug_config.file_output = new_file;
log_bytes = bytes; log_rotate_at = log_limit;
if (old_file && fclose(old_file) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
int error = errno;
LOG_UNLOCK();
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(error));
return -1;
}
LOG_UNLOCK();
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
return 0;
}
@@ -408,9 +573,14 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
struct timeval tv;
utun_gettimeofday(&tv, NULL);
time_t tv_sec = (time_t)tv.tv_sec;
struct tm* tm_info = localtime(&tv_sec);
struct tm tm_info;
#ifdef _WIN32
localtime_s(&tm_info, &tv_sec);
#else
localtime_r(&tv_sec, &tm_info);
#endif
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_info);
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
}
@@ -461,13 +631,26 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
}
/* Output to console if enabled and level is sufficient */
LOG_LOCK();
if (g_debug_config.file_output && log_limit && log_bytes >= log_rotate_at) log_rotate("size");
if (g_debug_config.file_output) {
fprintf(g_debug_config.file_output, "%s", buffer);
fflush(g_debug_config.file_output);
size_t size = strlen(buffer);
size_t written = fwrite(buffer, 1, size, g_debug_config.file_output);
log_bytes += written;
if (fflush(g_debug_config.file_output) || written != size) {
/* Не пытаемся снова писать в неисправный FILE и не теряем диагностику следующей строки. */
int error = errno;
FILE* broken = g_debug_config.file_output; g_debug_config.file_output = NULL;
log_io_error("write", g_debug_config.output_file, error);
if (fclose(broken)) log_io_error("close after write error", g_debug_config.output_file, errno);
g_debug_config.console_enabled = 1;
fputs(buffer, stderr); fflush(stderr);
}
} else if (g_debug_config.console_enabled) {
fprintf(stdout, "%s", buffer);
fflush(stdout);
}
LOG_UNLOCK();
#undef BUFFER_SIZE
}
+13 -2
View File
@@ -6,7 +6,7 @@
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
* Запись/смена файла и ротация синхронизированы; уровни/настройки форматирования согласует владелец.
*/
#ifndef DEBUG_CONFIG_H
@@ -132,8 +132,19 @@ void debug_enable_file_line(int enable);
void debug_set_output_file(const char* file_path);
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
* Путь заимствован; успешное открытие также выключает console_enabled. */
* Путь UTF-8 заимствован; успешное открытие выключает console_enabled; при отказе прежний FILE сохраняется. */
void debug_enable_file_output(const char* file_path, int truncate);
/* Ротация выключена после init. max_bytes=0 выключает; иначе >=4096, file_count=1..99
* включая текущий файл. После примерно max_bytes: path → path.1 → ...; последний удаляется.
* Только rename/remove, без сжатия. Строки не разрываются: превышение до одной строки.
* Размер читается только при открытии; далее счётчик записанных байтов. Один процесс владеет путём.
* 0/-1; повторная настройка и reopen сами по себе не ротируют. */
int debug_set_file_rotation(uint64_t max_bytes, unsigned file_count);
/* Начать новый лог немедленно (например, при запуске приложения); 0/-1.
* Нужны открытый файл и включённая ротация; пустой файл не создаёт пустое поколение.
* Ошибка ротации логируется; по возможности продолжается append, повтор через max_bytes. */
int debug_rotate_log(void);
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
void debug_disable_file_output(void);
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
+41
View File
@@ -6,14 +6,54 @@
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
#define CHECK(p, end) if ((p) >= (end)) return -1
static int json_unicode_unit(const char** text, const char* end, uint32_t* value) {
if (end - *text < 4) return -1;
uint32_t result = 0;
for (int i = 0; i < 4; i++) {
unsigned char c = (unsigned char)(*text)[i]; int digit;
if (c >= '0' && c <= '9') digit = c - '0';
else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10;
else return -1;
result = (result << 4) | (uint32_t)digit;
}
*text += 4; *value = result; return 0;
}
static int json_read_string(const char** pp, const char* end, char* out, size_t out_sz) {
const char* p = *pp;
CHECK(p, end);
if (*p != '"') return -1; p++;
size_t n = 0;
while (p < end && *p != '"') {
if ((unsigned char)*p < 32) return -1;
char ch = *p;
if (*p == '\\') { p++; CHECK(p, end);
if (*p == 'u') {
uint32_t code; p++; if (json_unicode_unit(&p, end, &code) < 0 || !code) return -1;
if (code >= 0xd800 && code <= 0xdbff) {
if (end - p < 2 || p[0] != '\\' || p[1] != 'u') return -1;
p += 2; uint32_t low;
if (json_unicode_unit(&p, end, &low) < 0 || low < 0xdc00 || low > 0xdfff) return -1;
code = 0x10000 + ((code - 0xd800) << 10) + low - 0xdc00;
} else if (code >= 0xdc00 && code <= 0xdfff) return -1;
uint8_t utf8[4]; size_t bytes;
if (code < 0x80) { utf8[0] = (uint8_t)code; bytes = 1; }
else if (code < 0x800) {
utf8[0] = (uint8_t)(0xc0 | (code >> 6)); utf8[1] = (uint8_t)(0x80 | (code & 63)); bytes = 2;
} else if (code < 0x10000) {
utf8[0] = (uint8_t)(0xe0 | (code >> 12)); utf8[1] = (uint8_t)(0x80 | ((code >> 6) & 63));
utf8[2] = (uint8_t)(0x80 | (code & 63)); bytes = 3;
} else {
utf8[0] = (uint8_t)(0xf0 | (code >> 18)); utf8[1] = (uint8_t)(0x80 | ((code >> 12) & 63));
utf8[2] = (uint8_t)(0x80 | ((code >> 6) & 63)); utf8[3] = (uint8_t)(0x80 | (code & 63)); bytes = 4;
}
if (out) {
if (n + bytes >= out_sz) return -1;
memcpy(out + n, utf8, bytes); n += bytes;
}
continue;
}
switch (*p) {
case '"': ch = '"'; break;
case '\\': ch = '\\'; break;
@@ -83,6 +123,7 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
if (*p == '}') break;
}
if (scratch != local) u_free(scratch);
if (result < 0) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "json_flat: invalid object/string bytes=%zu", capacity - 1);
return result;
}
+2 -1
View File
@@ -2,7 +2,8 @@
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
* Только строковые значения,обычные escapes и \uXXXX (surrogate pairs→UTF-8).
* Нулевой Unicode-символ,непарные суррогаты,неверный hex и сырые ASCII-control отклоняются.
* Callback получает полные key/value, действительные только до его возврата.
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
+161
View File
@@ -0,0 +1,161 @@
#include "packet_link_backend.h"
#include "u_async.h"
#include "ll_queue.h"
#include "mem.h"
#include "debug_config.h"
#include <stdlib.h>
struct packet_link {
struct UASYNC* ua;
struct packet_link_driver driver;
char* name;
struct packet_link_callbacks callbacks;
struct packet_link_state state;
void *arg, *notification;
unsigned active;
int closing, rx_enabled, sending;
};
static void destroy_deferred(void* arg) {
struct packet_link* link = arg;
link->driver.destroy(link->driver.ctx);
u_free(link->name);
u_free(link);
}
static void release(struct packet_link* link) {
if (--link->active || !link->closing) return;
/* Backend мог вызвать receive из своего I/O callback: его стек тоже должен завершиться. */
if (!uasync_call_soon(link->ua, link, destroy_deferred)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: cannot defer destruction"); abort();
}
}
static void notify(void* arg) {
struct packet_link* link = arg;
link->notification = NULL;
link->active++;
if (link->callbacks.state_changed) link->callbacks.state_changed(link, link->arg);
release(link);
}
static int schedule(struct packet_link* link) {
if (link->notification || link->closing) return 0;
link->notification = uasync_call_soon(link->ua, link, notify);
if (link->notification) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: cannot schedule state notification");
return -1;
}
static int valid_state(const struct packet_link_state* s) {
return s && s->phase >= PACKET_LINK_CONNECTING && s->phase <= PACKET_LINK_TERMINATED &&
s->max_packet && s->max_packet <= UINT16_MAX && (!s->tx_ready || s->phase == PACKET_LINK_OPEN);
}
static void driver_changed(void* arg);
static void driver_received(void* arg, struct ll_entry* packet);
struct packet_link* packet_link_bind(const struct packet_link_driver* driver,
const struct packet_link_callbacks* callbacks, void* arg) {
if (!driver || !driver->ua || !driver->name || !driver->name[0] || !driver->bind || !driver->get_state ||
!driver->send || !driver->stop || !driver->destroy || !driver->set_rx_enabled || !callbacks || !callbacks->received ||
driver->direction < PACKET_LINK_DIRECTION_UNKNOWN || driver->direction > PACKET_LINK_INCOMING) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: invalid driver API"); return NULL;
}
struct packet_link_state initial = driver->get_state(driver->ctx);
if (!valid_state(&initial) || initial.phase == PACKET_LINK_TERMINATED) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link %s: invalid initial state", driver->name); return NULL;
}
struct packet_link* link = u_calloc(1, sizeof(*link));
if (!link) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: allocation failed"); return NULL; }
link->ua = driver->ua; link->driver = *driver; link->state = initial; link->callbacks = *callbacks; link->arg = arg;
link->name = u_strdup(driver->name);
if (!link->name || schedule(link)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: cannot prepare binding"); u_free(link->name); u_free(link); return NULL;
}
link->driver.name = link->name;
const struct packet_link_events events = {driver_changed, driver_received};
if (driver->bind(driver->ctx, &events, link)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link %s: driver binding rejected", link->name);
uasync_call_soon_cancel(link->ua, link->notification); u_free(link->name); u_free(link); return NULL;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "packet link=%p driver=%s direction=%d phase=%d max=%zu ready=%d",
(void*)link, link->name, link->driver.direction, initial.phase, initial.max_packet, initial.tx_ready);
return link;
}
const char* packet_link_name(const struct packet_link* link) { return link ? link->name : "unbound"; }
enum packet_link_direction packet_link_get_direction(const struct packet_link* link) {
return link ? link->driver.direction : PACKET_LINK_DIRECTION_UNKNOWN;
}
struct packet_link_state packet_link_get_state(const struct packet_link* link) {
if (!link || link->closing) return (struct packet_link_state){PACKET_LINK_TERMINATED, 0, 0, PACKET_LINK_CLOSED};
return link->state;
}
static void driver_changed(void* arg) {
struct packet_link* link = arg;
if (!link || link->closing) return;
struct packet_link_state snapshot = link->driver.get_state(link->driver.ctx);
const struct packet_link_state* state = &snapshot;
if (!valid_state(state) || state->max_packet != link->state.max_packet || (link->state.phase == PACKET_LINK_TERMINATED && state->phase != PACKET_LINK_TERMINATED)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: invalid state transition"); return;
}
struct packet_link_state old = link->state;
if (old.phase == state->phase && old.max_packet == state->max_packet && old.tx_ready == state->tx_ready && old.error == state->error) return;
link->state = *state;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "packet link=%p phase=%d ready=%d max=%zu error=%d",
(void*)link, state->phase, state->tx_ready, state->max_packet, state->error);
/* Потерять обязательное событие нельзя; синхронный callback нарушил бы контракт send. */
if (schedule(link)) abort();
}
int packet_link_send(struct packet_link* link, struct ll_entry* packet) {
if (!link || link->closing || link->state.phase == PACKET_LINK_TERMINATED) return PACKET_LINK_CLOSED;
if (!packet || packet->owner || packet->next || packet->prev || !packet->dgram || !packet->len || packet->len > packet->memlen) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: invalid TX packet"); return PACKET_LINK_INVALID;
}
if (packet->len > link->state.max_packet) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link: oversized TX size=%u max=%zu", packet->len, link->state.max_packet);
return PACKET_LINK_TOO_LARGE;
}
if (!link->state.tx_ready || link->sending) return PACKET_LINK_WOULD_BLOCK;
link->active++; link->sending = 1;
int result = link->driver.send(link->driver.ctx, packet);
link->sending = 0;
release(link);
return result;
}
void packet_link_set_rx_enabled(struct packet_link* link, int enabled) {
if (!link || link->closing || link->rx_enabled == !!enabled) return;
link->active++; link->rx_enabled = !!enabled;
link->driver.set_rx_enabled(link->driver.ctx, !!enabled);
release(link);
}
static void driver_received(void* arg, struct ll_entry* packet) {
struct packet_link* link = arg;
if (!packet || packet->owner || packet->next || packet->prev || !packet->dgram || !packet->len ||
packet->len > packet->memlen || packet->len > link->state.max_packet ||
link->closing || !link->rx_enabled || link->sending || link->state.phase != PACKET_LINK_OPEN) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "packet link %s: invalid RX bytes=%u capacity=%u queued=%d phase=%d enabled=%d sending=%d closing=%d",
link->name, packet ? packet->len : 0, packet ? packet->memlen : 0, packet && packet->owner != NULL,
link->state.phase, link->rx_enabled, link->sending, link->closing);
if (packet) { queue_dgram_free(packet); queue_entry_free(packet); }
return;
}
link->active++;
link->callbacks.received(link, packet, link->arg);
release(link);
}
void packet_link_close(struct packet_link* link) {
if (!link || link->closing) return;
link->active++; link->closing = 1;
if (link->notification) { uasync_call_soon_cancel(link->ua, link->notification); link->notification = NULL; }
link->driver.stop(link->driver.ctx);
release(link);
}
+43
View File
@@ -0,0 +1,43 @@
/* Пакетный канал одного uasync-потока: state → send/received → close.
* Каждый модуль сообщает готовность только собственных ресурсов. Формат байтов задаёт пользователь.
* send не вызывает верхние callbacks; received передаёт владение entry и dgram получателю.
* close допустим из callback, отменяет следующие события; после close handle недействителен. */
#ifndef PACKET_LINK_H
#define PACKET_LINK_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
struct packet_link;
struct ll_entry;
enum packet_link_result {
PACKET_LINK_OK = 0, PACKET_LINK_WOULD_BLOCK = -1, PACKET_LINK_CLOSED = -2,
PACKET_LINK_TOO_LARGE = -3, PACKET_LINK_INVALID = -4, PACKET_LINK_ERROR = -5
};
enum packet_link_phase { PACKET_LINK_CONNECTING, PACKET_LINK_OPEN, PACKET_LINK_TERMINATED };
/* Направление физического подключения; не роль протокола, передаваемого по каналу. */
enum packet_link_direction { PACKET_LINK_DIRECTION_UNKNOWN, PACKET_LINK_OUTGOING, PACKET_LINK_INCOMING };
struct packet_link_state {
enum packet_link_phase phase; /* OPEN — готов транспорт, не ETCP-сессия. */
size_t max_packet; /* Максимальный пакет, включая байты формата пользователя. */
int tx_ready; /* Есть место для одного пакета до max_packet. */
int error; /* 0 — штатно; иначе packet_link_result. Подробности — в логе реализации. */
};
struct packet_link_callbacks {
void (*state_changed)(struct packet_link* link, void* arg); /* События объединяются; читать актуальный get_state. */
void (*received)(struct packet_link* link, struct ll_entry* packet, void* arg); /* Получатель владеет пакетом. */
};
/* Снимок без callbacks/изменения очередей; NULL возвращает TERMINATED/CLOSED. */
struct packet_link_state packet_link_get_state(const struct packet_link* link);
/* Неизменное направление из скопированного описания драйвера; NULL возвращает UNKNOWN. */
enum packet_link_direction packet_link_get_direction(const struct packet_link* link);
/* Целый извлечённый из очереди entry: OK передаёт владение, любой отказ оставляет его вызывающему. */
int packet_link_send(struct packet_link* link, struct ll_entry* packet);
/* Включить/приостановить received; нижний модуль сам ограничивает накопление. Изначально RX выключен. */
void packet_link_set_rx_enabled(struct packet_link* link, int enabled);
/* Отменить события и остановить реализацию; освобождение ждёт активные обработчики. NULL допустим. */
void packet_link_close(struct packet_link* link);
#ifdef __cplusplus
}
#endif
#endif
+37
View File
@@ -0,0 +1,37 @@
/* Экземпляр пакетного драйвера: создать → передать packet_link_bind → stop/destroy.
* Один ctx — один канал к пиру; общий сокет/радио принадлежит платформенному владельцу.
* Все API и события выполняются в ua. До bind RX выключен. */
#ifndef PACKET_LINK_BACKEND_H
#define PACKET_LINK_BACKEND_H
#include "packet_link.h"
#ifdef __cplusplus
extern "C" {
#endif
struct UASYNC;
struct packet_link_events {
void (*state_changed)(void* arg); /* Читать актуальный get_state; событие обязательно при изменении готовности. */
void (*received)(void* arg, struct ll_entry* packet); /* Передаёт владение entry+dgram получателю. */
};
struct packet_link_driver {
void* ctx; /* Управляющая структура одного канала. */
struct UASYNC* ua; /* Тот же event loop, что у потребителя. */
const char* name; /* Непустое диагностическое имя; оболочка копирует строку. */
/* Копирует events, заимствует arg. 0 — успех, отказ оставляет ctx непривязанным. Без синхронных событий. */
int (*bind)(void* ctx, const struct packet_link_events* events, void* arg);
struct packet_link_state (*get_state)(void* ctx); /* Снимок без побочных действий. */
int (*send)(void* ctx, struct ll_entry* packet); /* OK передаёт владение; любой отказ сохраняет его. Без RX callbacks. */
void (*set_rx_enabled)(void* ctx, int enabled); /* Разрешить/приостановить RX; накоплением управляет драйвер. */
void (*stop)(void* ctx); /* Сразу запретить события, остановить канал; не освобождать ctx. */
void (*destroy)(void* ctx); /* Освободить остановленный ctx после выхода обработчиков. */
enum packet_link_direction direction; /* Диагностика подключения; копируется при bind, 0 — неизвестно. */
};
/* Копирует API/name/callbacks. Успех передаёт ctx оболочке; отказ сохраняет его вызывающему.
* arg живёт до close. Первое state_changed отложено. TERMINATED необратим, max_packet не меняется. */
struct packet_link* packet_link_bind(const struct packet_link_driver* driver,
const struct packet_link_callbacks* callbacks, void* arg);
/* Заимствованное диагностическое имя до close. */
const char* packet_link_name(const struct packet_link* link);
#ifdef __cplusplus
}
#endif
#endif
+9 -1
View File
@@ -11,6 +11,8 @@
#define STRBUF_MIN_CAP 16
void strbuf_init(struct strbuf *sb, size_t initial_cap) {
*sb = strbuf_new();
if (initial_cap > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "strbuf: capacity exceeds allocator range=%zu", initial_cap); return; }
if (initial_cap < STRBUF_MIN_CAP) initial_cap = STRBUF_MIN_CAP;
sb->buf = u_malloc(initial_cap);
if (sb->buf) { sb->buf[0] = '\0'; sb->cap = initial_cap; }
@@ -35,8 +37,9 @@ void strbuf_reset(struct strbuf *sb) {
}
static int strbuf_grow(struct strbuf *sb, size_t need) {
if (need > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "strbuf: size exceeds allocator range=%zu", need); return -1; }
size_t nc = sb->cap ? sb->cap : STRBUF_MIN_CAP;
while (nc < need) { nc *= 2; if (nc < STRBUF_MIN_CAP) return -1; }
while (nc < need) { if (nc > UINT32_MAX / 2) { nc = need; break; } nc *= 2; }
char *n = u_realloc(sb->buf, nc);
if (!n) return -1;
sb->buf = n; sb->cap = nc;
@@ -51,6 +54,9 @@ int strbuf_addf(struct strbuf *sb, const char *fmt, ...) {
int need = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (need < 0) { return -1; }
if ((size_t)need > UINT32_MAX - 1 - sb->len) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "strbuf: formatted text exceeds allocator range"); return -1;
}
/* grow */
size_t want = sb->len + (size_t)need + 1; /* +1 for '\0' */
@@ -65,6 +71,7 @@ int strbuf_addf(struct strbuf *sb, const char *fmt, ...) {
}
void strbuf_addc(struct strbuf *sb, char c) {
if (sb->len > UINT32_MAX - 2) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "strbuf: character exceeds allocator range"); return; }
if (sb->len + 2 > sb->cap && strbuf_grow(sb, sb->len + 2) < 0) return;
sb->buf[sb->len++] = c;
sb->buf[sb->len] = '\0';
@@ -96,6 +103,7 @@ failed:
void strbuf_adds(struct strbuf *sb, const char *s) {
if (!s) return;
size_t slen = strlen(s);
if (slen > UINT32_MAX - 1 - sb->len) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "strbuf: string exceeds allocator range"); return; }
if (sb->len + slen + 1 > sb->cap && strbuf_grow(sb, sb->len + slen + 1) < 0) return;
memcpy(sb->buf + sb->len, s, slen);
sb->len += slen;
+3
View File
@@ -25,6 +25,9 @@ struct strbuf {
size_t len; /* current length (excluding '\0') */
};
/* Размер ограничен UINT32_MAX allocator-ом mem.h;превышение отклоняется до преобразования size_t.
* init предназначен для нового буфера;повторная инициализация живого буфера требует предварительного free. */
/* zero-init on stack: struct strbuf sb = {0}; */
#define strbuf_new() ((struct strbuf){NULL, 0, 0})
+30 -12
View File
@@ -403,19 +403,23 @@ static void drain_wakeup_pipe(struct UASYNC* ua) {
while (read(ua->wakeup_pipe[0], &val, sizeof(val)) > 0) {}
}
// Process posted tasks (lock-u_free during execution)
/* Стек пачек сохраняет доступ к ожидающим задачам при отмене из вложенного poll. */
struct uasync_post_batch {
struct uasync_post_batch* previous;
struct posted_task* head;
};
// Исполняем снимок очереди без блокировки; новые post остаются для следующей пачки.
static void process_posted_tasks(struct UASYNC* ua) {
if (!ua) return;
struct posted_task* list = NULL;
struct uasync_post_batch batch = { .previous = ua->posted_batches };
#ifdef _WIN32
EnterCriticalSection(&ua->posted_lock);
#else
pthread_mutex_lock(&ua->posted_lock);
#endif
list = ua->posted_tasks_head;
batch.head = ua->posted_tasks_head;
ua->posted_tasks_head = ua->posted_tasks_tail = NULL;
#ifdef _WIN32
LeaveCriticalSection(&ua->posted_lock);
@@ -423,16 +427,18 @@ static void process_posted_tasks(struct UASYNC* ua) {
pthread_mutex_unlock(&ua->posted_lock);
#endif
while (list) {
DEBUG_DEBUG(DEBUG_CATEGORY_TUN, "POSTed task get");
struct posted_task* t = list;
list = list->next;
ua->posted_batches = &batch;
while (batch.head) {
struct posted_task* t = batch.head;
batch.head = t->next; // Исполняемый callback уже не подлежит отмене.
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "uasync: executing post task=%p arg=%p", (void*)t, t->arg);
if (t->callback) {
t->callback(t->arg);
}
u_free(t);
}
ua->posted_batches = batch.previous;
}
#ifdef _WIN32
@@ -1846,7 +1852,7 @@ void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task) {
uasync_wakeup(ua); // будим mainloop
}
/* Владелец отменяет pending completion после join производителя вне callbacks. */
/* В потоке loop удаляем ожидающую задачу из общей очереди или ещё не исполненной части любой пачки. */
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task) {
#ifdef _WIN32
EnterCriticalSection(&ua->posted_lock);
@@ -1866,8 +1872,20 @@ err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task) {
#else
pthread_mutex_unlock(&ua->posted_lock);
#endif
if (found) u_free(task);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion is not pending");
const char* source = "queue";
for (struct uasync_post_batch* batch = ua->posted_batches; !found && batch; batch = batch->previous) {
p = &batch->head;
while (*p && *p != task) p = &(*p)->next;
if (*p) {
*p = task->next;
found = 1;
source = "batch";
}
}
if (found) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync_cancel_post: cancelled task=%p source=%s", (void*)task, source);
u_free(task);
} else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync_cancel_post: task=%p running or absent", (void*)task);
return found ? ERR_OK : ERR_FAIL;
}
+5 -2
View File
@@ -39,6 +39,7 @@ typedef void (*uasync_post_callback_t)(void* user_arg);
#include "memory_pool.h"
struct timeout_node; // Forward declaration
struct uasync_post_batch;
/* CPU одного потока: 100% = одно ядро. При работе цикла/запросе снимок обновляется раз в 100 мс, без пробуждений.
* До первого замера valid=0; window_us растёт до секунды. При задержке замера
@@ -87,6 +88,7 @@ struct UASYNC {
int use_epoll; // 1 = epoll; 0 = poll на POSIX или select на Windows
struct posted_task* posted_tasks_head;
struct posted_task* posted_tasks_tail;
struct uasync_post_batch* posted_batches; // Ожидающие задачи вложенных dispatch; только поток loop
#ifdef _WIN32
CRITICAL_SECTION posted_lock;
@@ -216,8 +218,9 @@ void uasync_post(struct UASYNC* ua, uasync_post_callback_t callback, void* user_
* uasync освобождает task после callback, успешной отмены или при destroy без callback; arg не освобождает.
* Публикация не проверяет running/stop. Ошибка wakeup не удаляет уже поставленную задачу и не возвращается. */
void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task);
// Отмена в потоке владельца вне callbacks после завершения производителя; ua/task не NULL.
// ERR_OK — task найден в очереди и освобождён; ERR_FAIL — не найден (в том числе уже взят на исполнение).
// Отмена в потоке loop после завершения производителя; допустима из callback, ua/task не NULL.
// ERR_OK — ожидающий task удалён и освобождён, в том числе из исполняемой пачки; arg остаётся владельцу.
// ERR_FAIL — task отсутствует или его callback уже начат; исполняющийся task отменить нельзя.
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task);
// Lock/unlock posted_lock у живого ua; не ждёт исполнения задач. post уже публикует данные под этим мьютексом.
+7 -4
View File
@@ -106,9 +106,12 @@ void thread_func(UTUN_INSTANCE* u) {
Задачу освобождает uasync после исполнения, успешной отмены или при уничтожении без вызова callback; `arg` не освобождается.
Ошибка wakeup оставляет уже опубликованную задачу в очереди; обе функции публикации не возвращают эту ошибку.
Отмена через `uasync_cancel_post` выполняется в потоке владельца вне callbacks после завершения производителя.
`ERR_OK` означает, что задача найдена в ожидающей очереди и освобождена; `ERR_FAIL` — не найдена.
Порция, уже взятая на исполнение, в этой очереди не находится. `uasync_memsync` лишь захватывает и отпускает
Отмена через `uasync_cancel_post` выполняется в потоке loop после завершения публикации производителем;
вызов из другого callback допустим. `ERR_OK` означает, что ожидающий task удалён и освобождён:
поиск охватывает общую очередь и ещё не исполненную часть всех активных batch, включая вложенные poll.
После успешной отмены task недействителен; arg остаётся владельцу, его можно освободить.
`ERR_FAIL` означает отсутствие task либо уже начавшийся callback; текущий callback не отменяется.
Отдельное обнуление callback/arg для ожидающих batch не требуется. `uasync_memsync` лишь захватывает и отпускает
`posted_lock`; он не ждёт исполнения задач и не заменяет согласование времени жизни контекста.
### Получение времени
@@ -183,7 +186,7 @@ Handle кодирует индекс без поколения и сохраня
|---------|----------|
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Выделить и опубликовать задачу для потока экземпляра, попытаться разбудить его. При OOM задача не публикуется, ошибка логируется; статуса возврата нет. |
| `uasync_post_reserved(ua, task)` | **Потокобезопасно.** Передать заранее выделенный через `u_malloc` task с заполненными callback/arg без новых аллокаций. `ua` и `task` обязательны. Владение task переходит uasync. |
| `uasync_cancel_post(ua, task)` | В потоке владельца вне callbacks после завершения производителя: `ERR_OK` при удалении и освобождении ожидающего task, `ERR_FAIL`, если он не найден. `ua` и `task` обязательны. |
| `uasync_cancel_post(ua, task)` | В потоке loop после публикации, допустим из другого callback. `ERR_OK` удаляет и освобождает ожидающий task из очереди или активного batch; `ERR_FAIL` — task отсутствует либо callback уже начат. Arg остаётся владельцу; `ua` и `task` обязательны. |
| `uasync_memsync(ua)` | Lock/unlock `posted_lock` живого экземпляра. Не ждёт исполнения задач; публикация через post уже использует этот мьютекс. |
| `uasync_wakeup(ua)` | 0 при успешной записи, -1 при ошибке/неинициализированном wakeup. POSIX: запись `uint64_t(1)` в eventfd/pipe; Windows: send одного байта в UDP-сокет. Экземпляр должен оставаться живым. |
| `uasync_get_wakeup_fd(ua)` | Заимствованный endpoint записи, либо -1 при `NULL`/неинициализированном wakeup. Linux: eventfd, тот же fd для чтения и записи (писать `uint64_t`); прочие POSIX: pipe; Windows: UDP-сокет. Самостоятельно не закрывать. |
+13 -2
View File
@@ -2,7 +2,7 @@ bin_PROGRAMS = utun
noinst_LIBRARIES = libutun.a
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3 routing_layer/address_profile.h routing_layer/route_ping_doc.md \
stream/stream_playback_audio.h stream/stream_playback_audio.cpp config_ini.h
stream/stream_playback_audio.h stream/stream_playback_audio.cpp config_ini.h config_error.h
stream_core_files = \
stream/stream.h \
@@ -44,6 +44,7 @@ utun_CORE_SOURCES = \
utun.c \
utun_instance.c \
config_parser.c \
config_error.c \
config_updater.c \
routing_layer/route_lib.c \
routing_layer/route6_lib.c \
@@ -82,6 +83,7 @@ utun_CORE_SOURCES = \
transport_layer/etcp_tx.c \
transport_layer/etcp_tx.h \
transport_layer/etcp_loadbalancer.c \
transport_layer/etcp_link_io.c \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
@@ -185,6 +187,8 @@ utun_CORE_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/merkle_tree.c \
@@ -192,6 +196,7 @@ utun_CORE_SOURCES = \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
chat/chat_whisper.h \
dm/dm_core.c \
dm/dm_history.c \
dm/dm_history.h \
@@ -234,6 +239,7 @@ libutun_a_SOURCES = \
$(stream_core_files) \
utun_instance.c \
config_parser.c \
config_error.c \
config_updater.c \
routing_layer/route_lib.c \
routing_layer/route6_lib.c \
@@ -270,6 +276,7 @@ libutun_a_SOURCES = \
transport_layer/etcp_tx.c \
transport_layer/etcp_tx.h \
transport_layer/etcp_loadbalancer.c \
transport_layer/etcp_link_io.c \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
transport_layer/secure_channel.c \
@@ -373,6 +380,8 @@ libutun_a_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/invite_link.c \
@@ -380,6 +389,8 @@ libutun_a_SOURCES = \
chat/merkle_tree.h \
chat/invite_build.c \
chat/chat_headless_control.c \
chat/chat_whisper.c \
chat/chat_whisper.h \
dm/dm_core.c \
dm/dm_history.c \
dm/dm_history.h \
@@ -414,7 +425,7 @@ libutun_a_CFLAGS = $(utun_CFLAGS)
# Platform-specific TUN libs (Windows only)
utun_TUN_LIBS = @TUN_LIBS@
utun_SOURCES = utun.c chat/chat_whisper.c
utun_SOURCES = utun.c
# Include paths
utun_CFLAGS = \
+32 -11
View File
@@ -503,8 +503,8 @@ static void call_post_stats(struct call_session* s) {
int buffer_ms = 0, tempo_x100 = 100;
struct call_jitter_range range = {0, 0, 60};
uint32_t dropped = 0, underruns = 0;
if (ctx->audio_ops && ctx->audio_ops->get_stats)
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range);
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
if (audio && audio->get_stats) audio->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range);
uint16_t rtt_tb = 0;
struct ETCP_ROUTER_CONN* rconn =
@@ -783,9 +783,9 @@ static void call_handle_media(struct call_ctx* ctx, uint64_t from, const uint8_t
memcpy(&ts_ms, b + 12, 4);
/* аудио-движок (декод + джиттер-буфер) — если зарегистрирован */
if (ctx->audio_ops && ctx->audio_ops->on_media)
ctx->audio_ops->on_media(ctx->inst, call_id, seq, ts_ms,
b + CALL_MEDIA_HDR_SIZE, (int)(n - CALL_MEDIA_HDR_SIZE), &timing);
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
if (audio && audio->on_media)
audio->on_media(ctx->inst, call_id, seq, ts_ms, b + CALL_MEDIA_HDR_SIZE, (int)(n - CALL_MEDIA_HDR_SIZE), &timing);
/* релей Opus-кадра потребителю (headless-аудио-сокет) */
if (ctx->media_recv_fn)
@@ -964,7 +964,8 @@ int call_busy(struct UTUN_INSTANCE* inst) {
return 0;
}
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id) {
/* Только uasync: проверка доступности перед созданием сессии; очереди групп не межпоточные. */
static int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id) {
if (!inst || !ch_id || !ch_id[0] || !inst->topo_groups) return 0;
if (peer_node_id == inst->node_id) return 1;
uint64_t group_id = strtoull(ch_id, NULL, 10);
@@ -981,9 +982,11 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
}
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops) {
struct call_ctx* ctx = call_of(inst);
if (!ctx) return;
ctx->audio_ops = ops;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call audio registration without instance"); return; }
if (inst->call_audio_ops == ops) return;
inst->call_audio_ops = ops;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call audio frontend %s inst=%p chat=%d",
ops ? "attached" : "detached", (void*)inst, inst->chat_started);
}
/* ─────────────────── TX медиа: tx_q + backpressure ─────────────────── */
@@ -1075,14 +1078,32 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id) {
struct call_ctx* ctx = call_of(inst);
if (!ctx || !ctx->initialized || !ch_id || !ch_id[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: call_start bad args (ctx=%p ch_id=%s)", CALL_ID, (void*)ctx, ch_id ? ch_id : "(null)");
if (!ctx || !ctx->initialized || !ch_id || !ch_id[0] || !peer_node_id || !out_call_id) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: call_start bad args ctx=%p ch=%s peer=%016llx id=%016llx", CALL_ID,
(void*)ctx, ch_id ? ch_id : "(null)", (unsigned long long)peer_node_id, (unsigned long long)out_call_id);
return;
}
struct call_session* existing = call_session_find(ctx, out_call_id);
if (existing) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call start ID already registered id=%016llx state=%s peer=%016llx requested_peer=%016llx ch=%s",
(unsigned long long)out_call_id, call_state_name(existing->state), (unsigned long long)existing->peer_node_id,
(unsigned long long)peer_node_id, ch_id);
return;
}
if (peer_node_id == inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: call_start self-call rejected", CALL_ID);
return;
}
/* Как для входящего INVITE, ENDED остаётся только для подтверждения завершения и не занимает линию. */
for (struct ll_entry* e = ctx->sessions->head; e; e = e->next) {
struct call_session* busy = (struct call_session*)e->data;
if (busy->state == CALL_ST_ENDED) continue;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call start rejected id=%016llx ch=%s peer=%016llx busy_id=%016llx state=%s busy_peer=%016llx",
(unsigned long long)out_call_id, ch_id, (unsigned long long)peer_node_id,
(unsigned long long)busy->call_id, call_state_name(busy->state), (unsigned long long)busy->peer_node_id);
call_event_id(ctx, CHAT_EVT_CALL_ENDED, out_call_id, CALL_REASON_BUSY);
return;
}
if (radio_call_busy(inst)) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call start rejected: pending radio invitation");
call_event_id(ctx, CHAT_EVT_CALL_ENDED, out_call_id, CALL_REASON_BUSY);
+43 -26
View File
@@ -25,27 +25,37 @@ extern "C" {
// ── жизненный цикл ──
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */
/* Зарегистрировать сигналинг/медиа в роутере и создать реестр сессий после dm_core_init.
* inst/ua/роутер должны существовать. 0 = успех (повтор идемпотентен), -1 = ошибка с откатом. */
int call_init(struct UTUN_INSTANCE* inst);
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
/* Остановить сессии, отменить таймеры/CM/waiters и снять сервисы роутера перед dm_core_destroy.
* Сначала закрыть call_headless. Без отправки завершающих команд и событий ENDED; NULL/повтор — no-op.
* Регистрация call_audio_ops у ядра сохраняется, DSP и устройства frontend здесь не освобождаются. */
void call_destroy(struct UTUN_INSTANCE* inst);
// ── сигналинг (uasync-поток) ──
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
* out_call_id — случайный uint64, генерит вызывающий. */
* out_call_id — ненулевой уникальный uint64, генерит вызывающий.
* При любом незавершённом обычном звонке/приглашении рации новый ID получает ENDED/BUSY без создания сессии.
* Уже зарегистрированный ID, включая ENDED до очистки, отклоняется без событий и изменения текущего звонка.
* После ENDED новый уникальный ID разрешён; результат запуска приходит через CHAT_EVT_CALL_*. */
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
// Callee: ответить/отклонить. hangup — любая сторона в любой момент.
// Callee: принять только RINGING; неизвестный ID или неподходящее состояние — no-op.
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id);
// Callee: отклонить существующую незавершённую сессию, события DECLINED и ENDED с причиной DECLINE.
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
// Любая сторона: завершить существующую сессию; неизвестный ID/ENDED — no-op.
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id);
// Проверка занятости обычным звонком; только uasync, включая входящий/исходящий.
// 1 = есть обычная сессия не в ENDED, включая WAIT_DECISION; 0 = свободен/модуль не запущен. Только uasync.
int call_busy(struct UTUN_INSTANCE* inst);
// Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync.
// Caller в WAIT_DECISION: подтвердить force и повторить INVITE с прежним ID/deadline. Только uasync.
void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id);
// Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости.
@@ -61,13 +71,12 @@ void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy
* Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */
uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
// Read-only онлайн-проверка (GUI может звать из любого потока).
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
// ── медиа (релей Opus-кадров, uasync-поток) ──
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */
* Только ACTIVE: len=1..988, opus/timing копируются до возврата; timing=NULL — без capture PTS.
* seq/ts выставляются автоматически. 0 = принят в локальную FIFO, -1 = неверные данные/нет ACTIVE/ошибка памяти.
* Результат не подтверждает доставку: FIFO750 удаляет старейший кадр при заполнении, роутер может отказать при drain. */
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
const struct call_capture_time* timing);
@@ -76,6 +85,8 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id,
const uint8_t* opus, int len, void* arg);
/* Один callback в текущем call_ctx, fn=NULL снимает; arg заимствован до замены/destroy.
* Требует call_init; stop/start чата сбрасывает этот callback. Независим от сохраняемого у ядра audio_ops. */
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg);
/* ── аудио-движок (опционально, GUI-сборки) ──
@@ -84,38 +95,44 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
struct call_audio_ops {
// uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер).
// uasync-поток: принятый Opus-кадр; opus/timing заимствованы до возврата. Декодирование определяет frontend.
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
* dropped/underruns — опционально, NULL допустим). */
/* uasync-поток: метрики (buffer_ms, tempo_x100=100 для 1x, накопительные dropped/underruns, range).
* Выходы необязательны. 0 = снимок получен, -1 = нет активного DSP; реализация может потреблять события диагностики. */
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
};
/* Только uasync: сохранить заимствованный ops у ядра (NULL снимает регистрацию), допустимо до запуска чата.
* Объект живёт до снятия/уничтожения ядра. Stop/start чата сохраняет регистрацию, состояние звонка — нет. */
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
// Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline.
/* Аргумент call_send_media_trampoline: heap-объект из u_malloc/u_calloc, callback освобождает его через u_free.
* Постить в inst->ua; inst должен жить до выполнения. Не заменяет эпохи/лимит TX в call_audio.h.
* len=1..256; timing передаётся в call_send_media как есть (без PTS задать timing.pts_us=INT64_MIN). */
struct call_media_arg {
struct UTUN_INSTANCE* inst;
uint64_t call_id;
uint16_t len;
struct call_capture_time timing;
uint8_t opus[256];
struct UTUN_INSTANCE* inst; // Заимствованное ядро, живёт до callback.
uint64_t call_id; // ID ACTIVE-сессии на момент выполнения callback.
uint16_t len; // Число заполненных байтов opus, 1..256.
struct call_capture_time timing; // Capture PTS/epoch/codec delay, копия метаданных кадра.
uint8_t opus[256]; // Встроенная копия закодированного кадра.
};
void call_send_media_trampoline(void* arg);
// ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ──
/* Trampoline выполняются в uasync, вызывают одноимённый API и освобождают arg через u_free.
* arg — соответствующий ненулевой heap-объект, inst живёт до callback. Постановка не удерживает ядро.
* call_start_arg.ch_id должен содержать завершающий NUL; остальные действия используют call_id_arg. */
struct call_start_arg {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t peer_node_id;
uint64_t call_id;
struct UTUN_INSTANCE* inst; // Заимствованное ядро, живёт до callback.
char ch_id[64]; // Десятичный ID группы либо dm:<conv_id>, с завершающим NUL.
uint64_t peer_node_id; // Ненулевой ID собеседника, не собственного узла.
uint64_t call_id; // Новый уникальный ненулевой ID, генерирует вызывающий.
};
struct call_id_arg {
struct UTUN_INSTANCE* inst;
uint64_t call_id;
struct UTUN_INSTANCE* inst; // Заимствованное ядро, живёт до callback.
uint64_t call_id; // ID сессии для accept/decline/hangup/continue.
};
void call_start_trampoline (void* arg);
+45 -16
View File
@@ -48,6 +48,7 @@ static int g_prepared_capture = 0;
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static uint64_t g_tx_generation;
static uint64_t g_instance_generation; /* Эпоха привязки; смена звонка её не меняет. */
static uint64_t g_capture_epoch;
static int64_t g_capture_end = INT64_MIN;
static int g_codec_delay;
@@ -69,6 +70,7 @@ struct call_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t call_id, generation, created_tb;
uint64_t instance_generation;
int len;
struct call_capture_time timing;
uint8_t opus[256];
@@ -77,9 +79,20 @@ struct call_audio_tx {
static void call_audio_send(void* arg) {
struct call_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
int owner = tx->instance_generation == g_instance_generation && g_inst == tx->inst;
int current = owner && tx->generation == g_tx_generation;
if (current) call_audio_duration_add(&g_post_time, (get_time_tb() - tx->created_tb) * 100);
if (g_tx_pending) --g_tx_pending;
/* Старый звонок той же привязки всё ещё занимает свой слот очереди. */
if (owner) {
if (g_tx_pending) --g_tx_pending;
else DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: TX pending underflow call=%llu instance_epoch=%llu", CALL_AUDIO_ID,
(unsigned long long)tx->call_id, (unsigned long long)tx->instance_generation);
}
if (!current) DEBUG_TRACE(DEBUG_CATEGORY_CALL,
"%s: stale TX call=%llu owner=%d instance_epoch=%llu/%llu call_epoch=%llu/%llu pending=%u", CALL_AUDIO_ID,
(unsigned long long)tx->call_id, owner, (unsigned long long)tx->instance_generation,
(unsigned long long)g_instance_generation, (unsigned long long)tx->generation,
(unsigned long long)g_tx_generation, g_tx_pending);
int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id;
if (get_time_tb() - tx->created_tb > 2000) {
send = 0;
@@ -104,33 +117,41 @@ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t*
}
int call_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: init missing instance", CALL_AUDIO_ID); return -1; }
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
pthread_mutex_unlock(&g_mtx);
if (g_inst != inst) {
unsigned int previous_pending = g_tx_pending;
g_inst = inst; g_tx_pending = 0;
++g_instance_generation;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized inst=%p instance_epoch=%llu previous_pending=%u", CALL_AUDIO_ID,
(void*)inst, (unsigned long long)g_instance_generation, previous_pending);
}
call_set_audio_ops(inst, &g_ops);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID);
pthread_mutex_unlock(&g_mtx);
return 0;
}
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
call_set_audio_ops(inst, NULL);
if (!inst) return;
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
if (inst->call_audio_ops == &g_ops) call_set_audio_ops(inst, NULL);
if (g_inst == inst) {
g_inst = NULL;
++g_instance_generation;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed inst=%p instance_epoch=%llu pending=%u", CALL_AUDIO_ID,
(void*)inst, (unsigned long long)g_instance_generation, g_tx_pending);
}
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
}
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing) {
(void)inst; (void)seq;
(void)seq;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
if (inst == g_inst && g_active && call_id == g_call_id && g_vj && opus && len > 0) {
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing);
}
pthread_mutex_unlock(&g_mtx);
@@ -145,7 +166,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
}
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
if (g_inst && g_active && g_call_id == call_id) {
int same_mode = g_prepared_capture == prepared;
pthread_mutex_unlock(&g_mtx);
if (!same_mode) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy cannot change within active call", CALL_AUDIO_ID);
@@ -156,6 +177,12 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
call_audio_release(g_call_id);
pthread_mutex_lock(&g_mtx);
}
if (!g_inst) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: start rejected without attached core call=%llu", CALL_AUDIO_ID,
(unsigned long long)call_id);
return -1;
}
opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1);
if (!enc) {
@@ -212,6 +239,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
/* компрессор микрофона (AGC) — как в рации: выравнивает тихий проводной микрофон
* (HAL не применяет AGC на проводном входе). lookahead=0 — без задержки. */
int compressor_gain = chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15);
struct audio_compressor* ac = audio_compressor_create();
if (ac) {
audio_compressor_config_t accfg = {0};
@@ -220,7 +248,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
accfg.block_duration_ms = CALL_AUDIO_FRAME_MS;
accfg.lookback_ms = 200;
accfg.lookahead_ms = 0;
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15);
accfg.max_gain_db = (float)compressor_gain;
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2);
accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; }
@@ -252,7 +280,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
(unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable",
chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15));
compressor_gain);
return 0;
}
@@ -452,6 +480,7 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count,
}
task->inst = inst; task->call_id = call_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
task->instance_generation = g_instance_generation;
task->timing = (struct call_capture_time){capture_pts_us, g_capture_epoch, (uint32_t)g_codec_delay};
task->len = len;
memcpy(task->opus, opus, (size_t)len);
+38 -16
View File
@@ -1,7 +1,8 @@
#ifndef CALL_AUDIO_H
#define CALL_AUDIO_H
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
// call_audio.h — общий аудио-движок P2P-звонка для frontend с захватом/воспроизведением (desktop/Android).
// TCP-сервис готового PCM имеет собственные кодеки и описан отдельно в call_headless.h.
//
// Владеет Opus codec, адаптивным джиттер-буфером (call_jitter: кодированные
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
@@ -11,12 +12,13 @@
// call_audio_feed_pcm.
//
// Потоки:
// - call_audio_init / destroy — uasync-поток, lifecycle ядра;
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
// - call_audio_feed_pcm / pull_pcm — по одному аудио-потоку на направление;
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
// decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк),
// call_jitter_push — только uasync-поток.
// Mutex защищает привязку, TX и снимки общих указателей. Encoder используется capture worker,
// decoder — playback worker (из call_jitter_pull через callback), call_jitter_push — uasync.
// Pull использует снимок указателей вне mutex: перед release/сменой ID владелец останавливает оба I/O-направления.
#include <stdint.h>
#include "call_jitter.h"
@@ -31,17 +33,28 @@ extern "C" {
#define CALL_AUDIO_FRAME_MS 20
#define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) /* 960 */
/* Зарегистрировать аудио-движок в call.c (идемпотентно). */
/* uasync-поток: привязать общий для процесса движок к inst, допустимо до запуска чата.
* Регистрация принадлежит ядру и сохраняется при stop/start чата: повторный init не требуется.
* Повтор для того же inst идемпотентен: не меняет эпоху владельца и занятые TX-слоты.
* Перед освобождением inst/ua остановить I/O, снять привязку через destroy в uasync.
* Лимит8 TX-задач относится к текущей привязке, возраст до отправки в call.c ограничен200мс.
* Задачи прежних привязок не отправляют медиа и не меняют
* её счётчик/позднюю статистику; uasync освобождает их после callback либо при destroy. 0 = успех, -1 = нет inst. */
int call_audio_init(struct UTUN_INSTANCE* inst);
/* uasync-поток: снять регистрацию inst; текущий владелец также инвалидирует TX-привязку.
* Поздний detach прежнего inst не затрагивает нового владельца. Codec/jitter освобождаются отдельно через stop/release. */
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
* Для desktop нужен start_prepared: AEC и stereo reference принадлежат VoiceAudioIo. 0 = успех. */
* Нужен привязанный через init живой inst; без него -1. Для desktop нужен start_prepared:
* AEC и stereo reference принадлежат VoiceAudioIo. Повтор активного ID сохраняет DSP;
* смена raw/prepared для него отклоняется. До смены ID остановить оба I/O-направления. 0 = успех, -1 = ошибка. */
int call_audio_start(uint64_t call_id);
/* AEC принадлежит платформенному захвату; pull больше не подаёт собственный референс. */
/* Контракт start, но AEC принадлежит платформенному захвату; pull не подаёт собственный референс. */
int call_audio_start_prepared(uint64_t call_id);
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */
/* GUI-поток: по CALL_ENDED прекратить TX, pull_pcm выдаст450мс тона, затем тишину.
* Сам движок не освобождается: frontend отдельно останавливает I/O и вызывает release. */
void call_audio_begin_end(uint64_t call_id);
/* GUI-поток (после остановки аудиоустройства): освободить все объекты. */
@@ -50,19 +63,24 @@ void call_audio_release(uint64_t call_id);
/* GUI-поток: остановить текущий активный звонок (удобная обёртка release). */
void call_audio_stop(void);
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
/* Capture worker: ровно960 raw-сэмплов; capture_pts_us — монотонная метка первого либо INT64_MIN.
* Encoder работает синхронно, PCM не сохраняется; отличие известного PTS от ожидаемого более2мс меняет capture_epoch.
* 0 = Opus поставлен в uasync, -1 = ошибка/отказ очереди; постановка не подтверждает доставку пиру.
* Для mute использовать feed_pcm_muted с настоящим микрофоном, не занулять raw-вход до AEC/AGC. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
/* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */
/* Подготовленный PCM и маска mute (960 элементов); AEC уже выполнен до этого API. */
/* Подготовленный PCM и обязательная маска mute (960 элементов); AEC уже выполнен до этого API.
* Маска применяется после core AGC, результат/PTS — как у feed_pcm. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
/* Raw capture: AEC/AGC получают настоящий PCM; muted!=0 зануляет только вход Opus после обработки.
* Android передаёт исходный кадр и snapshot mute через JNI; результат/PTS — как у feed_pcm. */
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted, int64_t capture_pts_us);
/* аудио-поток: выдать max_samples финального PCM (jitter + stretch + тоны + тишина).
* Активный движок всегда выдаёт полный блок; 0 означает отсутствие движка/ошибку аргументов. */
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (call_jitter_push,
* декодирование произойдёт на pull в аудио-потоке). */
/* uasync-поток (из call.c): кадр текущего inst/активного DSP передаётся в call_jitter_push;
* данные копируются, декодирование произойдёт на pull в аудио-потоке. Чужой inst/ID или отсутствие DSP — no-op. */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
@@ -77,14 +95,18 @@ int call_audio_get_clock(uint64_t call_id, int64_t now_us, struct call_clock_sna
int call_audio_try_clock(uint64_t call_id, int64_t now_us, struct call_clock_snapshot* out);
/* Playback worker: старый аппаратный якорь недействителен до progress нового выхода. PCM/JB сохраняются. */
void call_audio_suspend_device(uint64_t call_id, uint64_t epoch);
/* Любой поток: диагностический снимок без потребления событий CALL_STATS. */
/* Любой поток: диагностический снимок без потребления событий CALL_STATS.
* out обязателен; 0 = снимок получен, -1 = нет DSP/выхода. При отсутствии DSP out обнуляется. */
int call_audio_get_diagnostics(uint64_t call_id, struct call_jitter_diagnostics* out);
/* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */
/* uasync-поток: метрики для CALL_STATS; все выходы необязательны, 0 = успех, -1 = нет DSP (выходы не меняются).
* dropped/underruns накопительные; диагностические события и интервальные TX-измерения потребляются/логируются.
* Для чтения без потребления событий использовать get_diagnostics. */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
/* После остановки указанного направления: сбросить AEC, для playback также устаревший jitter/PCM.
/* После остановки указанного направления: сбросить AEC; capture также сбрасывает AGC и увеличивает capture_epoch.
* Playback сбрасывает устаревший jitter/PCM по call_jitter_reset (последний запас Opus сохраняется) и playback clock epoch.
* Capture и playback могут работать в разных потоках, по одному writer на направление.
* Release допустим только после остановки обоих. */
void call_audio_reset_io(uint64_t call_id, int playback);
+10 -8
View File
@@ -1,5 +1,6 @@
/* Снимок часов входящего аудио. PTS принадлежит шкале захвата удалённой стороны.
* epoch — локальная эпоха устройства; source_epoch — поколение удалённого encoder.
* epoch — локальная эпоха playback-карты: меняется при reset, а не при каждом переоткрытии устройства.
* source_epoch — поколение захвата отправителя в текущем PCM-участке.
* valid=0 запрещает продвижение видео по этому снимку. measured отличает аппаратную
* отметку от оценки backend. Тишина устройства/тоны удерживают PTS, remote mute — нет. */
#ifndef CALL_CLOCK_H
@@ -8,15 +9,16 @@
/* pts_us — первый захваченный сэмпл; INT64_MIN означает отсутствие метки.
* delay_samples — задержка Opus от encoder. epoch меняется при разрыве захвата. */
struct call_capture_time {
int64_t pts_us;
uint64_t epoch;
uint32_t delay_samples;
int64_t pts_us; // Первый сэмпл PCM до кодирования, монотонные мкс отправителя; INT64_MIN — неизвестно.
uint64_t epoch; // Ненулевое поколение захвата внутри звонка; 0 — источник без достоверной шкалы.
uint32_t delay_samples; // Задержка encoder в сэмплах 48кГц; вычитается при построении шкалы decoder.
};
struct call_clock_snapshot {
uint64_t epoch, source_epoch, played_samples, written_samples;
int64_t pts_us;
uint32_t held_back_us;
uint64_t epoch, source_epoch, played_samples, written_samples; // Поколения карты/источника и позиции PCM, см. ниже.
// played_samples — оценка выведенной позиции; written_samples — весь PCM pull/silence, включая тоны.
int64_t pts_us; // PTS удалённого захвата; -1 до первого валидного снимка, при valid=0 может быть прежним.
uint32_t held_back_us; // Насколько текущий PTS удержан от регрессии (допускается до 100мс).
uint32_t rate_ppm; // Фактический шаг source PTS в текущем PCM-участке; 1000000 = 1x, 0 = удержание.
int valid, measured;
int valid, measured; // valid разрешает использовать PTS; measured описывает последний принятый аппаратный якорь.
};
#endif
+24 -18
View File
@@ -22,43 +22,49 @@
class CallDelay {
public:
using Input = unsigned (*)(void*, int16_t*, unsigned);
enum class Mode { Direct, Stretch, Drain };
enum class Mode { Direct, Stretch, Drain }; // Прямая выдача, подача в SoundTouch, выдача готового DSP-хвоста.
struct Stats {
Mode mode = Mode::Direct;
double tempo = 1, pending = 0; // Заданный темп и ещё не выданная длительность исходного PCM в сэмплах.
uint64_t cuts = 0, entered = 0, exited = 0;
uint64_t cuts = 0, entered = 0, exited = 0; // Удалённые Direct-сэмплы и переходы в/из SoundTouch с последнего reset.
unsigned phase = 0; // В Direct: ошибка относительно 20мс выхода; 0 — границы совпадают.
};
// Оба callback обязательны; контексты заимствованы до уничтожения. После создания проверить valid().
CallDelay(Input input, void* input_context, soundtouch::SourceCallback source, void* source_context);
CallDelay(const CallDelay&) = delete;
CallDelay& operator=(const CallDelay&) = delete;
// Ошибка обработки необратима для объекта; reset не восстанавливает valid, нужен новый экземпляр.
bool valid() const { return !failed; }
// Выдать 0..count PCM-сэмплов, без дополнения тишиной; queued не включает уже принятый этим объектом PCM.
// output/count должны быть валидны; нехватка input даёт короткую выдачу, ошибка делает valid()==false.
unsigned pull(int16_t* output, unsigned count, uint64_t queued, unsigned reserve);
// Удалить PCM/DSP-хвост, счётчики и фазу; внешнюю PTS-карту владелец сбрасывает отдельно.
void reset();
void silence(unsigned count); // Тишина устройства тоже сдвигает выходную фазу.
// Снимок единственного worker; pending — исходные сэмплы, ещё не выданные, не размер готового DSP-выхода.
Stats stats() const;
private:
static constexpr unsigned Rate = 48000, Frame = 960, Capacity = 8192, Context = 16;
Input input;
Input input; // Заимствованный reader исходного PCM.
void* inputContext;
soundtouch::SourceCallback source;
soundtouch::SourceCallback source; // Заимствованный получатель происхождения выходного PCM.
void* sourceContext;
soundtouch::SoundTouch st;
std::array<float, Capacity> raw{};
soundtouch::SoundTouch st; // DSP только режима Stretch; готовый выход дочитывается в Drain.
std::array<float, Capacity> raw{}; // Кольцо исходного PCM, включая контекст перед границей Direct/Stretch.
struct Span { soundtouch::SourceSpan value{soundtouch::SourceKind::Copy}; };
std::array<Span, 32> spans{};
unsigned spanHead = 0, spanCount = 0;
uint64_t written = 0, cursor = 0, generated = 0, consumed = 0;
uint64_t stBase = 0, stFed = 0, entry = 0, queuedInput = 0;
uint64_t deviceSamples = 0;
int64_t phaseGoal = 0;
double playedSource = 0, tempo = 1, cutPhase = 0;
struct Bucket { uint64_t time = UINT64_MAX; double low = 0, inventory = 0; } buckets[4];
unsigned trim = 0;
bool failed = false, firstSpan = false, lastCopy = false;
Mode mode = Mode::Direct;
uint64_t cuts = 0, entered = 0, exited = 0;
std::array<Span, 32> spans{}; // Произведение DSP/Direct, ещё не учтённое consume.
unsigned spanHead = 0, spanCount = 0; // Начало/число действующих spans.
uint64_t written = 0, cursor = 0, generated = 0, consumed = 0; // Принятый вход, позиция raw, произведённый/выданный выход.
uint64_t stBase = 0, stFed = 0, entry = 0, queuedInput = 0; // Начало DSP-шкалы, переданный вход, граница входа, внешний FIFO.
uint64_t deviceSamples = 0; // Выданный PCM плюс объявленная локальная тишина.
int64_t phaseGoal = 0; // Целевая разность cursor-deviceSamples, кратная Frame.
double playedSource = 0, tempo = 1, cutPhase = 0; // Позиция источника на выходе, темп, дробный долг удаления Direct.
struct Bucket { uint64_t time = UINT64_MAX; double low = 0, inventory = 0; } buckets[4]; // Четыре интервала по 50мс.
unsigned trim = 0; // Уже выданный Direct-контекст, который нужно убрать с начала SoundTouch.
bool failed = false, firstSpan = false, lastCopy = false; // Необратимая ошибка; ожидаем первый DSP-span; хвост — Copy.
Mode mode = Mode::Direct; // Текущий способ выдачи PCM.
uint64_t cuts = 0, entered = 0, exited = 0; // Счётчики Direct-удалений и переходов режима с последнего reset.
bool fill();
bool pump();
+42 -15
View File
@@ -42,6 +42,8 @@ struct call_audio_client {
uint8_t send_buf[AUDIO_SEND_BUF_SIZE];
int send_len;
int send_offset;
uint32_t pcm_dropped; /* Целые исходящие PCM-кадры, отброшенные при переполнении. */
uint64_t drop_log_tb; /* Ограничение частоты WARN: не чаще раза в секунду. */
int closing;
struct call_audio* ca;
struct call_audio_client* next;
@@ -66,6 +68,7 @@ struct call_audio {
uint32_t c_dec_frames;
uint32_t c_enc_dropped;
uint32_t c_dec_dropped;
uint32_t c_pcm_dropped;
};
static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) {
@@ -76,12 +79,24 @@ static void ca_close_client(struct call_audio_client* cli);
/* ── отправка фрейма клиенту (с буферизацией как в control-сокете) ── */
/* Принимает целый ограниченный фрейм; short write сохраняет его остаток перед следующими кадрами. */
static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size_t len) {
struct call_audio* ca = cli->ca;
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return;
struct call_audio* ca = cli->ca;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
if (len > space) return;
if (len > space) {
++cli->pcm_dropped; ++ca->c_pcm_dropped;
uint64_t now = get_time_tb();
if (cli->pcm_dropped == 1 || now - cli->drop_log_tb >= 10000) {
cli->drop_log_tb = now;
DEBUG_WARN(DEBUG_CATEGORY_CALL,
"%s: PCM frame dropped call=%016llx fd=%d frame=%zu queued=%d offset=%d capacity=%zu dropped=%u total=%u",
CH_ID, (unsigned long long)cli->call_id, (int)cli->fd, len, cli->send_len, cli->send_offset,
sizeof(cli->send_buf), cli->pcm_dropped, ca->c_pcm_dropped);
}
return;
}
memcpy(cli->send_buf + cli->send_len, data, len);
cli->send_len += (int)len;
return;
@@ -103,15 +118,21 @@ static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size
}
}
/* Собрать header+PCM до обращения к очереди, чтобы overflow не оставил часть фрейма. */
static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t call_id,
const uint8_t* data, uint16_t len) {
uint8_t hdr[CALL_AUDIO_HDR_SIZE];
hdr[0] = type;
memcpy(hdr + 1, &call_id, 8);
hdr[9] = (uint8_t)(len >> 8);
hdr[10] = (uint8_t)(len & 0xFF);
ca_send_raw(cli, hdr, sizeof(hdr));
if (len) ca_send_raw(cli, data, len);
if (len > CA_PCM_FRAME_BYTES || (len && !data)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid output frame call=%016llx type=%u len=%u data=%p",
CH_ID, (unsigned long long)call_id, type, len, (const void*)data);
return;
}
uint8_t frame[CALL_AUDIO_HDR_SIZE + CA_PCM_FRAME_BYTES];
frame[0] = type;
memcpy(frame + 1, &call_id, 8);
frame[9] = (uint8_t)(len >> 8);
frame[10] = (uint8_t)(len & 0xFF);
if (len) memcpy(frame + CALL_AUDIO_HDR_SIZE, data, len);
ca_send_raw(cli, frame, CALL_AUDIO_HDR_SIZE + len);
}
/* ── Opus-кодеки (одна активная звонка за раз) ── */
@@ -198,8 +219,9 @@ static void ca_parse_frames(struct call_audio_client* cli) {
CH_ID, flen, CA_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
int16_t pcm[CA_FRAME_SAMPLES]; memcpy(pcm, data, sizeof(pcm)); // wire header 11 байт не выровнен для int16_t
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
int olen = enc ? opus_codec_encode(enc, pcm, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
@@ -248,7 +270,8 @@ static void ca_close_client(struct call_audio_client* cli) {
cli->closing = 1;
if (ca && ca->ua && cli->socket_id) uasync_remove_socket_t(ca->ua, cli->fd);
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx", CH_ID, (unsigned long long)cli->call_id);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx pcm_drop=%u",
CH_ID, (unsigned long long)cli->call_id, cli->pcm_dropped);
}
static void ca_read_callback(socket_t fd, void* arg) {
@@ -346,7 +369,10 @@ int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
call_set_media_recv_cb(inst, ca_media_recv, ca);
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
if (!ca->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
call_headless_destroy(inst); return -1;
}
ca->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%u", CH_ID, bind_ip, bound_port);
@@ -361,7 +387,7 @@ uint16_t call_headless_get_port(struct UTUN_INSTANCE* inst) {
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio* ca = ca_of(inst);
if (!ca || !ca->running) return;
if (!ca) return;
ca->running = 0;
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; }
call_set_media_recv_cb(inst, NULL, NULL);
@@ -377,6 +403,7 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
c = n;
}
ca->clients = NULL; ca->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u pcm_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped, ca->c_pcm_dropped);
inst->call_audio = NULL; u_free(ca);
}
+15
View File
@@ -11,6 +11,16 @@
// type = 0x02 FRAME (PCM int16 mono 48k, 960 сэмплов = 1920 байт)
// клиент → ядро = PCM захвата (кодируется и шлётся пиру);
// ядро → клиент = PCM воспроизведения (декодированный Opus пира)
// call_id хранится в порядке байтов хоста, len — старший байт первым; PCM — native int16_t.
// Это отдельный сервис с кодеками instance, без DSP/jitter движка call_audio.h.
// HELLO выбирает call_id для приёма; передаваемый FRAME содержит call_id своего звонка.
// До16 TCP-клиентов; если несколько выбрали один звонок, RX получает первый в списке (последний подключённый).
// Encoder/decoder создаются лениво и заменяются при смене call_id. Отправка требует ACTIVE в call.c.
// Все вызовы и callbacks выполняются в потоке ua; клиент обеспечивает темп и воспроизведение.
// Исходящая очередь принимает header+PCM целиком; при переполнении отбрасывается целый кадр с WARN/счётчиком.
// Частичная отправка продолжается перед следующим кадром. Буферы ввода/вывода по64КиБ на клиента;
// parser ждёт полный фрейм, неизвестный type/len>2048 закрывает соединение, неверный размер PCM отбрасывается.
// Закрытые клиенты освобождаются таймером каждые500мс; до очистки занимают слот лимита16.
#ifndef CALL_HEADLESS_H
#define CALL_HEADLESS_H
@@ -28,8 +38,13 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
/* Запустить IPv4/TCP listener bind_ip:port и зарегистрировать приём Opus в call.c.
* ua — тот же loop, что inst->ua; inst должен иметь call_init. port=0 выбирает ОС.
* Один работающий listener на inst; повтор init не меняет адрес. 0 = успех, -1 = ошибка.
* При ошибке вызывающий выполняет destroy, включая частично созданный контекст. */
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
/* Снять callback, отменить таймер и освободить listener/клиентов/кодеки до call_destroy и destroy ua. NULL допустим. */
void call_headless_destroy(struct UTUN_INSTANCE* inst);
// port=0 при init выбирает ОС; 0 для остановленной службы или при ошибке.
uint16_t call_headless_get_port(struct UTUN_INSTANCE* inst);
+34 -16
View File
@@ -11,43 +11,61 @@ extern "C" {
/* Звонок: FIFO Opus, запас 20мс..max_reserve_ms по изменению задержки доставки за 10 секунд и догон после паузы устройства.
* push — сетевой поток, pull/decode/CallDelay — единственный аудиопоток.
* Старт/восстановление ждут запас плюс запрошенный блок; при выдаче PCM запас не резервируется повторно.
* destroy — после остановки обоих потребителей. get_stats — любой поток. */
* destroy — после остановки всех обращений. get_stats/diagnostics/clock — любой поток,
* внутренний mutex защищает их снимки. Все операции playback (включая silence/reset)
* сериализует владелец; mutex не делает допустимыми два одновременных pull. */
#define CALL_JITTER_FRAME_MS 20
#define CALL_JITTER_MIN_RESERVE_MS 20
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */
/* Заимствованный context, encoded/len и выход/capacity (сэмплы, до 960); вернуть 1..capacity или <=0 при ошибке.
* Callback вызывается синхронно на pull без сетевого mutex; encoded действителен только до возврата. */
typedef int (*call_jitter_decode_fn)(void*, const uint8_t*, int, int16_t*, int);
struct call_jitter_range { int min_ms, max_ms, reserve_ms; };
struct call_jitter_range { int min_ms, max_ms, reserve_ms; }; // Экстремумы полной глубины и отдельная цель доставки, мс.
struct call_jitter;
struct call_jitter_diagnostics {
int depth_ms, reserve_ms, tempo_x100, error_result, error_len;
uint32_t decode_errors, budget_hits;
int resumed, catching_up, waiting;
int encoded_ms, processing_ms, pcm_samples;
int64_t arrival_spread_ms;
uint32_t underruns;
int delay_mode, tempo_ppm;
unsigned phase_samples;
uint64_t cut_samples, stretch_entries, stretch_exits;
int depth_ms, reserve_ms, tempo_x100, error_result, error_len; // Глубина перед pull, цель, темп 100=1x, последний decode-отказ.
uint32_t decode_errors, budget_hits; // События до reset_events: отказы decoder и исчерпание лимита попыток pull.
int resumed, catching_up, waiting; // Событие полного восстановления; темп>1.05; последнее ожидание/короткая выдача.
int encoded_ms, processing_ms, pcm_samples; // Текущий FIFO, исходный PCM в DSP (мс), сэмплы последнего pull.
int64_t arrival_spread_ms; // Размах вариации доставки за окно, без ограничения верхним reserve.
uint32_t underruns; // Число коротких pull после накопления; начальное ожидание не учитывается.
int delay_mode, tempo_ppm; // CallDelay::Mode и темп 1000000=1x.
unsigned phase_samples; // Ошибка Direct-фазы относительно границы 960 сэмплов.
uint64_t cut_samples, stretch_entries, stretch_exits; // Счётчики CallDelay с последнего reset.
};
/* Снимок и сброс событий под mutex; логировать после выхода из mutex голосового стека. */
/* j/out обязательны. reset_events!=0 обнуляет только decode_errors, budget_hits и resumed.
* Логировать после выхода из mutex голосового стека; dropped/underruns этим чтением не сбрасываются. */
void call_jitter_get_diagnostics(struct call_jitter*, struct call_jitter_diagnostics*, int reset_events);
/* decode обязателен; arg заимствован до destroy, max_reserve_ms=500..2000. NULL при ошибке. */
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms);
/* Освободить объект; NULL допустим. */
void call_jitter_destroy(struct call_jitter* j);
/* sent_ms — timestamp кадра на проводе (uint32 с переполнением), received_ms — локальные монотонные часы.
* Абсолютное смещение часов пиров не влияет на оценку джиттера. */
* Абсолютное смещение часов пиров не влияет на оценку джиттера. enc (1..512 байт) и timing копируются;
* timing=NULL означает отсутствие PTS. FIFO сохраняет порядок push, сортировки/дедупликации нет.
* При заполнении удаляется старейший кадр; неверные аргументы/регрессия received_ms отклоняются с WARN. */
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms,
const struct call_capture_time* timing);
/* Только после остановки pull: убрать устаревший PCM, сохранить свежий сетевой запас. */
/* Только после остановки playback-операций: удалить PCM/DSP, увеличить локальную epoch,
* оставить последние ceil(reserve/20) Opus-кадров. Оценка доставки и суммарные dropped/underruns сохраняются. */
void call_jitter_reset(struct call_jitter* j);
/* out — max_samples mono int16 @48кГц; вернуть 0..max_samples, без дополнения тишиной.
* Старт/восстановление ждут reserve плюс размер запроса. Decode ограничен min(750, 8+max_samples/480) попытками.
* NULL/неположительный размер дают 0. Вызывающий учитывает дополненную локальную тишину через silence. */
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
/* Playback worker: учесть локальную тишину/тон после pull и прогресс устройства. */
/* Playback worker: учесть только дополненные после pull локальные сэмплы; их PTS удерживается. */
void call_jitter_silence(struct call_jitter* j, unsigned samples);
/* Абсолютная PCM-позиция и локальный срок её вывода; epoch из clock, at_us/now_us на одной монотонной шкале.
* Условия свежести/ошибок карты — в call_timeline.h. */
void call_jitter_progress(struct call_jitter* j, uint64_t epoch, uint64_t position, int64_t at_us, int64_t now_us, int measured);
/* Приостановить аппаратный якорь до следующего progress, сохранив PCM и текущую epoch. */
void call_jitter_suspend_device(struct call_jitter* j, uint64_t epoch);
/* Любой поток: снимок часов, включая epoch при valid=0. */
/* Любой поток: снимок часов, включая epoch при valid=0; NULL j/out оставляет out без изменений. */
void call_jitter_clock(struct call_jitter* j, int64_t now_us, struct call_clock_snapshot* out);
/* Неблокирующее чтение: 1 — снимок получен, 0 — mutex занят. Аргументы должны быть валидны. */
int call_jitter_try_clock(struct call_jitter* j, int64_t now_us, struct call_clock_snapshot* out);
/* Необязательные выходы: FIFO+processing (мс исходной речи), темп 100=1x, накопительные потери/недоборы и range.
* Счётчики чтением не сбрасываются; dropped включает удаление старых FIFO-кадров при overflow/reset. NULL j — no-op. */
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
+11 -7
View File
@@ -3,14 +3,17 @@
#include <algorithm>
#include <cstdint>
// Десять секундных интервалов. Отдельные экземпляры для глубины очереди и задержки доставки.
// Десять секундных интервалов (текущий и девять предыдущих), без внутренней синхронизации.
// Владелец передаёт неубывающие неотрицательные now/received_ms. Отдельные экземпляры
// для глубины очереди и задержки доставки: возраст PCM сам по себе не меняет сетевой reserve.
struct call_jitter_window {
struct bucket { int64_t second = -1, low = 0, high = 0; } buckets[10];
int64_t low = 0, high = 0;
int reserve = CALL_JITTER_MIN_RESERVE_MS;
int max_reserve = 1000;
int64_t last_received_ms = -1, transit_ms = 0;
uint32_t last_sent_ms = 0;
struct bucket { int64_t second = -1, low = 0, high = 0; } buckets[10]; // -1 — интервал ещё не наблюдался.
int64_t low = 0, high = 0; // Экстремумы depth/transit в действующих интервалах, мс.
int reserve = CALL_JITTER_MIN_RESERVE_MS; // clamp(high-low, 20, max_reserve), мс.
int max_reserve = 1000; // Владелец задаёт предел не ниже CALL_JITTER_MIN_RESERVE_MS.
int64_t last_received_ms = -1, transit_ms = 0; // Последний приём и накопленная вариация задержки, мс.
uint32_t last_sent_ms = 0; // Предыдущий wire timestamp; соседние интервалы должны укладываться в int32.
// Добавить depth (мс, допускается отрицательный transit); true — начался другой секундный интервал.
bool observe(int64_t now_ms, int64_t depth) {
int64_t second = now_ms / 1000;
auto& b = buckets[second % 10];
@@ -27,6 +30,7 @@ struct call_jitter_window {
}
// Разность интервалов прихода и отправки: смещение часов исключается, uint32 timestamp разворачивается.
// Первый приём задаёт transit=0; возврат и обновлённые поля — как у observe().
bool observe_arrival(int64_t received_ms, uint32_t sent_ms) {
if (last_received_ms >= 0) transit_ms += received_ms - last_received_ms - (int32_t)(sent_ms - last_sent_ms);
last_received_ms = received_ms; last_sent_ms = sent_ms;
+39 -28
View File
@@ -1,43 +1,54 @@
/* Внутреннее состояние call.c/call_video_transport.c; доступ только из uasync. */
/* Внутренний реестр звонков: call.c владеет ctx/сессиями, video_transport и process
* видеопорта обращаются к ним только из uasync. Workers используют публичный port API.
* Сессия размещена в data внешнего ll_entry; sessions индексирует call_id, а не пару group/peer.
* Find возвращает заимствованный указатель, в том числе на ENDED до удаления записи.
* При ENDED аудио-TX/video останавливаются сразу; CM/запись могут ждать END_ACK/таймаут.
* Обычное удаление снимает запись из реестра и освобождает entry через call_soon;
* call_destroy отменяет callbacks и освобождает оставшиеся в реестре записи синхронно.
* Не переносить указатели ctx/session в другой поток или отложенную команду: передавать call_id
* и повторять find в uasync. Описание публичных действий/событий — call.h/call_proto.h. */
#ifndef CALL_PRIV_H
#define CALL_PRIV_H
#include "call.h"
#include "../../lib/ll_queue.h"
/* IDLE — только создание; RINGING бывает у обеих ролей, WAIT_DECISION — ожидание force
* при телефонной занятости; ENDED в реестре уже не занимает линию и не принимает медиа. */
enum call_state { CALL_ST_IDLE, CALL_ST_OUTGOING, CALL_ST_RINGING, CALL_ST_ACTIVE, CALL_ST_ENDED, CALL_ST_WAIT_DECISION };
enum call_role { CALL_ROLE_CALLER, CALL_ROLE_CALLEE };
struct call_video_transport;
struct call_session {
struct ll_entry ll;
uint64_t call_id, group_id, peer_node_id;
uint8_t role, state, reason;
struct ll_entry ll; /* Не используется; реальный queue entry находится перед s == entry->data. */
uint64_t call_id, group_id, peer_node_id; /* Неизменные ID попытки, группы маршрутизации и собеседника. */
uint8_t role, state, reason; /* call_role/call_state; reason — CALL_REASON_* после перехода в ENDED. */
uint8_t force; /* вызывающий подтвердил обход телефонной занятости */
uint64_t started_tb; /* неизменный deadline попытки, включая выбор продолжения */
uint8_t end_subcmd;
uint64_t end_started_tb;
struct call_ctx *ctx;
struct CONN_MGR_HANDLE *cm_handle;
void *ring_timer, *invite_timer;
uint8_t no_route_strikes;
void *traffic_timer, *teardown_timer, *stats_timer;
uint8_t connection_reported, connection_type;
uint64_t connection_next_hop, last_media_tb;
uint16_t tx_media_seq;
struct ll_queue *tx_q;
struct queue_waiter_handle send_waiter;
uint32_t c_tx_frames, c_rx_frames, c_tx_dropped, c_rx_dropped;
uint64_t tx_wait_total_ms;
uint32_t tx_wait_frames, tx_wait_max_ms;
struct call_video_transport *video;
uint64_t started_tb; /* Начало попытки get_time_tb (100мкс); 60с отсчитываются отсюда, включая WAIT_DECISION. */
uint8_t end_subcmd; /* Финальная команда для повтора; 0 — нет своей команды либо уже принят END_ACK. */
uint64_t end_started_tb; /* Начало ENDED в tb; предел повтора своей команды — 20с. */
struct call_ctx *ctx; /* Заимствованный владелец, живёт до удаления сессии из реестра. */
struct CONN_MGR_HANDLE *cm_handle; /* Собственный запрос оптимизации пути; ACTIVE не требует готового CM. */
void *ring_timer, *invite_timer; /* Caller: 60с/повтор INVITE 300мс; callee: ring_timer — watchdog INVITE до 2с. */
uint8_t no_route_strikes; /* Последовательные INVITE ticks без маршрута; сбрасывается при наличии маршрута. */
void *traffic_timer, *teardown_timer, *stats_timer; /* RX watchdog 20с, завершение/END retry, путь и ACTIVE-статистика 1с. */
uint8_t connection_reported, connection_type; /* Было событие CONNECTION / последний router path.type (0..3). */
uint64_t connection_next_hop, last_media_tb; /* Кэш next hop и время последнего RX; watchdog использует traffic_timer. */
uint16_t tx_media_seq; /* Следующий sequence аудиопакета с wrap uint16; расходуется до выделения TX entry. */
struct ll_queue *tx_q; /* Собственная FIFO готовых audio packets [svc][media]; до 750, переполнение удаляет голову. */
struct queue_waiter_handle send_waiter; /* Отменяемое одноразовое ожидание места в router audio send_q. */
uint32_t c_tx_frames, c_rx_frames, c_tx_dropped, c_rx_dropped; /* TX принят router / валидный RX / TX overflow / RX не ведётся. */
uint64_t tx_wait_total_ms; /* Сумма времени audio enqueue->drain, включая отказ router; сброс при отчёте stats. */
uint32_t tx_wait_frames, tx_wait_max_ms; /* Число drained кадров/максимальное ожидание за тот же отчётный интервал. */
struct call_video_transport *video; /* Собственное ленивое состояние видеотрека; cleanup обнуляет и освобождает его. */
};
struct call_ctx {
struct UTUN_INSTANCE *inst;
int initialized;
struct ll_queue *sessions;
uint32_t c_calls_started, c_calls_ended;
call_media_recv_fn media_recv_fn;
void *media_recv_arg;
const struct call_audio_ops *audio_ops;
struct UTUN_INSTANCE *inst; /* Заимствованное ядро, владеющее ctx в inst->call; audio_ops хранится отдельно у ядра. */
int initialized; /* Сигналинг привязан; 1 также во время регистрации media services/отката call_init. */
struct ll_queue *sessions; /* Собственные queue entries с call_session в data и 8-байтовым индексом call_id. */
uint32_t c_calls_started, c_calls_ended; /* Созданные исходящие / все переходы в ENDED; это не парные счётчики. */
call_media_recv_fn media_recv_fn; /* Один необязательный headless RX callback, вызываемый в uasync. */
void *media_recv_arg; /* Заимствованный arg до замены callback/destroy; пересоздание ctx сбрасывает привязку. */
};
/* Найти call_id, включая ENDED: заимствованная сессия либо NULL (также при NULL ctx/sessions).
* Только uasync; не проверяет state/group/peer. После удаления/destroy указатель использовать нельзя. */
struct call_session *call_session_find(struct call_ctx *ctx, uint64_t call_id);
#endif
+23 -20
View File
@@ -5,6 +5,9 @@
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
// Сигналинг и медиа требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT: шифрование тела
// и подпись заголовка/шифртекста между конечными узлами, независимо от маршрута.
// Структуры упакованы без padding; целые поля копируются в порядке байтов хоста, без htons/ntohs.
// Здесь фиксирован один аудиотрек, согласования аудиоформата нет: отправитель объявляет Opus48k mono/20мс/32кбит/с.
// call.c проверяет размер/число треков, но не настраивает decoder по полям descriptor; кодеки создаёт frontend.
#ifndef CALL_PROTO_H
#define CALL_PROTO_H
@@ -16,7 +19,7 @@ extern "C" {
/* ── подкоманды (первый байт payload после svc_id) ── */
enum {
CALL_SUBCMD_INVITE = 0x01, /* {call_id:8, group_id:8, tracks:[]} */
CALL_SUBCMD_INVITE = 0x01, /* call_pkt_invite: call_id, group_id, num_tracks, force, expires_us, один track */
CALL_SUBCMD_RINGING = 0x02, /* {call_id:8} */
CALL_SUBCMD_ACCEPT = 0x03, /* {call_id:8} */
CALL_SUBCMD_DECLINE = 0x04, /* {call_id:8, reason:1} */
@@ -28,7 +31,7 @@ enum {
CALL_SUBCMD_MEDIA = 0x10, /* call_pkt_media + payload[] */
};
/* ── треки и кодеки (модель расширяема под видео) ── */
/* Типы треков; в этом протоколе INVITE/MEDIA отправляют только AUDIO. Видео согласуется отдельно. */
enum { CALL_TRACK_AUDIO = 0x01, CALL_TRACK_VIDEO = 0x02 };
enum { CALL_CODEC_OPUS = 0x01 };
@@ -48,21 +51,21 @@ enum {
#pragma pack(push, 1)
struct call_pkt_track {
uint8_t type; /* CALL_TRACK_AUDIO / CALL_TRACK_VIDEO */
uint8_t codec; /* CALL_CODEC_OPUS */
uint32_t sample_rate;
uint8_t channels;
uint32_t bitrate;
uint8_t type; /* Отправитель ставит CALL_TRACK_AUDIO. */
uint8_t codec; /* CALL_CODEC_OPUS. */
uint32_t sample_rate; /* 48000 Гц. */
uint8_t channels; /* 1, mono. */
uint32_t bitrate; /* 32000 бит/с; информационное поле, не настройка удалённого encoder. */
};
struct call_pkt_invite {
uint8_t subcmd; /* CALL_SUBCMD_INVITE */
uint64_t call_id;
uint64_t group_id;
uint8_t num_tracks;
uint8_t num_tracks; /* Ровно1; размер пакета = sizeof(call_pkt_invite), 38 байт. */
uint8_t force; /* 1 = явно подтверждённое продолжение при телефонной занятости */
int64_t expires_us; /* Unix microseconds; устаревшая кодограмма не начинает вызов */
struct call_pkt_track tracks[1]; /* tracks[num_tracks] */
int64_t expires_us; /* Unix us с NTP-поправкой: отправка now+2с, приём требует now<expires<=now+4с. */
struct call_pkt_track tracks[1]; /* Один descriptor (11 байт), без списка произвольной длины. */
};
struct call_pkt_ringing { uint8_t subcmd; uint64_t call_id; };
@@ -75,14 +78,14 @@ struct call_pkt_end_ack { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_media {
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
uint64_t call_id;
uint8_t track_id;
uint16_t seq;
uint32_t ts_ms;
int64_t capture_pts_us; /* INT64_MIN — источник не предоставляет capture clock. */
uint64_t capture_epoch;
uint32_t codec_delay_samples;
/* payload[] */
uint64_t call_id; /* ID существующего ACTIVE-звонка; peer/group проверяет call.c. */
uint8_t track_id; /* CALL_TRACK_AUDIO. */
uint16_t seq; /* Счётчик отправителя с переполнением; call_audio FIFO не сортирует по seq. */
uint32_t ts_ms; /* Монотонное время постановки в TX FIFO, миллисекунды modulo 2^32. */
int64_t capture_pts_us; /* Монотонный PTS первого входного сэмпла; INT64_MIN — часы неизвестны. */
uint64_t capture_epoch; /* Ненулевая эпоха capture при известном PTS; смена означает разрыв. */
uint32_t codec_delay_samples; /* Opus lookahead при48кГц, не более48000. */
/* Далее непустой Opus payload; call_send_media ограничивает общий media-пакет1024 байтами. */
};
#pragma pack(pop)
@@ -96,11 +99,11 @@ struct call_pkt_media {
/* ── таймауты и ёмкости (timebase 0.1ms) ── */
#define CALL_RING_TIMEOUT_TB 600000 /* 60с от начала, включая ожидание решения */
#define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING + RINGING) */
#define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING/RINGING/WAIT_DECISION) */
#define CALL_RING_DEAD_TB 20000 /* 2с — вызываемый: нет кодограммы → вызов сорван */
#define CALL_NO_ROUTE_MAX_STRIKES 20 /* 20 × 300мс ≈ 6с без маршрута → fail-fast CONNECT_FAIL */
#define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */
#define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */
#define CALL_TEARDOWN_DELAY_TB 5000 /* Не используется; без своей end-команды CM/роль callee удерживают запись2.1с. */
#define CALL_END_RETRY_TB 3000 /* 300мс — повтор финальной команды до END_ACK */
#define CALL_END_TIMEOUT_TB 200000 /* 20с — предел удержания завершённой сессии без END_ACK */
#define CALL_MAX_TX_FRAMES 750 /* 15с: 10с перебоя + 5с запаса */
+26 -14
View File
@@ -12,27 +12,38 @@
class CallTimeline {
public:
// Начать пустую карту локальной эпохи; сбрасывает ошибки, позиции, PTS и аппаратный якорь.
void reset(uint64_t epoch);
// Зарегистрировать 1..8192 входных сэмплов 48кГц до callback source; INT64_MIN/epoch=0 не дают валидный PTS.
void feed(int64_t pts_us, unsigned samples, uint64_t source_epoch);
// Callback происхождения DSP/CallDelay: Copy/Mix должны описывать непрерывный выход из зарегистрированного входа.
void source(const soundtouch::SourceSpan& span);
// Перенести только реально извлечённый PCM на шкалу устройства; повторно учитывать его в silence нельзя.
void read(unsigned samples);
// Дописать локальную тишину/тоны, удерживая последний PTS. До первого валидного источника PTS остаётся недостоверным.
void silence(unsigned samples);
// Якорь локальных часов: position — абсолютная PCM-позиция от reset, at_us — срок её вывода,
// received_us — время наблюдения. Чужая epoch игнорируется; регрессия позиции/received или position>written
// запрещает часы до reset. Будущий at_us учитывает очередь устройства; measured — аппаратная метка или оценка.
void progress(uint64_t epoch, uint64_t position, int64_t at_us, int64_t received_us, bool measured);
// Приостановить якорь своей эпохи до progress, сохранив PCM/PTS/позиции; прежние ошибки не сбрасывает.
void suspend_device(uint64_t epoch);
// Снимок с продвижением/очисткой карты. Якорь старше 250мс, suspend, ошибки карты или неизвестный PTS дают valid=0.
// PTS не регрессирует: откат до 100мс удерживается, больший откат делает снимок недостоверным.
call_clock_snapshot clock(int64_t now_us);
private:
struct Span {
uint64_t first = 0, epoch = 0;
unsigned count = 0;
double pts = 0, end = 0;
bool valid = false;
uint64_t first = 0, epoch = 0; // Первый индекс участка в его кольце и поколение удалённого захвата.
unsigned count = 0; // Число сэмплов участка.
double pts = 0, end = 0; // PTS начала и конца, мкс; равны при удержании локальной тишины.
bool valid = false; // Есть достоверная шкала одного источника.
Span() = default;
Span(uint64_t f, uint64_t e, unsigned n, double p, double q, bool v)
: first(f), epoch(e), count(n), pts(p), end(q), valid(v) {}
};
template<size_t N> struct Ring {
std::array<Span, N> data{};
size_t head = 0, count = 0;
std::array<Span, N> data{}; // Фиксированное кольцо; push при заполнении возвращает false.
size_t head = 0, count = 0; // Начало и число действующих участков.
Span& front() { return data[head]; }
const Span& back() const { return data[(head + count - 1) % N]; }
void pop() { head = (head + 1) % N; --count; }
@@ -43,14 +54,15 @@ private:
}
};
Ring<256> input; // >5с входа; SoundTouch звонка держит лишь несколько блоков.
Ring<1024> ready;
Ring<2048> output;
uint64_t inputWritten = 0, generated = 0, consumed = 0, written = 0, epoch = 0;
uint64_t anchorPosition = 0, minimumPosition = 0;
int64_t anchorTime = 0, anchorReceived = 0, lastPts = -1;
double endPts = 0;
uint64_t endEpoch = 0;
int sourceError = 0;
Ring<1024> ready; // Произведённый DSP-выход, ещё не учтённый read.
Ring<2048> output; // PCM устройства до подтверждённого продвижения; overflow запрещает часы до reset.
uint64_t inputWritten = 0, generated = 0, consumed = 0, written = 0, epoch = 0; // Вход/DSP/прочитано/устройство/локальная эпоха.
uint64_t anchorPosition = 0, minimumPosition = 0; // Последний device-якорь и монотонная нижняя граница сыгранного PCM.
int64_t anchorTime = 0, anchorReceived = 0, lastPts = -1; // Срок/приём якоря и последний валидный PTS; -1 — ещё нет PTS.
double endPts = 0; // Конец последнего записанного участка, удерживаемый silence.
uint64_t endEpoch = 0; // Поколение источника конца output.
int sourceError = 0; // Отложенная ошибка DSP-карты; логируется при read.
// Необратимые до reset ошибки output/device; наличие/тип якоря; валидность конца output.
bool outputFailed = false, deviceFailed = false, haveAnchor = false, measured = false, endValid = false;
static double at(const Span& s, double position);
const Span* findInput(uint64_t position) const;
+5 -3
View File
@@ -12,17 +12,19 @@ extern "C" {
struct call_tones;
/* Создать выключенный генератор; NULL при нехватке памяти. Управление и render сериализует владелец. */
struct call_tones* call_tones_create(void);
void call_tones_destroy(struct call_tones* t);
void call_tones_destroy(struct call_tones* t); // Освободить после остановки render; NULL допустим.
void call_tones_reset(struct call_tones* t); /* в тишину */
void call_tones_glitch_start(struct call_tones* t); /* запустить/продолжить цикл глитча */
void call_tones_glitch_stop(struct call_tones* t); /* прекратить глитч (тишина) */
void call_tones_ended_start(struct call_tones* t); /* 3 нисходящих тона, один раз */
void call_tones_ended_start(struct call_tones* t); /* 3 нисходящих тона 700/500/400Гц по 150мс; повторный start начинает заново */
int call_tones_ended_pending(struct call_tones* t); /* 1 = тон завершения ещё звучит */
/* Выдать до max_samples сэмплов (тон или тишина). Возвращает записанное число. */
/* При валидных аргументах заполняет ровно max_samples (тон/паузы/тишина после завершения).
* Возвращает max_samples; 0 при NULL t/out или max_samples<=0. Остальные операции с NULL t безопасны. */
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples);
#ifdef __cplusplus
+36 -18
View File
@@ -1,7 +1,10 @@
/* Общий контракт видеотрека. Все операции выполняет владелец в uasync-потоке.
/* Общий контракт видеотрека: чистые проверки/сериализация доступны любому потоку,
* один receiver принадлежит одному владельцу, без внутренней синхронизации.
* Сетевой transport использует receiver из uasync; codec workers используют проверки формата.
* Кодеки: H.264 Baseline/Constrained Baseline, H.265 Main (Main/High tier), 8 bit,
* 4:2:0, progressive, без B-кадров/reorder. DPB ограничен уровнем потока.
* Поколение меняется только при смене формата/камеры; bitrate не меняет поколение.
* Поля format неизменны внутри generation; его увеличивают при смене формата/камеры.
* Текущий bitrate encoder не входит в format и не меняет generation.
* KEY: config_length:4BE + Annex B configuration + Annex B access unit (как uAV2).
* Приём хранит один частичный кадр. Готовый view заимствован до следующего push/reset.
* Очередью готовых кадров, decoder и показом по audio clock владеет вызывающий. */
@@ -31,29 +34,31 @@ extern "C" {
* Поля — независимые верхние границы, не обещание поддержки их сочетания:
* выбранный режим дополнительно проверяется открытием платформенного codec. */
struct call_video_limit {
uint16_t width, height, fps;
uint32_t pixels_per_second, maximum_bps;
uint16_t width, height, fps; // Верхние границы осей (16..4096) и частоты (1..60); нулевой профиль — все поля нулевые.
uint32_t pixels_per_second, maximum_bps; // Ограничение width*height*fps и целевого bitrate (300000..4000000 бит/с).
uint8_t profiles; // Маска режимов: AVC Baseline/CB; HEVC Main с Main/High tier. Приём каждого проверяется отдельно.
uint8_t level;
};
struct call_video_caps { struct call_video_limit encode[2], decode[2]; };
struct call_video_format {
uint64_t generation;
uint16_t width, height, fps, rotation;
uint8_t codec, level;
uint64_t generation; // Ненулевой, монотонно возрастающий номер формата в одном направлении звонка.
uint16_t width, height, fps, rotation; // Чётные размеры 16..4096, 1..60 fps, поворот отображения 0/90/180/270 градусов.
uint8_t codec, level; // CALL_VIDEO_H264/H265 и AVC level_idc / HEVC general_level_idc.
uint32_t maximum_bps; // Предел целевого bitrate; не обещание мгновенного размера выхода/CPB.
uint8_t profile;
uint8_t profile; // Один режим 1/2, не маска: AVC Baseline/CB либо HEVC Main с Main/High tier.
};
struct call_video_frame {
uint64_t call_id, frame_id;
int64_t pts_us;
uint64_t call_id, frame_id; // Ненулевые ID; frame_id возрастает сквозь поколения, пропуск требует KEY.
int64_t pts_us; // Capture MONOTONIC отправителя: 0..INT64_MAX/2, строго возрастает сквозь поколения.
struct call_video_format format;
uint32_t size, offset, length;
uint8_t flags;
const uint8_t *data;
uint32_t size, offset, length; // Размер всего кадра, смещение и размер данного фрагмента; полный кадр: offset=0, length=size.
uint8_t flags; // Только CALL_VIDEO_KEY либо 0.
const uint8_t *data; // Начало payload данного фрагмента, уже со смещением offset; владение задаёт вызывающий API.
int64_t received_us; // Только локальная RX port метка MONOTONIC; по сети не передаётся.
};
/* 1 — все limits структурно корректны, включая полностью нулевые CAPS; 0 — ошибка/NULL.
* Не проверяет реальные codec и совместимость сочетания независимых границ. */
int call_video_caps_valid(const struct call_video_caps *caps);
/* После успешных платформенных проб width×height и height×width расширяет обе осевые
* границы limit до длинной стороны, сохраняя pixel-rate/FPS/bitrate/profile/level.
@@ -61,34 +66,47 @@ int call_video_caps_valid(const struct call_video_caps *caps);
* 0 — успех, -1 — неверный limit/codec (limit не меняется). */
int call_video_limit_orientations(struct call_video_limit *limit, int codec);
/* format задаёт generation/размер/FPS/rotation/maximum_bps, level=0, profile=0 для auto.
* codec=0: H.265, затем H.264. Успех: codec (1/2), заполняются codec и минимальный level.
* codec=0: H.265, затем H.264. Успех: codec (1/2), заполняются codec/profile и минимальный level.
* 0 — нет совместимого режима, -1 — неверные аргументы; на отказе format не меняется. */
int call_video_choose(const struct call_video_caps *sender, const struct call_video_caps *receiver,
struct call_video_format *format);
/* Проверка уже выбранного/полученного режима, включая оба направленных CAPS и level. */
/* 1 — формат допустим для sender.encode и receiver.decode, 0 — нет/неверный аргумент. */
int call_video_format_supported(const struct call_video_caps *sender, const struct call_video_caps *receiver,
const struct call_video_format *format);
/* 1/0: проверка полей и таблиц level по padded geometry/FPS/bitrate, без открытия codec. */
int call_video_format_valid(const struct call_video_format *format);
/* Нормативный предел DPB в кадрах по coded geometry и level; 0 при неверном уровне. */
unsigned call_video_dpb_limit(int codec, unsigned level, unsigned width, unsigned height);
/* 1/0: ненулевые HRD bitrate (бит/с) и cpb (биты) в пределах таблицы уровня.
* nal!=0 включает NAL-множитель; для HEVC используется консервативная таблица Main tier. */
int call_video_hrd_valid(int codec, unsigned level, int nal, uint64_t bitrate, uint64_t cpb);
/* Сравнение всех полей format, включая generation; оба указателя обязательны. */
int call_video_same_format(const struct call_video_format *a, const struct call_video_format *b);
/* CAPS: четыре 16-байтовых limits (encode AVC/HEVC, decode AVC/HEVC), integers BE.
* 0/-1; при ошибке выход не меняется. Parse требует ровно CALL_VIDEO_CAPS_BYTES. */
int call_video_caps_encode(const struct call_video_caps *caps, uint8_t out[CALL_VIDEO_CAPS_BYTES]);
int call_video_caps_parse(const uint8_t *data, size_t length, struct call_video_caps *out);
/* Весь header проверяется до записи/публикации view. Отказ: -1, выход не меняется. */
/* Header 56 байт BE + 1..1200 байт payload; size всего кадра <=2 МиБ.
* Encode копирует fragment.data[0..length); parse возвращает view внутри data.
* Успех 0; весь header проверяется до записи/публикации view. Отказ -1, выход не меняется. */
int call_video_fragment_encode(const struct call_video_frame *frame, uint8_t *out, size_t capacity, size_t *length);
int call_video_fragment_parse(const uint8_t *data, size_t length, struct call_video_frame *out);
struct call_video_receiver;
/* Создать пустую сборку, изначально требующую KEY; NULL — ошибка выделения. */
struct call_video_receiver *call_video_receiver_create(void);
/* Destroy/reset допускают NULL. Reset освобождает буфер и забывает ID/PTS/формат. */
void call_video_receiver_destroy(struct call_video_receiver *receiver);
void call_video_receiver_reset(struct call_video_receiver *receiver);
/* 1 — полный кадр, 0 — частичный/пропущенный, -1 — неверный поток; после пропуска нужен KEY.
* bytes — строго один пакет, уже проверенный на принадлежность call_id/peer/group владельцем.
* now_us — монотонное время для ограничения жизни частичной сборки (2с).
* fragment — валидный заголовок и payload одного фрагмента, call_id/peer/group проверяет владелец.
* now_us — неубывающее локальное время; возраст частичной сборки >2с проверяется при push.
* Фрагменты одного кадра идут без разрывов offset; повтор/перестановка сбрасывают сборку.
* KEY здесь проверяется только по envelope; NAL проверяет call_video_bitstream/платформа.
* Новый формат принимается только с KEY. Старые готовые кадры вызывающий сохраняет до их PTS. */
int call_video_receiver_push(struct call_video_receiver *receiver, const struct call_video_frame *fragment,
uint64_t now_us, struct call_video_frame *complete);
/* 1 — нужен независимый KEY (также для NULL), 0 — цепочка сборки непрерывна. */
int call_video_receiver_needs_key(const struct call_video_receiver *receiver);
#ifdef __cplusplus
}
+7 -2
View File
@@ -3,7 +3,8 @@
* progressive 8-bit 4:2:0, один spatial/temporal layer, без reorder. DPB — по Annex A.
* HEVC допускает dependent segments внутри IDR и явный pic_output_flag=1;
* первый слайс независимый, адреса остальных — в границах CTB геометрии SPS.
* Результат публикуется целиком только после успешной проверки VPS/SPS/PPS и IDR.
* Это проверка нужных полей parameter sets и заголовков IDR, без полного разбора entropy payload;
* она не заменяет проверку реальным decoder. Результат публикуется только при успехе.
* Все функции доступны worker-потокам; состояния и платформенных объектов нет. */
#ifndef CALL_VIDEO_BITSTREAM_H
#define CALL_VIDEO_BITSTREAM_H
@@ -12,9 +13,13 @@
extern "C" {
#endif
struct call_video_bitstream {
unsigned width, height, level, profile, dpb;
unsigned width, height, level, profile, dpb; // Видимая геометрия после crop, wire level/profile и SPS DPB в кадрах.
};
/* codec=1/2, data — полный KEY envelope размером 9..CALL_VIDEO_MAX_FRAME.
* 0 — заполнен out, -1 — отказ с логом; out на ошибке не меняется, data заимствован на время вызова. */
int call_video_inspect_key(int codec, const uint8_t *data, size_t length, struct call_video_bitstream *out);
/* Inspect + сравнение геометрии/profile и actual level <= format.level; 0/-1.
* AVC CB принимается и в Baseline envelope. Сам format предварительно валидирует владелец. */
int call_video_check_key(const struct call_video_format *format, const uint8_t *data, size_t length);
/* KEY-проба 1280x720 либо 1920x1080, включая обратную ориентацию, fps=30 либо 60.
* Буфер 8192 байт достаточен; возвращает размер или -1 с диагностикой. */
+13 -8
View File
@@ -1,6 +1,7 @@
/* Общий таймер показа видео, один владелец — RX worker. Изображениями не владеет.
/* Общий таймер показа видео, один владелец — worker показа. Изображениями не владеет.
* poll вызывается для первого готового decoded кадра. queued — все ещё не показанные полные кадры.
* SHOW/DROP_ORDER/DROP_LATE потребляют decoded кадр, WAIT сохраняет его. Encoded входы не пропускаются.
* DROP_ORDER/DROP_LATE освобождают decoded кадр, SHOW удерживает его до presented; WAIT сохраняет голову.
* Сам модуль очереди не меняет. Encoded входы не пропускаются.
* received_us ставится на входе RX port, до ожидания worker; все локальные времена — MONOTONIC, не BOOTTIME.
* SHOW резервирует один кадр до presented(pts, фактическое время); только ACK продвигает часы.
* Короткое опустошение сохраняет расписание. Темп плавно зависит от суммарного запаса кадров.
@@ -14,7 +15,7 @@ extern "C" {
#endif
struct call_video_playout_frame {
int64_t pts_us, received_us;
int64_t pts_us, received_us; // Capture PTS отправителя и локальное время полной RX-сборки, мкс.
uint32_t interval_us; // 1000000/format.fps; разрыв capture PTS не создаёт повторную паузу.
};
enum call_video_playout_action {
@@ -22,9 +23,9 @@ enum call_video_playout_action {
};
struct call_video_playout_event;
struct call_video_playout {
uint64_t call_id;
int anchored, audio_valid;
int64_t last_pts, last_deadline, deadline_us;
uint64_t call_id; // Для диагностики; не фильтрует входные кадры по звонку.
int anchored, audio_valid; // Есть фаза подтверждённого показа / последний clock был valid.
int64_t last_pts, last_deadline, deadline_us; // Последний выбранный/пропущенный PTS, фаза и срок следующего poll.
int64_t report_us, last_show_us, interval_min, interval_max, av_min, av_max, dispatch_max, data_shift;
int64_t last_show_pts, last_received, gap_pts, gap_received, gap_dispatch;
uint32_t speed_ppm; // Темп аудио + коррекция отставания, максимум 1.6x.
@@ -34,7 +35,7 @@ struct call_video_playout {
unsigned source_gaps;
int64_t source_gap_max; // Большие разрывы capture PTS, уже прошедшие до показа.
struct call_video_playout_frame pending; // Единственный выбранный кадр до подтверждения потребителем.
int awaiting;
int awaiting; // Есть SHOW без соответствующего presented; новые SHOW запрещены.
int64_t selected_us, pending_av, head_pts, head_due_us, lead_us, rate_us;
int64_t head_show_us; // Плановая презентация головы; GUI ACK корректирует фазу плавно.
unsigned queued, reserve; // reserve — будущие кадры сверх головы, включая необходимый pipeline backend.
@@ -54,16 +55,20 @@ struct call_video_playout_event {
/* Reporter вне timer: форматирует и выводит сохранённое событие через DEBUG категории call. */
void call_video_playout_log_event(const struct call_video_playout_event *event);
/* Локальное MONOTONIC время, мкс (Windows использует get_time_us); не capture PTS удалённого узла. */
int64_t call_video_playout_now_us(void);
/* Обнулить состояние и logger, задать id. p обязателен; вызывать до первого poll/после остановки владельца. */
void call_video_playout_init(struct call_video_playout *p, uint64_t call_id);
/* frame==NULL обновляет состояние без снятия временной привязки.
* deadline_us — абсолютный срок при WAIT, проверять аудиочасы не реже чем раз в 5мс.
* queued учитывает encoded, decoder, decoded и GUI каждый ровно один раз; reserve обычно 1.
* p обязателен, now_us — неубывающий MONOTONIC; NULL/invalid audio означает WAIT.
* Некорректный PTS/received/interval даёт DROP_ORDER только при valid audio и отсутствии awaiting.
* source_epoch не участвует: коррекция аудио не означает разрыв видеокодека. */
enum call_video_playout_action call_video_playout_poll(struct call_video_playout *p,
const struct call_video_playout_frame *frame, int64_t now_us, const struct call_clock_snapshot *audio,
unsigned queued, unsigned reserve);
/* Подтверждение выбранного PTS в MONOTONIC. Повторный/чужой ACK не меняет расписание. */
/* Подтверждение выбранного PTS в MONOTONIC, не раньше выбора. Повторный/чужой ACK логируется и игнорируется. */
void call_video_playout_presented(struct call_video_playout *p, int64_t pts_us, int64_t now_us);
#ifdef __cplusplus
}
+13 -6
View File
@@ -1,6 +1,7 @@
/* call_video_port — обмен готовыми видеокадрами между платформенными workers и ядром звонка.
* Attach подключает ядро; для активного call_id: open → send/pull/status → close. Перед отправкой настроить encoder через format.
* Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта.
* Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта
* и снять transport callbacks через close с обработкой команды либо остановку сессии звонка.
* Send копирует кадры; pull передаёт буфер вызывающему, освобождение — u_free(). Команды исполняются в uasync.
* В процессе один порт: TX до восьми кадров по 2 МиБ, RX до восьми кадров и 4 МиБ суммарно. */
#ifndef CALL_VIDEO_PORT_H
@@ -24,7 +25,7 @@ struct call_video_port_status {
};
// ── Привязка к ядру ──
// Подключить единственное ядро к порту; 0/-1.
// Подключить единственное ядро к порту; 0/-1. Повторный attach, даже того же instance, — ошибка.
int call_video_port_attach(struct UTUN_INSTANCE *inst);
// Отменить ожидающий post и освободить очереди; выполнять до уничтожения ядра.
@@ -32,7 +33,8 @@ void call_video_port_detach(struct UTUN_INSTANCE *inst);
// ── Открытие и закрытие видеотрека ──
// Скопировать локальные capabilities и запланировать настройку видеотрека; 0/-1, не подтверждение готовности пира.
// Скопировать CAPS и запланировать настройку; 0/-1. Active-call проверяется асинхронно.
// Повтор open того же id сохраняет RX/TX/счётчики/wake; другой id очищает их.
int call_video_port_open(uint64_t id, const struct call_video_caps *caps);
// Закрыть текущий id и очистить его очереди; чужой id игнорируется.
@@ -54,7 +56,9 @@ int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg);
// ── Настройка кодера и исходящие кадры ──
// Передать фактический формат и битрейт открытого encoder; формат копируется. 0 — команда принята, -1 — отказ.
// Передать формат/битрейт открытого encoder; 0 — команда принята, -1 — отказ.
// Перед сменой формата владелец при необходимости ждёт tx_idle; сама команда не ждёт drain.
// Итог проверять по status.error/configured_generation; 0 не подтверждает настройку транспорта.
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
// Подтвердить команду status.rate_generation/rate_request: bps — применённый битрейт, 0 — отказ encoder. 0/-1.
@@ -62,9 +66,12 @@ int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request,
#define CALL_VIDEO_PORT_TX_FRAMES 8u
#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME)
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
/* Атомарно копирует готовую пачку одного звонка/формата с возрастающими frame_id/PTS.
* 1 — принята целиком, 0 — mailbox занят,
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES.
* Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта. */
* Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта.
* 1 подтверждает копирование в mailbox; transport проверяет формат/цепочку позже.
* Transport busy сохраняет непринятый хвост, ошибка освобождает его и увеличивает key_serial. */
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
// ── Входящие кадры ──
+14 -10
View File
@@ -1,7 +1,9 @@
/* Регулятор исходящего видеотрека: чистое состояние без таймеров, транспорта и encoder.
* Владелец вызывает step каждые 50мс с согласованными окнами доставки/производства 500мс.
* Все размеры — уникальные байты сжатого видео без заголовков. Held A/V не входит в debt.
* После смены транспортной эпохи нужен init. Команды encoder сериализует вызывающий. */
* После смены транспортной эпохи нужен init. Команды encoder сериализует вызывающий.
* Step допускает ненулевые границы/applied до 100 Мбит/с; диапазон звонка
* 300..4000 кбит/с проверяют parse_range и transport. */
#ifndef CALL_VIDEO_RATE_H
#define CALL_VIDEO_RATE_H
#include <stdint.h>
@@ -18,32 +20,34 @@ enum call_video_rate_reason {
};
struct call_video_rate_sample {
uint64_t now_us, window_end_us;
uint64_t delivered_bytes, produced_bytes, debt_bytes;
uint32_t applied_bps, minimum_bps, maximum_bps;
uint32_t baseline_rtt_us, rtt_us, frame_interval_us;
uint64_t now_us, window_end_us; // Неубывающее время и граница завершённого 50мс интервала: 0 <= now-end <50мс.
uint64_t delivered_bytes, produced_bytes, debt_bytes; // ACK/выход за 500мс и текущий долг; каждый <=UINT32_MAX.
uint32_t applied_bps, minimum_bps, maximum_bps; // Фактически применённый bitrate и желаемые границы, бит/с.
uint32_t baseline_rtt_us, rtt_us, frame_interval_us; // RTT и период кадра 1..1000000мкс для допуска штатного inflight.
uint64_t debt_limit_bytes; /* Жёсткий предел admission, независимо от ACK-оценки. */
int valid, ready, no_route, receive_busy, probing, locally_held, command_pending;
int valid, ready, no_route, receive_busy, probing, locally_held, command_pending; // Флаги окна, канала и encoder-команды.
};
struct call_video_rate_result {
uint32_t target_bps;
enum call_video_rate_reason reason;
uint32_t target_bps; // Рекомендация; при pending остаётся applied, даже если диапазон изменился.
enum call_video_rate_reason reason; // Причина рекомендации либо удержания/блокировки.
int admit; /* Уже принятые кадры продолжают drain; новые запрещены при переполнении. */
uint64_t delivery_bps, excess_bytes;
uint64_t delivery_bps, excess_bytes; // delivered_bytes*16 и долг сверх допуска RTT + два периода кадра.
};
struct call_video_rate {
// Внутренняя история; изменяется только init/step у одного владельца.
uint64_t last_us, next_change_us, previous_debt, high_since_us, last_log_us;
uint32_t applied_bps;
int initialized, high;
};
/* Сбросить историю к now_us; NULL диагностируется и игнорируется. */
void call_video_rate_init(struct call_video_rate *rate, uint64_t now_us);
/* Атомарная настройка min:max в десятичных кбит/с, 300 <= min <= max <= 4000.
* На ошибке выходные значения не меняются; результат 0/-1. */
int call_video_rate_parse_range(const char *text, uint32_t *minimum_bps, uint32_t *maximum_bps);
/* 0 — результат получен, -1 — недопустимый снимок (состояние не меняется).
/* 0 — результат получен, -1 — недопустимый снимок (состояние и result не меняются).
* target != applied — рекомендация, не подтверждение применения. Pending запрещает новую команду. */
int call_video_rate_step(struct call_video_rate *rate, const struct call_video_rate_sample *sample,
struct call_video_rate_result *result);
+14 -4
View File
@@ -11,7 +11,9 @@ struct call_session;
extern "C" {
#endif
struct call_video_ops {
// peer/frame заимствованы на время callback; frame — полный собранный encoded кадр.
void (*capabilities)(void *arg, const struct call_video_caps *peer);
// 1 — есть место для полного кадра bytes, 0 — router удерживает первый фрагмент/ACK до resume.
int (*receive_ready)(void *arg, uint32_t bytes);
void (*frame)(void *arg, const struct call_video_frame *frame);
void (*need_key)(void *arg);
@@ -19,8 +21,11 @@ struct call_video_ops {
void (*decoded)(void *arg, uint64_t generation, int accepted);
void (*writable)(void *arg, int admit); // Разрешение новых входов encoder; также будит retry сохранённой пачки.
};
/* Настройка активного звонка, включая receive-only. NULL ops снимает платформенные callbacks.
* capabilities отражают только реально проверенные профили. 0 — успешно, -1 — ошибка. */
/* Настройка активного звонка, включая receive-only. При ops обязательны frame/receive_ready;
* NULL ops снимает callbacks, обнуляет локальные CAPS/регулятор и очищает TX/частичную сборку RX.
* caps обязательны даже при NULL ops; отражают только реально проверенные профили.
* 0 — CAPS поставлены в router, -1 — ошибка; при отказе отправки настройка уже может быть применена.
* CAPS повторяются до прикладного CAPS_ACK; успешный configure ещё не разрешает TX. */
int call_video_configure(struct UTUN_INSTANCE *inst, uint64_t call_id, const struct call_video_caps *caps,
const struct call_video_ops *ops, void *arg);
/* Копирует один полный кадр. 1 принят, 0 временная занятость, -1 неверный формат/сессия/цепочка.
@@ -29,10 +34,12 @@ int call_video_configure(struct UTUN_INSTANCE *inst, uint64_t call_id, const str
* Каждый кадр выходит только после соответствующего аудио; deadline головы 2с.
* При истечении deadline сбрасывается вся зависимая цепочка и запрашивается KEY. */
int call_video_send_frame(struct UTUN_INSTANCE *inst, const struct call_video_frame *frame);
/* Повторить доставку router RX после освобождения места платформой; чужой/закрытый id игнорируется. */
void call_video_resume_receive(struct UTUN_INSTANCE *inst, uint64_t call_id);
/* Decoder потерял references. Запрос идемпотентный, отправляется не чаще раза в 500мс. */
void call_video_request_key(struct UTUN_INSTANCE *inst, uint64_t call_id);
/* Результат запуска decoder конкретного поколения, отдельно от потери references. */
/* Результат последнего полностью полученного поколения, accepted=0/1, 0/-1.
* Отправка control при необходимости повторяется timer, отдельно от потери references. */
int call_video_decode_result(struct UTUN_INSTANCE *inst, uint64_t call_id, uint64_t generation, int accepted);
/* После открытия encoder; format — согласованный и фактически проверенный режим.
* maximum_bps — дополнительный предел регулятора, не выше format.maximum_bps;
@@ -44,10 +51,13 @@ int call_video_decode_result(struct UTUN_INSTANCE *inst, uint64_t call_id, uint6
* applied=0 в rate_applied означает отказ encoder, выключает регулятор до configure_rate. */
int call_video_configure_rate(struct UTUN_INSTANCE *inst, uint64_t call_id, const struct call_video_format *format,
uint32_t applied_bps, uint32_t maximum_bps);
/* 0 — ненулевой результат принят; -1 — stale/неверный результат либо обработанный отказ applied_bps=0. */
int call_video_rate_applied(struct UTUN_INSTANCE *inst, uint64_t call_id, uint64_t generation,
uint64_t request, uint32_t applied_bps);
/* Только call.c: управление общей сессией и граница уже отправленного аудио. */
/* Только call.c: bind 0/-1 регистрирует два router-сервиса; unbind снимает их.
* Stop отменяет timer/продолжение/send waiter и освобождает transport сессии.
* Audio_sent обновляет границу уже отправленного аудио и повторяет flush; end_pts_us<0 игнорируется. */
int call_video_transport_bind(struct UTUN_INSTANCE *inst);
void call_video_transport_unbind(struct UTUN_INSTANCE *inst);
void call_video_transport_stop(struct call_session *session);
+22 -6
View File
@@ -32,10 +32,11 @@
static int ed25519_pub_from_priv(const uint8_t priv[32], uint8_t pub[32]) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32);
if (!pkey) return -1;
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 private key initialization failed", CA_ID); return -1; }
size_t plen = 32;
int ok = EVP_PKEY_get_raw_public_key(pkey, pub, &plen) == 1 && plen == 32;
EVP_PKEY_free(pkey);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 public key derivation failed bytes=%zu", CA_ID, plen);
return ok ? 0 : -1;
}
@@ -45,12 +46,23 @@ static int ed25519_pub_from_priv(const uint8_t priv[32], uint8_t pub[32]) {
static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry) return;
if (!conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 8 + 32 + 32) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — invalid delivery len=%u", CA_ID, entry->len);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
struct UTUN_INSTANCE* inst = conn->instance;
if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — no instance", CA_ID);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (!cc || !cc->initialized || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — not initialized", CA_ID);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — import rejected inside external transaction", CA_ID);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
const uint8_t* d = entry->dgram;
uint64_t from_node;
@@ -84,6 +96,10 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (topo_node_sqlite_channel_set_privs(cc->db, ch_id, x_priv, ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — set_privs FAILED ch=%s from 0x%016llx",
CA_ID, ch_id, (unsigned long long)from_node);
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — rollback left transaction open ch=%s; restart required", CA_ID, ch_id);
inst->running = 0;
}
queue_dgram_free(entry); queue_entry_free(entry); return;
}
@@ -120,8 +136,8 @@ void chat_admin_destroy(struct UTUN_INSTANCE* inst) {
/* ─── передача прав (вызывается из uasync-потока) ─── */
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !inst || !cc->db) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — not initialized", CA_ID);
return;
}
@@ -151,7 +167,7 @@ void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uin
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — queue_entry_new failed", CA_ID); return; }
entry->dgram = u_malloc(1 + sizeof(payload));
if (!entry->dgram) { queue_entry_free(entry); return; }
if (!entry->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — payload allocation failed", CA_ID); queue_entry_free(entry); return; }
entry->dgram[0] = ETCP_RT_ID_ADMIN_KEY;
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
+8 -3
View File
@@ -2,9 +2,13 @@
* chat_admin.h — передача канального приватного ключа (прав админа) другому узлу
*
* Владелец канала (имеющий ed25519_privkey + x25519_privkey) может передать оба ключа
* выбранному мемберу. Передача идёт через сервисный пакет etcp_router с
* выбранному узлу. Передача идёт через сервисный пакет etcp_router с
* ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT: X25519 ECDH + AES-128-CCM и Ed25519-подпись.
* Получатель автоматически импортирует ключи и становится совладельцем канала.
* Получатель сверяет выведенные публичные ключи с уже известными ключами канала,
* сохраняет оба privkey атомарно и после commit синхронно публикует CHAT_EVT_ADMIN_KEY_RECEIVED.
* Ошибка SQL/crypto сохраняет прежнюю пару без события успеха; корректный повтор допустим.
* Внешняя SQL-транзакция отклоняет входящий импорт; незавершённый rollback требует restart.
* Модуль включается/отключается chat_sync; все вызовы — в потоке uasync.
*/
#ifndef CHAT_ADMIN_H
@@ -18,8 +22,9 @@ extern "C" {
struct UTUN_INSTANCE;
/* Зарегистрировать/снять обработчик сервиса ETCP_RT_ID_ADMIN_KEY. */
/* Зарегистрировать ETCP_RT_ID_ADMIN_KEY; 0/-1. inst обязателен, повтор заменяет callback. */
int chat_admin_init(struct UTUN_INSTANCE* inst);
/* Снять обработчик; NULL допустим, отсутствие регистрации логирует router. */
void chat_admin_destroy(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
+1065 -256
View File
File diff suppressed because it is too large. Load diff
+47 -15
View File
@@ -2,7 +2,21 @@
* send готовит контейнер и публикует сообщение; downloaded/want запускают получение ключа и расшифровку.
* Автор отвечает на запрос через decide. Ключи и очередь доставки сохраняются в локальной БД.
* Все вызовы, кроме identity, — uasync. init после media_async; quiesce до остановки workers, destroy после них.
* Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон. */
* Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон.
* Ответы REQUEST_ACK/GRANT/DENY/RESULT_ACK атомарно меняют ключ, запрос и outbox собственной транзакцией.
* Внешняя транзакция для ответов запрещена. GRANT/DENY подтверждаются только после проверенного commit;
* повтор с тем же результатом допустим, конфликтующий ключ/решение отклоняется. Отказ SQL сохраняет прежнее состояние.
* Приём REQUEST владеет собственной транзакцией проверки идентичности/прав и сохранения запроса/повтора ответа.
* Внешняя транзакция отклоняется без изменений; ACK/событие — после commit. Дубликат сохраняет решение и дату:
* state=0 ждёт автора, 2/4 повторяют GRANT, 3 повторяет DENY; чужое направление и другие состояния отклоняются.
* Исходящий REQUEST сохраняется собственной транзакцией request+outbox после повторной проверки состояния.
* Уведомление/отправка следуют после commit; внешняя транзакция откладывает весь проход файлов и очереди.
* Перед сетевой попыткой отдельно фиксируется retry_at; отказ SQL не отправляет команду и сохраняет очередь/дату.
* Пустая очередь завершается без commit hook. retry_at отодвигается на5с; tick каждые10с, до16 команд за проход.
* Расшифровка стартует после commit decrypting=1. До commit метаданных завершение остаётся у модуля:
* таймер повторяет SQL без нового worker; не более 16 активных/ожидающих завершения файлов на instance.
* quiesce останавливает повторы, destroy после workers освобождает также отложенные завершения.
* Незавершённый rollback отключает модуль и запрашивает остановку ядра владельцем вне callback. */
#ifndef CHAT_ATTACHMENT_H
#define CHAT_ATTACHMENT_H
#include "../media_async/attachment.h"
@@ -14,45 +28,61 @@ struct UTUN_INSTANCE;
// ── Жизненный цикл модуля ──
// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1.
// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1. Внешняя транзакция запрещена.
int chat_attachment_init(struct UTUN_INSTANCE* inst);
// Запретить новые подготовки и остановить таймер; запущенные workers завершаются отдельно.
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst);
// После завершения workers убрать незавершённые подготовки и отключить router-сервис.
// После workers убрать незавершённые подготовки; открытая транзакция откладывает recovery, сохраняя файлы.
void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
// ── Отправка и учёт вложений ──
/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули.
* Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки.
* -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */
* -1 — отказ до принятия, source остаётся у вызывающего, события MSG_SUBMITTED здесь нет.
* Ключ/UUID/метаданные фиксируются собственной SQL-транзакцией до передачи source и запуска worker.
* Внешняя транзакция запрещена и остаётся нетронутой при отказе. Ошибка rollback с открытой транзакцией
* запрещает новые задачи модуля и запрашивает остановку ядра; уничтожение выполняет владелец вне callback.
* Ненулевой request возвращается в единственном CHAT_EVT_MSG_SUBMITTED; отказ media_async_submit может
* дать его синхронно, до возврата 0. Commit не усиливает установленную ядром политику SQLite synchronous. */
/* type проверяется encrypted_file_validate_type до выделения задачи, генерации UUID/ключа и записи в БД. */
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий.
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1, id меняется только при успехе. Подпись проверяет вызывающий.
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД.
/* Собственной транзакцией связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1.
* Массивы копируются в БД. Повтор той же пары допустим, её изменение запрещено; отказ сохраняет прежние поля.
* Внешняя транзакция отклоняется без изменений; все SQL-этапы и commit проверяются. */
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
const uint8_t message[32], const uint8_t hash[32]);
// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются.
/* Учесть проверенное db_sync сообщение собственной транзакцией, не меняя ключ/состояние существующей идентичности.
* 0 — записано/точный дубликат/обычное сообщение, -1 — SQL/OOM-отказ для повтора, -2 — неверный manifest/коллизия.
* Внешняя транзакция запрещена. Подпись проверяет вызывающий; JSON заимствован до возврата. */
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
uint64_t author, const char* json, size_t length);
// ── Получение файла и выдача доступа ──
// После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать.
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
/* После загрузки/наличия сохранённого контейнера: своей транзакцией сохранить downloaded и policy wanted.
* 0/-1; запись observe обязательна. Повтор неизменного состояния не даёт события. Криптографию проверит worker.
* verified_download!=0 — новая проверенная передача, также сбрасывает прежний error; 0 — найденный cache, error сохраняется.
* Внешняя транзакция запрещена, SQL-отказ сохраняет прежние поля, событие — после commit. */
int chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], int verified_download);
// Ручная загрузка на storage-узле также означает запрос пользовательского доступа.
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
/* Ручная загрузка, включая storage: одной транзакцией установить wanted, сбросить error и ready отсутствующего cache.
* 0/-1; запись observe обязательна. Внешняя транзакция запрещена, отказ сохраняет прежние поля. */
int chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
/* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать.
* 0 — решение и исходящий ответ записаны в БД, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. */
* 0 — собственная транзакция решения+outbox завершена, затем послано одно уведомление изменения;
* -1 — ошибка/нет прав или ожидающего запроса, прежнее состояние сохранено. Доставка идёт позже.
* Внешняя транзакция отклоняется без изменений; проверка прав выполняется внутри собственной.
* Открытая после отказа rollback транзакция требует остановки/закрытия БД владельцем после poll. */
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve);
// ── Команда интерфейса ──
@@ -64,13 +94,15 @@ struct chat_attachment_decision {
uint64_t peer;
int approve;
};
// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED.
// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; успех уже уведомлён decide, ошибка — событие с success=0.
void chat_attachment_decision_trampoline(void* arg);
// ── Локальное удаление канала ──
// Канал удаляется локально: отменить очереди, стереть ключи и частные копии.
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
/* После commit удаления канала убрать собственные UUID.file; 0/-1, при I/O-ошибке можно повторить.
* Ключи/запросы/outbox удаляет SQLite-trigger атомарно с channels; SQL-отказ сохраняет все записи.
* Внешняя транзакция или существующий канал запрещают файловую очистку; завершения workers видят удаление через БД. */
int chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
#ifdef __cplusplus
}
+203 -163
View File
@@ -32,123 +32,165 @@
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/crypto.h>
#include <errno.h>
#include <stdlib.h>
/* ─── Ed25519 подпись join-сообщения ─── */
uint64_t chat_channel_id(const char* text) {
if (!text) return 0;
size_t length = strnlen(text, 20);
if (!length || length == 20 || text[0] == '0' || strspn(text, "0123456789") != length) return 0;
uint64_t id = strtoull(text, NULL, 10); return id <= INT64_MAX ? id : 0;
}
static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519,
uint64_t node_id, const uint8_t* node_pubkey,
uint64_t join_ts, const uint8_t* ed25519_privkey,
uint8_t sig_out[64])
{
uint8_t msg[128];
int mlen = member_sync_build_join_msg(ch_x25519, ch_ed25519, node_id, node_pubkey, join_ts,
msg, (int)sizeof(msg));
if (mlen < 0) { memset(sig_out, 0, 64); return -1; }
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
ed25519_privkey, 32);
if (!pkey) { memset(sig_out, 0, 64); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); memset(sig_out, 0, 64); return -1; }
int ok = EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1
&& EVP_DigestSign(mdctx, sig_out, &(size_t){64}, msg, (size_t)mlen) == 1;
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
if (!ok) memset(sig_out, 0, 64);
return ok ? 0 : -1;
/* Один формат описания для создания и проверки подписи входящего JOIN/invite. */
int chat_channel_description_data(const char* channel, const char* name, uint64_t owner,
const uint8_t x25519[32], const uint8_t ed25519[32], uint8_t* out, size_t capacity) {
if (!channel || !name || !x25519 || !ed25519 || !out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel description: missing argument"); return -1;
}
size_t channel_len = strnlen(channel, 20), name_len = strnlen(name, 256);
if (!chat_channel_id(channel) || name_len == 256 || capacity < channel_len + name_len + 74) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel description: invalid channel/name lengths=%zu/%zu capacity=%zu",
channel_len, name_len, capacity); return -1;
}
size_t offset = channel_len + 1; memcpy(out, channel, offset);
memcpy(out + offset, name, name_len + 1); offset += name_len + 1;
memcpy(out + offset, &owner, 8); offset += 8;
memcpy(out + offset, x25519, 32); offset += 32;
memcpy(out + offset, ed25519, 32); offset += 32;
return (int)offset;
}
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
if (chat_feedback_ensure_channel(inst, ch_id) < 0) return;
if (si_find(inst, ch_id)) return;
if (topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return;
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !chat_channel_id(ch_id) || !inst->topo_groups) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel preparation without active chat/topology"); return -1;
}
if (chat_feedback_ensure_channel(inst, ch_id) < 0 || topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return -1;
uint64_t gid = strtoull(ch_id, NULL, 10);
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(inst, g);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
int created_group = !group;
if (!group) group = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (!group || member_sync_subscribe_group(inst, group) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel group preparation failed ch=%s", ch_id);
if (group && created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid);
if (si) {
si_register(inst, si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: db_sync_instance_add failed for ch=%s",
CC_ID, ch_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
int created_sync = !si;
char table[80]; msg_table_name(ch_id, table, sizeof(table));
if (!si) si = db_sync_instance_add(inst, table, gid);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel sync allocation failed ch=%s", ch_id);
if (created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
if (created_sync) db_sync_set_insert_cb(si, on_msg_inserted, inst);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "channel prepared ch=%s new_group=%d new_sync=%d", ch_id, created_group, created_sync);
return 0;
}
/* ─── создание канала ─── */
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel NOT INITIALIZED ch=%s",
CC_ID, req ? req->channel_id : "(null)");
return;
int chat_core_create_channel(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !inst->topo_groups || !req || req->owner_node_id != inst->node_id ||
strnlen(req->name, sizeof(req->name)) == sizeof(req->name)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: invalid context/owner/name"); return -1;
}
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel req=NULL", CC_ID); return; }
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: external transaction rejected"); return -1;
}
uint8_t description[CHAT_CHANNEL_DESCRIPTION_MAX], public_key[32];
int bytes = chat_channel_description_data(req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->ed25519_pubkey, description, sizeof(description));
if (bytes < 0 || sc_compute_public_key_from_private(req->x25519_privkey, public_key) != SC_OK ||
memcmp(public_key, req->x25519_pubkey, 32)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: invalid description or X25519 key pair"); return -1;
}
EVP_PKEY* ed = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, req->ed25519_privkey, 32);
size_t public_size = 32;
int valid_pair = ed && EVP_PKEY_get_raw_public_key(ed, public_key, &public_size) == 1 && public_size == 32;
EVP_PKEY_free(ed);
if (!valid_pair || memcmp(public_key, req->ed25519_pubkey, 32) ||
sc_ed25519_verify(req->ed25519_pubkey, description, (size_t)bytes, req->signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: invalid Ed25519 key pair/description signature ch=%s", req->channel_id); return -1;
}
uint64_t group_id = strtoull(req->channel_id, NULL, 10);
if (topo_groups_find(inst->topo_groups, group_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: group ID already in use ch=%s", req->channel_id); return -1;
}
time_t now = ntp_time_get_seconds(inst);
if (now <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: invalid join time ch=%s", req->channel_id); return -1; }
uint64_t timestamp = (uint64_t)now;
uint8_t join[64], root[64], update[64], body[MS_USERINFO_MAX + 73]; char profile[MS_USERINFO_MAX + 1];
bytes = member_sync_build_join_msg(req->x25519_pubkey, req->ed25519_pubkey, inst->node_id,
inst->my_keys.public_key, timestamp, body, sizeof(body));
if (bytes < 0 || sc_ed25519_sign(inst->my_ed25519_privkey, body, (size_t)bytes, join) != SC_OK ||
member_sync_sign_pubkey(req->ed25519_privkey, inst->my_keys.public_key, root) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: root JOIN/invitation signing failed ch=%s", req->channel_id); return -1;
}
if (chat_core_name_userinfo(inst->name, profile, sizeof(profile))) return -1;
bytes = member_sync_build_update_msg(join, timestamp, profile, body, sizeof(body));
if (bytes < 0 || sc_ed25519_sign(inst->my_ed25519_privkey, body, (size_t)bytes, update) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: root profile signing failed ch=%s", req->channel_id); return -1;
}
sqlite3_stmt* st = NULL; const char* stage = "begin";
int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* channel creation */", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: begin ch=%s rc=%d: %s", req->channel_id, rc, sqlite3_errmsg(cc->db)); return -1;
}
stage = "lookup channel";
rc = sqlite3_prepare_v2(cc->db, "SELECT 1 FROM channels WHERE channel_id=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, req->channel_id, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) { stage = "channel already exists"; rc = SQLITE_CONSTRAINT; goto error; }
if (rc != SQLITE_DONE) goto error;
stage = "lookup finalize"; rc = sqlite3_finalize(st); st = NULL;
if (rc != SQLITE_OK) goto error;
stage = "store description";
if (topo_node_sqlite_channel_put(cc->db, req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->x25519_privkey, req->ed25519_pubkey, req->ed25519_privkey, req->signature)) {
rc = SQLITE_ERROR; goto error;
}
stage = "feedback schema";
if (chat_feedback_ensure_channel(inst, req->channel_id)) { rc = SQLITE_ERROR; goto error; }
stage = "store root member";
int changes = member_sync_put(inst, req->channel_id, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
join, timestamp, update, timestamp, profile, NULL, NULL, 0, 0, root);
if (changes < 0 || !(changes & MS_APPLY_CHANGED)) { rc = SQLITE_ERROR; goto error; }
stage = "verify stored root";
if (member_sync_author_key(inst, req->channel_id, inst->node_id, public_key) != MS_AUTHOR_ACTIVE ||
memcmp(public_key, inst->my_ed25519_pubkey, 32)) { rc = SQLITE_CONSTRAINT; goto error; }
stage = "commit"; rc = sqlite3_exec(cc->db, "COMMIT /* channel creation */", NULL, NULL, NULL);
if (rc != SQLITE_OK) goto error;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel BEGIN ch=%s name=%s",
CC_ID, req->channel_id, req->name);
int rc = topo_node_sqlite_channel_put(cc->db,
req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->x25519_privkey,
req->ed25519_pubkey, req->ed25519_privkey,
req->signature);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel_put FAILED ch=%s rc=%d",
CC_ID, req->channel_id, rc);
} else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel_put OK ch=%s name=%s owner=0x%016llx",
CC_ID, req->channel_id, req->name, (unsigned long long)req->owner_node_id);
chat_core_ensure_channel_ready(inst, req->channel_id);
uint8_t ch_id_len = (uint8_t)strlen(req->channel_id);
uint8_t data[65];
data[0] = ch_id_len;
memcpy(data + 1, req->channel_id, ch_id_len);
{
uint64_t myid = inst->node_id;
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
uint8_t join_sig[64];
if (channel_sign_join(req->x25519_pubkey, req->ed25519_pubkey, myid, inst->my_keys.public_key,
join_ts, inst->my_ed25519_privkey, join_sig) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: root join signing failed ch=%s", CC_ID, req->channel_id); return;
}
/* корень дерева приглашений: подписан ключом группы (канальный ed25519) */
uint8_t root_sig[64];
struct ms_member_rec root = { .node_id = myid, .x25519 = inst->my_keys.public_key, .signature = root_sig };
if (member_sync_sign_pubkey(req->ed25519_privkey, inst->my_keys.public_key, root_sig) < 0
|| member_sync_verify_signature(cc->db, req->channel_id, &root) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: root invitation signing failed ch=%s", CC_ID, req->channel_id); return;
}
int mrc = member_sync_put(inst, req->channel_id, myid,
inst->my_keys.public_key, inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, "{}", NULL, NULL, 0, 0, root_sig);
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
}
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member(inst);
int ready = !chat_core_ensure_channel_ready(inst, req->channel_id);
if (!ready) DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel create: committed ch=%s, runtime activation deferred; retry preparation", req->channel_id);
member_sync_record_committed(inst, req->channel_id, inst->node_id, (unsigned)changes);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "channel created: ch=%s name=%s root=%016llx committed=1 runtime_ready=%d",
req->channel_id, req->name, (unsigned long long)inst->node_id, ready);
uint8_t event[21]; size_t length = strlen(req->channel_id); event[0] = (uint8_t)length;
memcpy(event + 1, req->channel_id, length); chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, event, (int)length + 1);
return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: ch=%s stage=%s rc=%d (%s) db=%s",
req->channel_id, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(cc->db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: cleanup finalize ch=%s rc=%d", req->channel_id, rc);
if (!sqlite3_get_autocommit(cc->db)) {
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: rollback ch=%s rc=%d: %s",
req->channel_id, rc, sqlite3_errmsg(cc->db));
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel create: transaction remains open ch=%s; requesting owner stop/DB close", req->channel_id);
utun_instance_stop(inst);
}
chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);
}
return -1;
}
void chat_core_create_channel_trampoline(void* arg) {
@@ -156,6 +198,7 @@ void chat_core_create_channel_trampoline(void* arg) {
struct chat_channel_create* req = (struct chat_channel_create*)arg;
if (!req) return;
chat_core_create_channel(req->inst, req);
OPENSSL_cleanse(req, sizeof(*req));
u_free(req);
}
@@ -166,62 +209,51 @@ void chat_core_create_channel_auto_trampoline(void* arg) {
u_free(arg);
}
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !name) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel_auto — not initialized or NULL name", CC_ID);
return;
/* Генерация двух независимых типов ключей с проверкой всех стадий OpenSSL. */
static int channel_generate_keypair(int type, uint8_t public_key[32], uint8_t private_key[32]) {
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(type, NULL); EVP_PKEY* key = NULL;
const char* stage = "context"; size_t public_size = 32, private_size = 32;
if (!ctx) goto error;
stage = "initialize"; if (EVP_PKEY_keygen_init(ctx) != 1) goto error;
stage = "generate"; if (EVP_PKEY_keygen(ctx, &key) != 1) goto error;
stage = "public key";
if (EVP_PKEY_get_raw_public_key(key, public_key, &public_size) != 1 || public_size != 32) goto error;
stage = "private key";
if (EVP_PKEY_get_raw_private_key(key, private_key, &private_size) != 1 || private_size != 32) goto error;
EVP_PKEY_free(key); EVP_PKEY_CTX_free(ctx); return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel key generation: type=%d stage=%s public_bytes=%zu private_bytes=%zu",
type, stage, public_size, private_size);
EVP_PKEY_free(key); EVP_PKEY_CTX_free(ctx); return -1;
}
uint64_t chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !inst->topo_groups || !name || strnlen(name, 128) == 128) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel auto create: invalid context/name or name exceeds127 bytes"); return 0;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel_auto BEGIN name=%s", CC_ID, name);
uint8_t x25519_pub[32] = {0}, x25519_priv[32] = {0};
{ EVP_PKEY* pkey = NULL; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); }
if (pkey) { size_t pkLen = 32, skLen = 32; EVP_PKEY_get_raw_public_key(pkey, x25519_pub, &pkLen); EVP_PKEY_get_raw_private_key(pkey, x25519_priv, &skLen); EVP_PKEY_free(pkey); } }
uint8_t ed25519_pub[32] = {0}, ed25519_priv[32] = {0};
EVP_PKEY* ed_pkey = NULL;
{ EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &ed_pkey); EVP_PKEY_CTX_free(ctx); }
if (ed_pkey) { size_t pkLen = 32, skLen = 32; EVP_PKEY_get_raw_public_key(ed_pkey, ed25519_pub, &pkLen); EVP_PKEY_get_raw_private_key(ed_pkey, ed25519_priv, &skLen); } }
uint64_t chId = 0; RAND_bytes((uint8_t*)&chId, sizeof(chId));
chId &= 0x7FFFFFFFFFFFFFFFULL; /* как node_id — всегда < 2^63 (помещается в signed Long) */
if (chId == 0) chId = 1;
char channelId[64]; snprintf(channelId, sizeof(channelId), "%llu", (unsigned long long)chId);
uint8_t sig[64] = {0};
if (ed_pkey) {
size_t nameLen = strlen(name);
size_t chIdLen = strlen(channelId);
size_t msgLen = chIdLen + 1 + nameLen + 1 + 8 + 32 + 32;
uint8_t* msg = u_malloc(msgLen);
if (msg) {
uint8_t* p = msg;
memcpy(p, channelId, chIdLen); p += chIdLen; *p++ = '\0';
memcpy(p, name, nameLen); p += nameLen; *p++ = '\0';
memcpy(p, &cc->my_node_id, 8); p += 8;
memcpy(p, x25519_pub, 32); p += 32;
memcpy(p, ed25519_pub, 32);
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) { EVP_DigestSignInit(mdctx, NULL, NULL, NULL, ed_pkey); size_t sl = 64; EVP_DigestSign(mdctx, sig, &sl, msg, msgLen); EVP_MD_CTX_free(mdctx); }
u_free(msg);
}
EVP_PKEY_free(ed_pkey);
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel auto create: external transaction rejected before key generation"); return 0;
}
struct chat_channel_create* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channelId);
snprintf(req->name, sizeof(req->name), "%s", name);
req->owner_node_id = cc->my_node_id;
memcpy(req->x25519_pubkey, x25519_pub, 32); memcpy(req->x25519_privkey, x25519_priv, 32);
memcpy(req->ed25519_pubkey, ed25519_pub, 32); memcpy(req->ed25519_privkey, ed25519_priv, 32);
memcpy(req->signature, sig, 64);
chat_core_create_channel(inst, req);
u_free(req);
struct chat_channel_create req = {.inst = inst, .owner_node_id = inst->node_id};
uint8_t description[CHAT_CHANNEL_DESCRIPTION_MAX]; uint64_t group_id = 0; int result = -1;
if (channel_generate_keypair(EVP_PKEY_X25519, req.x25519_pubkey, req.x25519_privkey) ||
channel_generate_keypair(EVP_PKEY_ED25519, req.ed25519_pubkey, req.ed25519_privkey)) goto done;
if (RAND_bytes((uint8_t*)&group_id, sizeof(group_id)) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel auto create: ID generation failed"); goto done;
}
group_id &= INT64_MAX; if (!group_id) group_id = 1;
snprintf(req.channel_id, sizeof(req.channel_id), "%llu", (unsigned long long)group_id);
memcpy(req.name, name, strlen(name) + 1);
int bytes = chat_channel_description_data(req.channel_id, req.name, req.owner_node_id,
req.x25519_pubkey, req.ed25519_pubkey, description, sizeof(description));
if (bytes < 0 || sc_ed25519_sign(req.ed25519_privkey, description, (size_t)bytes, req.signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel auto create: description signing failed ch=%s", req.channel_id); goto done;
}
result = chat_core_create_channel(inst, &req);
done:
OPENSSL_cleanse(&req, sizeof(req));
return result == 0 ? group_id : 0;
}
/* ─── управление подключением к каналу (выбор в GUI) ─── */
@@ -321,12 +353,15 @@ void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — not initialized", CC_ID);
return;
}
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — empty ch_id", CC_ID); return; }
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel rejected inside external transaction ch=%s", CC_ID, ch_id); return;
}
if (chat_media_defer_channel_delete(inst, ch_id)) return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel BEGIN ch=%s", CC_ID, ch_id);
@@ -334,7 +369,6 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
/* 1. отмена медиа-загрузок/релэев/суперузлов канала (держат conn_mgr handle группы) */
media_delivery_cancel_channel(inst, gid);
chat_attachment_delete_channel(inst, ch_id);
member_files_cancel_group(inst, gid);
group_avatar_cancel_group(inst, gid);
@@ -354,16 +388,22 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
/* 5. немедленно убрать канал из кеша chat_sync */
chat_sync_on_channel_deleted(inst, ch_id);
/* 6. удалить медиафайлы канала на диске + строки media_files */
char media_base[MEDIA_BASE_MAX];
int media_error = media_index_get_base(cc->db, media_base) || media_index_delete_channel(cc->db, ch_id, media_base);
if (media_error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "channel=%s removed with incomplete media cleanup; retry required", ch_id);
/* 7. удалить таблицы и метаданные канала */
/* 6. удалить таблицы, метаданные и attachment-записи одной SQL-транзакцией до файловой очистки */
if (topo_node_sqlite_channel_delete(cc->db, ch_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel metadata deletion failed ch=%s", ch_id); return;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel metadata deletion failed ch=%s; files preserved, retry required", ch_id);
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel delete left transaction open ch=%s; requesting owner stop", ch_id);
chat_attachment_quiesce(inst); utun_instance_stop(inst);
}
return;
}
/* 7. удалить файлы только после commit; I/O-отказ не восстанавливает удалённый канал */
int media_error = chat_attachment_delete_channel(inst, ch_id);
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base) || media_index_delete_channel(cc->db, ch_id, media_base)) media_error = 1;
if (media_error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "channel=%s removed with incomplete media cleanup; retry required", ch_id);
/* 8. уведомить GUI */
{
uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch_id);
+637 -438
View File
File diff suppressed because it is too large. Load diff
+156 -68
View File
@@ -21,6 +21,10 @@
struct sqlite3;
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
// ── Жизненный цикл ──
// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1.
@@ -30,24 +34,32 @@ int chat_service_start(struct UTUN_INSTANCE* inst);
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
void chat_service_stop(struct UTUN_INSTANCE* inst);
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
/* Внутренняя часть запуска:заимствовать БД работающего ядра,сохранить метаданные и подготовить каналы. 0/-1.
* Собственная транзакция включает schema/identity/media_base и установку обязательных подписок;чужая запрещена.
* db_path<=511 байт задаёт default media base,отдельная БД не открывается. Готовность публикуется после commit/activation.
* Activation error сохраняет committed метаданные;caller откатывает runtime через chat_service_stop и может повторить старт.
* Для запуска всего чат-сервиса использовать chat_service_start(). */
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра.
void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
* в media_files) — вызывается после загрузки каналов/сообщений при старте. */
/* Startup: восстановить производный attachment-индекс из подписанной истории и зарегистрировать сохранённые медиа.
* Вызывать после инициализации attachment и startup_backfill, чтобы SQL-отказ мог поставить повтор. Ключи из истории не восстанавливаются. */
void chat_media_backfill(struct UTUN_INSTANCE* inst);
/* Докачка недостающего медиа за последние storage_backfill_days дней. Проходит по
* медиасообщениям каналов и запускает загрузку для тех, чей файл отсутствует на диске. */
/* Докачка недостающего медиа за последние storage_backfill_days дней и применение квоты.
* Также восстанавливает attachment-индекс/доступность сохранённых контейнеров; SQL-отказ ставит повтор.
* Перед удалением по квоте/очисткой сирот требуется полный снимок ссылок;ошибка SQL/памяти отменяет удаление.
* Один путь учитывается один раз,с самой новой ссылкой;derived plaintext входит в общий кеш. */
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
/* Подписать завершения Merkle: первый обмен запускает общий scan после 2с тишины; отдельные каналы — batch до16 через2с.
* Повтор индекса не отменяет MSG_RECEIVED committed-истории; следующий batch может быть поставлен во время текущего.
* При переполнении очереди и для восстановления допускаются последующие общие scan.
* init:0 — обе подписки установлены/повтор,-1 — отказ с отменой частичной установки.
* Destroy снимает подписки/таймеры;вызывать до освобождения chat_core. */
int chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
@@ -74,19 +86,33 @@ struct chat_msg_submit {
uint32_t data_len; /* размер data в байтах */
uint64_t timestamp; /* успешная отправка возвращает назначенное время */
uint8_t message_id[32]; /* успешная обычная отправка возвращает общий ID */
uint64_t expected_ver; /* >0: trampoline редактирует message_id вместо новой отправки. */
uint8_t reply_to[32]; /* общий ID оригинала в этом канале; нули = не ответ */
uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */
};
// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1.
/* Подписать,сохранить и разослать:0/-1,req/data остаются у caller.
* channel/content_type — NUL-строки в пределах массивов,канал канонический;data<=4000 байт без NUL.
* Итоговый экранированный JSON также должен помещаться в4000 байт. SQL JSON/bind/finalize проверяются до подписи/записи. */
int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Правка собственного text: ожидаемая версия защищает открытый редактор от устаревшего сохранения.
* 0 — сохранено/текст прежний, -1 — ошибка, -2 — версия изменилась. ID и порядок сообщения сохраняются. */
int chat_core_edit_message(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
uint64_t expected_ver, const char* text, size_t len);
/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0.
* req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки.
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием.
* Необязательный complete(arg,success) вызывается ровно один раз после сохранения/отказа и освобождения задачи,
* в том числе синхронно при отказе запуска;arg принадлежит caller и живёт до callback. */
typedef void (*chat_media_submitted_fn)(void* arg, int success);
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req,
chat_media_submitted_fn complete, void* arg);
// Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора.
/* Ручная загрузка по локальному номеру строки; внешняя SQL-транзакция запрещена.
* Manifest ограничен общим DB_SYNC_MESSAGE_MAX. Зашифрованный индекс восстанавливается из истории до wanted/загрузки;
* отсутствие plaintext сбрасывает ready с сохранением ключа. Отказ SQL логируется, состояние модуля атомарно сохраняется. */
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id);
// ── DB-операции (оставлены для интроспекции) ──
@@ -94,48 +120,57 @@ void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* chann
// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса.
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта.
* 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
* Списки ограничены буфером/count<=65535;out_len — записанные байты,числа native.
* SQL читается целиком даже после заполнения буфера;отказ сохраняет buf и обнуляет допустимый out_len.
* Каналы идут по created_at,channel_id. */
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1.
// Неудалённые участники:count:u16,node_id:u64;unsigned ID по возрастанию;канал канонический,остальной контракт как у list_channels.
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
/* Ключи X25519/Ed25519 (по32 байта,NULL в БД→нули),count:u8,затем адреса:
* family:u8,protocol:u8,address_len:u8,address[4/16],port:u16,rtt:i16;числа native,RTT насыщается.
* buf_size>=65;не более255 целых первых адресов в детерминированном порядке БД.
* 0 — прочитано,-1 — нет узла/ошибка,buf сохраняется,out_len=0. Проверяется весь SQL-снимок. */
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
// ── Создание канала (GUI → uasync) ──
struct chat_channel_create {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char name[128];
uint64_t owner_node_id;
uint8_t x25519_pubkey[32];
uint8_t x25519_privkey[32];
uint8_t ed25519_pubkey[32];
uint8_t ed25519_privkey[32];
uint8_t signature[64];
struct UTUN_INSTANCE* inst; /* Заимствован trampoline; прямой вызов использует свой аргумент inst. */
char channel_id[64]; /* NUL-строка: каноническое десятичное число 1..INT64_MAX. */
char name[128]; /* NUL-строка до127 байт, без молчаливого обрезания. */
uint64_t owner_node_id; /* node_id создающего instance. */
uint8_t x25519_pubkey[32]; /* Канальный ключ шифрования; должен соответствовать privkey. */
uint8_t x25519_privkey[32];/* Независимый приватный X25519-ключ канала. */
uint8_t ed25519_pubkey[32];/* Канальный ключ подписи; должен соответствовать raw seed. */
uint8_t ed25519_privkey[32];/* Raw Ed25519 seed, без преобразования sc_derive_ed25519_pubkey. */
uint8_t signature[64]; /* Подпись id\0 || name\0 || owner(native u64) || X25519 || Ed25519. */
};
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
// Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт.
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Подготовить таблицы, группу и синхронизацию; 0/-1. Ошибка удаляет новые runtime-ресурсы,
* проверенные данные БД сохраняются. Членства на других узлах не даёт. */
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего.
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
/* Создать новый канал:проверить пары ключей/подпись,одним commit сохранить описание,root JOIN/профиль/приглашение и Merkle.
* 0 — сохранено,-1 — отказ без публикации;req заимствован и не меняется. Владелец — этот instance.
* Внешняя транзакция и занятый ID запрещены. CHANNEL_UPDATED — после commit и попытки runtime-подготовки.
* Отказ подготовки не отменяет созданный канал:можно повторить ensure_channel_ready. Неудачный rollback
* с открытой транзакцией запрашивает stop/закрытие БД владельцем вне callback. */
int chat_core_create_channel(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req);
// Забирает u_malloc-структуру chat_channel_create, очищает ключи и освобождает её после вызова.
void chat_core_create_channel_trampoline(void* arg);
// Сгенерировать ключи и создать канал, добавив себя владельцем.
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
/* Сгенерировать независимые пары X25519/Ed25519 и создать канал. ID>0 — сохранён,0 — отказ.
* name — NUL-строка<=127байт,не обрезается. Проверяет RNG/генерацию/извлечение/подписи;
* временные приватные ключи очищаются. Внешняя транзакция отклоняется до генерации. */
uint64_t chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
// Забирает и освобождает u_malloc-структуру chat_create_auto_arg; instance должен дожить до выполнения.
void chat_core_create_channel_auto_trampoline(void* arg);
// ── Локальное удаление канала (GUI → uasync) ──
@@ -149,19 +184,23 @@ struct chat_delete_channel_req {
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют.
* Исходящие video/register задачи сначала завершаются; удаление продолжит последний completion.
* Новые исходящие медиа этого канала в это время отклоняются. Событие CHANNEL_DELETED приходит после удаления метаданных;
* ошибка файловой очистки логируется отдельно, записи неочищенных файлов сохраняются для повтора. */
* Новые исходящие медиа этого канала в это время отклоняются. Внешняя SQL-транзакция запрещена до отмены ресурсов.
* Ключи/запросы/outbox удаляются триггером атомарно с метаданными; при SQL-ошибке данные и файлы сохраняются,
* сетевые ресурсы уже могут быть отменены — требуется повтор. Открытая после ошибки транзакция требует stop владельцем.
* CHANNEL_DELETED приходит после commit метаданных и попытки файловой очистки. Её I/O-ошибка логируется отдельно;
* остатки media_files сохраняются, собственный каталог plaintext можно очистить повторно после commit. */
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_delete_channel_trampoline(void* arg);
// ── Утилиты ──
// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет.
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
// Сохранить имя узла и обновить собственные member-записи каналов.
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
/* Имя (NUL,<=127байт,включая пустое):0 — committed/no-op,-1 — отказ без изменения runtime.
* Один BEGIN IMMEDIATE сохраняет local_identity,nodes,подписанный NODEINFO и собственные полные member-блоки/Merkle.
* Ожидающий JOIN без полного member пропускается. No-op не меняет версии и не вызывает commit hook.
* Только после commit обновляются inst/LAN и идут NODEINFO/member уведомления;внешняя транзакция запрещена.
* Неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. Конфиг не сохраняется. */
int chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
@@ -173,24 +212,31 @@ void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
// Сохранение key-value в ui_state (вызывается из uasync-потока)
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
/* Сохранить key/value в uasync:0 — committed/no-op,-1 — отказ;key непустой,value допускает пустую строку.
* Собственная транзакция проверяет все SQL-этапы;внешняя запрещена,no-op не вызывает commit hook.
* Ошибка сохраняет прежнее значение;unrecovered rollback запрашивает owner stop/DB close. */
int chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
* get возвращает0/1,-1 при ошибке;отсутствие записи —1. set возвращает0/-1,
* после собственного commit/no-op рассылает желаемое значение через chat_sync best effort.
* SQL-отказ не рассылается,идентичный повтор повторяет доставку. ch_id — канонический ID канала. */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
int chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
void chat_core_set_sound_on_message_trampoline(void* arg);
/* Полный request принадлежит trampoline/u_free;data содержит key+NUL,value+NUL в пределах массива. */
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
void chat_core_save_ui_state_trampoline(void* arg);
// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync)
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
/* Установить через chat_setting:0 — успех,-1 — неизвестное имя,-2 — валидация,-3 — LAN apply pending.
* Только успешный group_autoconnect запускает пересмотр групп;остальные эффекты — в chat_setting.h. */
int chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
/* Trampoline владеет полным request/u_free;data — name+NUL,value+NUL,обе строки проверяются в границах. */
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_set_setting_trampoline(void* arg);
@@ -223,12 +269,16 @@ struct attachment_dl_req {
};
void chat_core_attachment_download_trampoline(void* arg);
// Обновление local_attrs для записи (вызывается из uasync-потока)
/* Заменить local_attrs указанной подписи/timestamp:0 — committed,-1 — отказ/нет записи/не JSON-object.
* Канал канонический,timestamp1..INT64_MAX,подпись64 байта. Собственная транзакция,чужая запрещена;
* ошибка bind/step/finalize/commit откатывается,незавершённый rollback запрашивает owner stop. */
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json);
// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs)
/* Добавить/заменить pl=1 через JSON,сохранив остальные local_attrs любой допустимой длины.
* NULL/пустые attrs становятся объектом;неверный объект не перезаписывается. Первая строка по id
* с timestamp/author;собственная транзакция как у update_local_attrs,результат/ошибки логируются. */
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
uint64_t author_node_id);
@@ -241,51 +291,70 @@ struct voice_played_req {
uint64_t author_node_id;
};
// Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst)
/* Сбор статуса (NTP + connections) в uasync. arg — заимствованный inst.
* Ответ CHAT_EVT_STATUS_REFRESH: UTF-8 без завершающего NUL в длине; размер растёт по данным.
* Текст заимствован только на время синхронного event callback; отказ памяти/форматирования логируется без события. */
void chat_core_collect_status_trampoline(void* arg);
// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD.
void chat_core_collect_cpu_load_trampoline(void* arg);
// Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst
/* Список до250 ETCP-соединений:arg — заимствованный inst,ответ CONN_LIST [count:u16 native][entry:46]*.
* Entry:peer:u64,flags:u8,links:u8,rtt:u16,inflight_KiB:u16,name:32;числа native,links/inflight насыщаются. */
void chat_core_collect_conn_list_trampoline(void* arg);
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg)
/* Полные метрики одного соединения, вызывается в uasync. arg — u_malloc struct chat_node_arg,
* trampoline освобождает arg; inst остаётся заимствованным. CHAT_EVT_CONN_METRICS содержит UTF-8
* без завершающего NUL в длине; буфер растёт по числу линков, при OOM нет частичного события.
* Данные ответа заимствованы только на время синхронного event callback. */
void chat_core_collect_conn_metrics_trampoline(void* arg);
// Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg)
void chat_core_collect_member_detail_trampoline(void* arg);
/* Диагностика карточки участника: запрос/codec находятся в chat_member.h. */
// ── Headless CLI API ──
// Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}]
/* JSON-списки:0 — полный JSON с NUL,-1 — ошибка;out_len без NUL,при отказе0,допустимый buf очищается.
* buf/out_len обязательны,buf_size>=3. SQL/память/переполнение не дают успешный префикс;
* Строки экранируются целиком;channels/members отклоняют скрытый NUL в канальных/профильных SQLite TEXT.
* Каналы:[{id,name,owner_id,owner_name,peers,msgs,online}];отложенная history-таблица означает msgs=0. */
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
* От новых к старым; count<=0 — до 100 записей, offset — сколько пропустить. */
* От новых к старым;count<=0 — до100 записей,offset>=0 — сколько пропустить.
* Канал — канонический ID1..INT64_MAX;общий контракт буфера как у channels_json. */
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/* Мемберы в JSON: [{node_id,name,online,connected,adm_tags,x25519,ed25519,addrs}].
* connected означает наличие транспорта в реестре, а не его UP или готовность группы. */
* connected означает наличие транспорта в реестре,а не его UP/готовность группы;deleted показывается offline.
* addrs.proto=UDP/TCP/TCP+REALITY,IPv4/IPv6 текстовые;канал/буфер как у messages/channels_json. */
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
// Имя узла из таблицы nodes
/* Имя nodes с NUL,при необходимости обрезанное до sz-1 байт:0 — прочитано/узел отсутствует,-1 — ошибка.
* out/sz>0 обязательны;отсутствие узла и ошибка оставляют пустую строку (если буфер допустим). */
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
// Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку.
/* Ссылка avatars/<sha256>.json внутри дерева своего UID/канала;server=0 удаляет ссылку,file остаётся обязательной NUL-строкой.
* 0 — committed/семантически прежнее,-1 — отказ;чужая транзакция запрещена. Storage-роль сервера проверяется под блокировкой. */
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file);
/* Построить {name:string},включая Unicode/control escaping;0/-1,при ошибке out не меняется. */
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
/* Прочитать собственный подписанный профиль:0/-1,при отказе все выходы сохраняются.
* Проверяет SQL,размеры,JOIN/update-подписи и целый плоский JSON с уникальными строковыми полями.
* out требует capacity>=длина+1,signature64,timestamp — версия update_ts в секундах/монотонных шагах. */
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
uint8_t signature[64], uint64_t* timestamp);
/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ.
* Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle.
* Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */
* Остальные подписанные поля сохраняются;неподписанные старые атрибуты не авторизуются новым обновлением.
* Имя/аватар меняются своими API. Чтение/проверка/версия/запись/Merkle защищены одним BEGIN IMMEDIATE.
* Семантический no-op сохраняет подпись/версию независимо от порядка ключей/escaping;ошибка не меняет БД/Merkle.
* Уведомления идут после commit,без SQLite cursor. Вызов в uasync вне чужой транзакции;
* неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. */
struct chat_profile_option { const char* key; const char* value; };
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
const struct chat_profile_option* options, size_t count);
@@ -304,26 +373,41 @@ void chat_core_set_my_avatar_trampoline(void* arg);
struct chat_member_tags;
// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет.
/* Загрузить проверенный owner-снимок полного участника:NULL при отсутствии/placeholder/SQL/подписи/формате.
* Объект принадлежит caller до free,используется в uasync и освобождается до остановки chat-сервиса. */
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit.
/* Изменить желаемое поле:0/-1;отказ не меняет editor. val=NULL удаляет поле;admin/moder принимают yes/no.
* Пустой ключ/ver запрещены,версию выбирает commit;место для ver резервируется в лимите16 ключей. */
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
// Получить значение ключа. Возвращает NULL если ключ не найден.
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
// Заменить все теги данными из JSON-строки (плоский, строковые значения).
/* Заменить желаемые теги:0/-1,при отказе editor не меняется. Весь объект<=8192 байта,уникальные
* непустые ключи,только строки;admin/moder yes/no. Необязательный ver обязан совпадать с исходным снимком. */
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
* записать через member_sync_put() → синхронизируется всем. */
/* Собственный BEGIN IMMEDIATE проверяет исходный owner-снимок и фиксирует новую подпись/версию/Merkle.
* 0 — committed/no-op,-2 — конфликт с изменившимся owner-блоком,-1 — прочий отказ;чужая транзакция запрещена.
* Отказ сохраняет editor/БД,успех обновляет editor для следующей правки. No-op сохраняет версию/подпись.
* Версия1..INT_MAX;admin/moder сохраняют историю,отключение дописывает d<seconds>. Приватный ключ очищается.
* Уведомления после commit без cursor;при неудачном rollback запрашивается stop/закрытие БД владельцем. */
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
/* uasync: атомарно сохранить локальный nickname и частичный owner_json для участника channel/node.
* Строки заимствованы на время вызова; nickname="" снимает псевдоним, owner_json="" сохраняет только ник.
* Непустой JSON использует правила tags_req ниже, включая проверку ver; нужен канальный приватный ключ для изменения прав.
* 0 — commit и MEMBERS_CHANGED, -1 — отказ, -2 — конфликт owner-снимка. Чужая транзакция запрещена.
* Ошибка не сохраняет частичный результат; SQL/подпись/rollback логируются, зависший rollback запрашивает stop. */
int chat_core_save_member(struct UTUN_INSTANCE* inst, const char* channel, uint64_t node,
const char* nickname, const char* owner_json);
// Освободить без сохранения.
void chat_member_tags_free(struct chat_member_tags* t);
// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память).
/* Трамплин для gui_bridge:владеет req/u_free. json — частичный объект желаемых полей,неуказанные сохраняются;
* необязательный ver проверяет снимок формы. Пустая строка — no-op;ошибки логируются без commit. */
struct chat_member_tags_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
@@ -335,6 +419,7 @@ void chat_member_tags_commit_trampoline(void* arg);
// ── Передача прав админа (владелец → другому узлу) ──
// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router).
// uasync-поток; NULL/неинициализированный inst отклоняется с ERROR. Постановка в транспорт не подтверждает импорт.
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
void chat_core_transfer_admin_trampoline(void* arg);
@@ -345,4 +430,7 @@ struct chat_transfer_admin_req {
uint64_t target_node_id;
};
#ifdef __cplusplus
}
#endif
#endif /* CHAT_CORE_H */
+64 -26
View File
@@ -2,7 +2,9 @@
* chat_core_priv.h — внутренний заголовок для под-модулей chat_core
*
* Предоставляет per-instance chat_core_ctx через CC(inst), общие SQL/сериализационные хелперы
* и внутренние операции частей chat_core/member_sync. Не включать извне chat/.
* и внутренние операции частей chat_core/member_sync. Не является frontend API.
* Все обращения — в uasync; контекст создаёт/освобождает chat_core. Сначала закрываются
* приём команд, callbacks и media workers, затем освобождается контекст; БД остаётся ядру.
*/
#ifndef CHAT_CORE_PRIV_H
@@ -19,8 +21,15 @@
#include <stdint.h>
#define CC_ID "chat_core"
#define CHAT_MEDIA_BACKFILL_CHANNELS 16
#define CHAT_CHANNEL_DESCRIPTION_MAX (20 + 256 + 8 + 32 + 32)
/* ── Base64 encode/decode (URL-safe, used for msg "d" field prefix) ── */
/* Base64url для префикса поля d:заимствованные непересекающиеся буферы.
* encode возвращает записанные байты,дописывает '=' для неполной тройки,без NUL;
* при нехватке места останавливается перед очередной четверкой.
* decode пропускает чужие символы,останавливается на '=',принимает хвост из2/3 символов;
* возвращает ограниченный dst_cap префикс,не валидирует каноничность или весь вход.
* Строгие потребители отдельно сверяют длину и повторное кодирование (chat_attachment_identity). */
static const char B64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -80,50 +89,79 @@ struct media_index_result;
struct ms_apply_cbk; /* member_sync.c */
struct group_avatar;
struct media_submit_ctx;
struct ak_file;
struct chat_whisper;
struct member_route_event;
struct chat_core_ctx {
struct UTUN_INSTANCE* inst;
sqlite3* db;
char db_path[512];
uint8_t shared_db;
uint64_t my_node_id;
uint8_t initialized;
/* per-instance sub-module state (was file-scope statics) */
struct ms_props_cbk* props_cbks; /* was member_sync.c g_props_cbks */
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
struct group_avatar* group_avatar;
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
struct UTUN_INSTANCE* inst; /* Заимствованный владелец;не уничтожается из callbacks чата. */
sqlite3* db; /* Заимствована у ядра;chat_core её не открывает/не закрывает. */
char db_path[512]; /* Копия пути init для идентичности запуска/default media base;не отдельное соединение БД. */
uint64_t my_node_id; /* node_id владельца,снимок при init. */
uint8_t initialized; /* Метаданные committed,подписки установлены;не означает готовность всего chat_service. */
struct chat_whisper* whisper; /* Ленивый владелец модели/одной задачи; stop отсоединяет до ожидания workers. */
struct member_route_event* member_route_events; /* Отложенные UI-снимки топологии по (канал,узел), без TOPO_* указателей. */
struct ms_props_cbk* props_cbks; /* Владение списком подписчиков свойств member_sync. */
unsigned props_dispatch_depth; /* снятие props-подписки откладывает free до выхода из вложенных callbacks */
struct ms_apply_cbk* apply_cbks; /* Владение списком уведомлений о принятии мембера. */
unsigned apply_dispatch_depth; /* снятые apply callbacks освобождаются после выхода из всех вложенных вызовов */
struct group_avatar* group_avatar; /* Контекст запросов/передач аватарки группы;освобождает group_avatar_destroy. */
uint64_t media_cache_bytes; /* Производный размер уникальных файлов/расшифрованных копий;сверяется полным scan. */
struct media_submit_ctx* media_submissions; /* исходящие задачи до последнего callback; удаление канала ждёт их */
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
void* media_channel_backfill_timer; /* debounce per-channel докачки */
char media_channel_backfill[16][64]; /* pending ch_id */
int media_channel_backfill_pending;
void* attachment_timer;
int attachment_closing;
int attachment_initialized;
void* media_backfill_timer;/* Общий scan после debounce истории либо повтор при отказе/переполнении batch. */
int media_backfill_ran; /* Первый общий scan уже запущен;последующие завершения не переносят его. */
struct UTUN_INSTANCE* media_backfill_inst; /* inst при установленных done-подписках,NULL после destroy;заимствован. */
void* media_channel_backfill_timer; /* ограниченный batch докачки/восстановления индекса через 2с от первой заявки */
char media_channel_backfill[CHAT_MEDIA_BACKFILL_CHANNELS][64]; /* pending ch_id */
int media_channel_backfill_pending; /* Число занятых строк batch,0..16. */
void* attachment_timer; /* Периодический pump запросов ключей/отложенных completion,1с. */
int attachment_closing; /* Quiesce запрещает новые операции и публикацию worker-результатов. */
int attachment_initialized; /* Зарегистрированы модели/обработчики attachment;не признак наличия ключей. */
struct ak_file* attachment_files; /* workers и завершения, ожидающие SQL commit; владеет chat_attachment */
unsigned attachment_file_count; /* максимум MEDIA_ASYNC_MAX_TASKS, включая отложенные завершения */
};
/* Per-instance chat context. inst->chat_core обязателен после chat_core_init. */
/* Заимствованный контекст или NULL;inst обязан быть ненулевым,макрос не проверяет lifecycle. */
#define CC(inst) ((inst)->chat_core)
/* ── Хелперы ── */
/* Внутренние имена таблиц:не валидатор ID. Ненулевые строки/буферы,out_sz>0;
* sanitize сохраняет ASCII alnum/_,остальное заменяет '_',обрезает до out_sz-1 и ставит NUL.
* table_name предварительно ограничивает очищенный ID63 байтами,добавляет msg_/peers_ через snprintf. */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz);
void msg_table_name(const char* ch_id, char* buf, size_t sz);
void peers_table_name(const char* ch_id, char* buf, size_t sz);
/* Тихая проверка канонического десятичного ID1..INT64_MAX;0 — отказ,NULL также недопустим.
* Использующий модуль логирует ошибку с контекстом своей операции. */
uint64_t chat_channel_id(const char* text);
/* Проверить весь JSON-object<=limit:уникальные ключи,только строковые значения,без скрытого NUL.
* SQLite заимствована;0/-1 с логом,callback может быть NULL. Callback получает строки до следующего step,
* строит только временный результат и возвращает0/-1;при поздней ошибке caller отбрасывает весь результат. */
int chat_json_strings(sqlite3* db, const char* text, size_t limit,
int (*visit)(const char* key, const char* value, void* arg), void* arg);
/* Байты подписи описания:канонический ID+NUL,имя<=255байт+NUL,owner native-u64,X25519/Ed25519 public[32].
* Возвращает длину/-1;аргументы обязательны,при отказе out не меняется. Общий формат create/JOIN/invite. */
int chat_channel_description_data(const char* channel, const char* name, uint64_t owner,
const uint8_t x25519[32], const uint8_t ed25519[32], uint8_t* out, size_t capacity);
/* ── db_sync registry (chat_core.c) ── */
/* Заимствованный зарегистрированный history instance канонического канала;NULL при отсутствии/неверном ID. */
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id);
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id);
/* ── db_sync callback (chat_msg.c) ── */
/* db_sync callback после commit:производный attachment-индекс,feedback/UI,live download и last_msg_at.
* data/message_id заимствованы до возврата;updated даёт MSG_UPDATED без звука/unread/повторной загрузки. */
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32], uint64_t record_ts,
const char* data, size_t len, uint64_t author, int initial_sync, void* arg);
/* Разобрать подписанный manifest канального медиа; result освобождается media_index_result_free. */
const char* data, size_t len, uint64_t author, int initial_sync, int updated, void* arg);
/* Проверить весь NUL-terminated manifest<=DB_SYNC_MESSAGE_MAX,hex,разделители и геометрию1..100 блоков.
* 0 — result владеет block_ids/block_sigs до media_index_result_free,-1 — формат/аргументы,-2 — память.
* При допустимых аргументах result обнуляется до разбора;подписи/содержимое файла здесь не проверяются. */
int chat_msg_parse_media_body(const char* body, struct media_index_result* result);
/* Учесть также расшифрованные производные файлы и применить общую квоту кеша. */
void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
+17 -5
View File
@@ -17,7 +17,7 @@
#endif
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler) {
if (!inst) return;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: handler requires instance"); return; }
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-evt", "chat_event_set_handler: handler=%p old=%p",
(void*)handler, (void*)inst->chat_event_handler);
@@ -26,7 +26,11 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha
}
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (inst && inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
if (!inst || type <= 0 || len < 0 || (len && !data)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: invalid event instance=%p type=%d bytes=%d data=%p",
(void*)inst, type, len, (const void*)data); return;
}
if (inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
static const char* names[] = {
[1]="MSG_RECEIVED", [2]="CONNECT_RESULT", [3]="NEW_PEER",
@@ -40,7 +44,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
[20]="CONN_LIST", [21]="CONN_METRICS",
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
[26]="BGP_INFO", [27]="CONNECTING_NODES",
[27]="CONNECTING_NODES",
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
@@ -50,15 +54,22 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
[43]="RADIO_TALK",
[44]="CALL_CONNECTION",
[45]="DM_CONVERSATIONS", [46]="ATTACHMENT_PREPARATION", [47]="DM_CALL_ROUTE",
[48]="DM_INVITE_RESULT", [49]="MEMBER_FILES_RESPONSE", [50]="MEMBER_FILES_TRANSFER",
[51]="AVATAR_RESULT", [52]="GROUP_AVATAR_RESULT", [53]="FEEDBACK_CHANGED", [54]="FEEDBACK_RESULT",
[55]="RADIO_MUTE_RESULT",
[56]="MSG_SUBMITTED",
[57]="CPU_LOAD",
[58]="STREAM_SOURCES",
[59]="DM_DELETE_RESULT",
[60]="ATTACHMENT_KEYS_CHANGED",
[61]="LOCAL_DEVICES", [62]="LOCAL_INVITE", [63]="LOCAL_DELIVERY", [64]="LOCAL_DISCOVERY_ACTIVE",
[65]="RADIO_CALL", [66]="CALL_PHONE_BUSY", [67]="MSG_UPDATED",
};
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : "?";
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : NULL;
if (!n) n = "?";
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE && type != CHAT_EVT_INVITE_LINK_READY)
log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
}
/* Идентификатор связывает результат с конкретной отправкой, независимо от текущего канала GUI. */
@@ -73,6 +84,7 @@ void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int s
/* ID задачи не даёт позднему завершению изменить статус другой подготовки. */
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state) {
if (!target || !id) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: preparation event requires target and UUID"); return; }
char name[68];
int n = snprintf(name, sizeof(name), "%s%s", is_dm ? "dm:" : "", target);
if (n <= 0 || (size_t)n >= sizeof(name) || state < 0 || state > 2) {
+18 -14
View File
@@ -6,7 +6,8 @@
* post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет.
* data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия.
*
* Без обработчика — данные логируются через DEBUG_DEBUG + log_dump.
* Без обработчика — DEBUG_DEBUG + log_dump; LOCAL_INVITE/INVITE_LINK_READY с секретами не дампятся.
* Обработчик может удалить канал; stop/destroy сервиса выполняются вне callbacks.
* Все вызовы — из uasync-потока.
*/
@@ -24,14 +25,14 @@ extern "C" {
// Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости)
#define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8][request_id:8], native */
#define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */
#define CHAT_EVT_CHANNEL_UPDATED 4 /* [ch_id_len:1][ch_id:var] */
#define CHAT_EVT_MEMBERS_CHANGED 5 /* [ch_id_len:1][ch_id:var] */
#define CHAT_EVT_MY_NODE_ID 6 /* [node_id:8] */
#define CHAT_EVT_MY_NODE_ID 6 /* [node_id:8],native;после commit метаданных и активации каналов chat_core */
#define CHAT_EVT_AUTO_CONNECT_STATUS 7 /* [status:1][total_tried:2][node_count:2][success:2] */
#define CHAT_EVT_CHANNEL_PEERS_ONLINE 8 /* [ch_id_len:1][ch_id:var][online_count:2] */
#define CHAT_EVT_DB_READY 9 /* data: none */
#define CHAT_EVT_DB_READY 9 /* нет data;готов chat_core,последующие части chat_service_start ещё могут отказать */
#define CHAT_EVT_STATUS_REFRESH 10 /* data: status text */
#define CHAT_EVT_KEYS_GENERATED 11 /* [pub_hex:64] */
#define CHAT_EVT_SERVICE_STARTED 12 /* data: none */
@@ -44,31 +45,30 @@ extern "C" {
#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */
#define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */
#define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */
#define CHAT_EVT_MEMBER_DETAIL 22 /* [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live-снапшот для панели деталей участника */
#define CHAT_EVT_MEMBER_DETAIL 22 /* Адресованный ответ диагностики; wire описан в chat_member.h. */
#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */
#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */
#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */
#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */
#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var][generation:8LE] — беседа создана/изменена */
#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var][generation:8 native] — беседа создана/изменена */
#define CHAT_EVT_CALL_INCOMING 34 /* [ch_id_len:1][ch_id:var][caller_node_id:8][call_id:8] */
#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8] */
#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8] */
#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8][reason:1=0], native */
#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8][reason:1=0], native */
#define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */
#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1][text:var] */
#define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][JSON:utf8] — сообщения беседы и состояния доставки */
#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1] */
#define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][generation:8][JSON:utf8] — история указанного поколения */
#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */
#define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */
#define CHAT_EVT_CALL_CONNECTION 44 /* [call_id:8][CONN_TYPE:1][next_hop_node_id:8] — текущий путь отправки звонка */
#define CHAT_EVT_DM_CONVERSATIONS 45 /* [JSON:utf8] — последние 200 личных бесед */
#define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], 0=start 1=ready 2=failed */
#define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], target=channel либо "dm:"+conv;0/1/2=start/ready/failed */
#define CHAT_EVT_DM_CALL_ROUTE 47 /* [conv_id_len:1][conv_id:var][peer_node_id:8][group_id:8], 0 = нет BGP-пути */
#define CHAT_EVT_DM_INVITE_RESULT 48 /* [request_id:8][result:4][peer_node_id:8][conv_id:8][generation:8], dm_invite_result */
@@ -91,6 +91,8 @@ extern "C" {
#define CHAT_EVT_LOCAL_DISCOVERY_ACTIVE 64 /* [active:1], Android MulticastLock */
#define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */
#define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */
#define CHAT_EVT_MSG_UPDATED 67 /* [channel_len:1][channel][message_id:32] — правка после commit, без unread/звука */
#define CHAT_EVT_GROUP_AVATAR_SNAPSHOT 68 /* Только Android JNI: id/group/version:8LE, hash:32, can_edit/failed:1, size:2LE, image:size. */
// ── Подключение получателя событий ──
@@ -101,10 +103,12 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha
// ── Отправка событий из ядра ──
// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*.
// Синхронно передать type>0 и len>=0 байт data; inst обязателен,data может быть NULL только при len=0.
// Неверные аргументы логируются без доставки;формат CHAT_EVT_* проверяет получатель,память остаётся отправителю.
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
// Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM.
// Сообщить этап подготовки: state=0 начало,1 готово,2 ошибка; target — ID без префикса, is_dm добавляет "dm:".
// target/id обязательны, копируются в синхронный payload; неверный state/переполнение строки только логируется.
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
// request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки.
+276 -93
View File
@@ -41,16 +41,17 @@ static int fb_db_error(struct UTUN_INSTANCE* inst, const char* channel, const ch
}
/* Канонический ID сообщения не зависит от локального порядкового номера. */
int chat_message_id(uint64_t channel_id, uint64_t timestamp, const uint8_t signature[64], uint8_t id[32]) {
static const char domain[] = "utun-chat-message-v1";
uint8_t body[sizeof(domain) - 1 + 16 + 64];
if (!channel_id || !signature || !id) {
int chat_message_id(uint64_t channel_id, uint64_t timestamp, uint64_t author, const uint8_t nonce[16], uint8_t id[32]) {
static const char domain[] = "utun-chat-message-v2";
uint8_t body[sizeof(domain) - 1 + 24 + 16];
if (!channel_id || !author || !nonce || !id) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid message identity arguments"); return -1;
}
memcpy(body, domain, sizeof(domain) - 1);
fb_write64(body + sizeof(domain) - 1, channel_id);
fb_write64(body + sizeof(domain) - 1 + 8, timestamp);
memcpy(body + sizeof(domain) - 1 + 16, signature, 64);
fb_write64(body + sizeof(domain) - 1 + 16, author);
memcpy(body + sizeof(domain) - 1 + 24, nonce, 16);
if (EVP_Digest(body, sizeof(body), id, NULL, EVP_sha256(), NULL) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: message identity digest failed"); return -1;
}
@@ -114,7 +115,8 @@ int chat_feedback_ensure_channel(struct UTUN_INSTANCE* inst, const char* channel
"node_id INTEGER NOT NULL,kind INTEGER NOT NULL,ver INTEGER NOT NULL CHECK(ver>0),"
"data BLOB NOT NULL,signature BLOB NOT NULL CHECK(length(signature)=64),"
"UNIQUE(message_id,node_id,kind));"
"CREATE INDEX IF NOT EXISTS idx_%s_bucket ON %s(bucket,record_key)", table, table, table);
"CREATE INDEX IF NOT EXISTS idx_%s_bucket ON %s(bucket,record_key);"
"CREATE INDEX IF NOT EXISTS idx_%s_author ON %s(node_id,bucket)", table, table, table, table, table);
if (sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) != SQLITE_OK) return fb_db_error(inst, channel, "ensure table");
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "feedback: table ready ch=%s table=%s", channel, table); return 0;
}
@@ -133,6 +135,33 @@ static int fb_read_row(sqlite3_stmt* st, struct chat_feedback_record* r) {
memcpy(r->signature, sqlite3_column_blob(st, 5), 64); return 0;
}
/* Единственный отбор активного набора для хеша, страницы и восстановления индекса.
* Неактивная история не интерпретируется; при возвращении автора проверяется заново. */
static int fb_read_active(struct UTUN_INSTANCE* inst, const char* channel, uint64_t id,
sqlite3_stmt* st, struct chat_feedback_record* r) {
uint8_t key[32], body[FB_WIRE_MAX + 32];
uint64_t author = (uint64_t)sqlite3_column_int64(st, 1);
enum ms_author_status status = member_sync_author_key(inst, channel, author, key);
if (status == MS_AUTHOR_ERROR) return -1;
if (status != MS_AUTHOR_ACTIVE) return 0;
if (fb_read_row(st, r) < 0) return -1;
int bytes = fb_signed_bytes(id, r, body);
if (bytes < 0) return -1;
int signature = sc_ed25519_verify(key, body, bytes, r->signature);
if (signature != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: stored signature failed ch=%s node=%016llx rc=%d",
channel, (unsigned long long)author, signature); return -1;
}
merkle_key_t record_key;
if (fb_key(id, r, &record_key) < 0) return -1;
if (sqlite3_column_bytes(st, 6) != 32 || memcmp(record_key.bytes, sqlite3_column_blob(st, 6), 32)
|| (uint64_t)sqlite3_column_int64(st, 7) != (fb_read64(record_key.bytes) >> 39)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: corrupt local index ch=%s node=%016llx",
channel, (unsigned long long)author); return -1;
}
return 1;
}
int chat_feedback_get(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
uint64_t node_id, uint8_t kind, struct chat_feedback_record* out) {
char table[40], sql[256];
@@ -142,12 +171,19 @@ int chat_feedback_get(struct UTUN_INSTANCE* inst, const char* channel, const uin
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature FROM %s"
" WHERE message_id=? AND node_id=? AND kind=?", table);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) return fb_db_error(inst, channel, "get");
sqlite3_bind_blob(st, 1, message_id, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id);
sqlite3_bind_int(st, 3, kind);
int rc = sqlite3_step(st), result = rc == SQLITE_ROW ? (fb_read_row(st, out) < 0 ? -1 : 1) : 0;
sqlite3_finalize(st);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) return fb_db_error(inst, channel, "get step");
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 1, message_id, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 3, kind);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
struct chat_feedback_record record;
int result = rc == SQLITE_ROW ? (fb_read_row(st, &record) < 0 ? -1 : 1) : 0;
int final = sqlite3_finalize(st);
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: lookup failed ch=%s node=%016llx kind=%u step=%d finalize=%d db=%s",
channel, (unsigned long long)node_id, kind, rc, final, sqlite3_errmsg(inst->topo_sqlite_db)); return -1;
}
if (result > 0) *out = record;
return result;
}
@@ -155,8 +191,14 @@ int chat_feedback_get(struct UTUN_INSTANCE* inst, const char* channel, const uin
static int fb_apply_row(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_feedback_record* r) {
char table[40], sql[512]; uint64_t id; uint8_t body[FB_WIRE_MAX + 32], key[32];
if (fb_table(inst, channel, table, &id) < 0) return -1;
enum ms_author_status status = member_sync_author_key(inst, channel, r->node_id, key);
if (status == MS_AUTHOR_INACTIVE || status == MS_AUTHOR_PENDING) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "feedback: deferred/ignored ch=%s node=%016llx status=%d ver=%llu",
channel, (unsigned long long)r->node_id, status, (unsigned long long)r->ver); return 0;
}
if (status != MS_AUTHOR_ACTIVE) return -1;
int bytes = fb_signed_bytes(id, r, body);
if (bytes < 0 || member_sync_verified_ed25519(inst, channel, r->node_id, key) < 0) return -1;
if (bytes < 0) return -1;
if (sc_ed25519_verify(key, body, bytes, r->signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid signature ch=%s node=%016llx kind=%u ver=%llu",
channel, (unsigned long long)r->node_id, r->kind, (unsigned long long)r->ver); return -1;
@@ -182,14 +224,22 @@ static int fb_apply_row(struct UTUN_INSTANCE* inst, const char* channel, const s
snprintf(sql, sizeof(sql), "INSERT INTO %s(record_key,bucket,message_id,node_id,kind,ver,data,signature)"
" VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(record_key) DO UPDATE SET ver=excluded.ver,data=excluded.data,signature=excluded.signature", table);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) return fb_db_error(inst, channel, "put");
sqlite3_bind_blob(st, 1, record_key.bytes, 32, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)(fb_read64(record_key.bytes) >> 39));
sqlite3_bind_blob(st, 3, r->message_id, 32, SQLITE_STATIC); sqlite3_bind_int64(st, 4, (sqlite3_int64)r->node_id);
sqlite3_bind_int(st, 5, r->kind); sqlite3_bind_int64(st, 6, (sqlite3_int64)r->ver);
sqlite3_bind_blob(st, 7, r->data, r->data_len, SQLITE_STATIC); sqlite3_bind_blob(st, 8, r->signature, 64, SQLITE_STATIC);
int rc = sqlite3_step(st); sqlite3_finalize(st);
if (rc != SQLITE_DONE) return fb_db_error(inst, channel, "put step");
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 1, record_key.bytes, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)(fb_read64(record_key.bytes) >> 39));
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, r->message_id, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 4, (sqlite3_int64)r->node_id);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 5, r->kind);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 6, (sqlite3_int64)r->ver);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 7, r->data, r->data_len, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 8, r->signature, 64, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
int changed = rc == SQLITE_DONE ? sqlite3_changes(inst->topo_sqlite_db) : 0;
int final = sqlite3_finalize(st);
if (rc != SQLITE_DONE || changed != 1 || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: write failed ch=%s node=%016llx kind=%u step=%d changes=%d finalize=%d db=%s",
channel, (unsigned long long)r->node_id, r->kind, rc, changed, final, sqlite3_errmsg(inst->topo_sqlite_db)); return -1;
}
if (merkle_sync_recompute_path(inst, MT_DATA_FEEDBACK, channel, fb_read64(record_key.bytes)) < 0) return -1;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "feedback: applied ch=%s message=%s node=%016llx kind=%u ver=%llu previous=%llu bytes=%u",
channel, message_hex, (unsigned long long)r->node_id, r->kind, (unsigned long long)r->ver,
@@ -204,15 +254,23 @@ static void fb_committed(struct UTUN_INSTANCE* inst, const char* channel, const
chat_event_post(inst, CHAT_EVT_FEEDBACK_CHANGED, event, (int)len + 34);
}
/* Завершает savepoint, при ошибке не публикует частичные изменения. */
/* Завершает только свою транзакцию; неудачный rollback никогда не переходит в commit. */
static int fb_finish_transaction(struct UTUN_INSTANCE* inst, const char* channel, int result) {
sqlite3* db = inst->topo_sqlite_db;
if (result < 0 && sqlite3_exec(db, "ROLLBACK TO feedback_update", NULL, NULL, NULL) != SQLITE_OK)
fb_db_error(inst, channel, "rollback");
if (sqlite3_exec(db, "RELEASE feedback_update", NULL, NULL, NULL) == SQLITE_OK) return result;
fb_db_error(inst, channel, "commit");
if (sqlite3_exec(db, "ROLLBACK TO feedback_update; RELEASE feedback_update", NULL, NULL, NULL) != SQLITE_OK)
fb_db_error(inst, channel, "commit cleanup");
if (result >= 0) {
int rc = sqlite3_exec(db, "COMMIT /* feedback update */", NULL, NULL, NULL);
if (rc == SQLITE_OK) return result;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: commit failed ch=%s rc=%d (%s) db=%s", channel, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
}
if (!sqlite3_get_autocommit(db)) {
int rc = sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: rollback failed ch=%s rc=%d (%s) db=%s",
channel, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
}
if (!sqlite3_get_autocommit(db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: transaction remains open ch=%s; requesting core stop and owner DB close after callback", channel);
utun_instance_stop(inst);
}
return -1;
}
@@ -224,7 +282,7 @@ int chat_feedback_apply(struct UTUN_INSTANCE* inst, const char* channel, const s
if (!sqlite3_get_autocommit(inst->topo_sqlite_db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: public apply inside external transaction ch=%s", channel); return -1;
}
if (sqlite3_exec(inst->topo_sqlite_db, "SAVEPOINT feedback_update", NULL, NULL, NULL) != SQLITE_OK)
if (sqlite3_exec(inst->topo_sqlite_db, "BEGIN IMMEDIATE /* feedback update */", NULL, NULL, NULL) != SQLITE_OK)
return fb_db_error(inst, channel, "begin");
int result = fb_finish_transaction(inst, channel, fb_apply_row(inst, channel, r));
if (result > 0) { merkle_sync_changed(inst, MT_DATA_FEEDBACK, channel); fb_committed(inst, channel, r); }
@@ -237,44 +295,69 @@ int chat_feedback_set(struct UTUN_INSTANCE* inst, const char* channel, const uin
if (fb_table(inst, channel, table, &id) < 0 || !message_id || len > CHAT_FEEDBACK_DATA_MAX || (len && !data)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid local state"); return -1;
}
if (!sqlite3_get_autocommit(inst->topo_sqlite_db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local set inside external transaction ch=%s", channel); return -1;
}
if (sqlite3_exec(inst->topo_sqlite_db, "BEGIN IMMEDIATE /* feedback local set */", NULL, NULL, NULL) != SQLITE_OK)
return fb_db_error(inst, channel, "local begin");
int result = -1;
struct chat_feedback_record r = { .node_id = inst->node_id, .ver = 1, .kind = kind, .data_len = len }, old;
memcpy(r.message_id, message_id, 32); if (len) memcpy(r.data, data, len);
uint8_t author_key[32];
enum ms_author_status status = member_sync_author_key(inst, channel, inst->node_id, author_key);
if (status != MS_AUTHOR_ACTIVE) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local action denied ch=%s node=%016llx status=%d",
channel, (unsigned long long)inst->node_id, status); goto finish;
}
int found = chat_feedback_get(inst, channel, message_id, inst->node_id, kind, &old);
if (found < 0) return -1;
if (found && old.data_len == len && !memcmp(old.data, r.data, len)) { if (out_ver) *out_ver = old.ver; return 0; }
if (found < 0) goto finish;
if (found && old.data_len == len && !memcmp(old.data, r.data, len)) { r.ver = old.ver; result = 0; goto finish; }
if (found && old.ver == INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: version exhausted ch=%s", channel); return -1;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: version exhausted ch=%s", channel); goto finish;
}
if (found) r.ver = old.ver + 1;
uint8_t body[FB_WIRE_MAX + 32]; int bytes = fb_signed_bytes(id, &r, body);
if (bytes < 0 || sc_ed25519_sign(inst->my_ed25519_privkey, body, bytes, r.signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local signing failed ch=%s", channel); return -1;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local signing failed ch=%s", channel); goto finish;
}
int result = chat_feedback_apply(inst, channel, &r);
result = fb_apply_row(inst, channel, &r);
finish:
result = fb_finish_transaction(inst, channel, result);
if (result >= 0 && out_ver) *out_ver = r.ver;
if (result > 0) DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local state saved ch=%s kind=%u ver=%llu bytes=%zu",
channel, kind, (unsigned long long)r.ver, len);
if (result > 0) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "feedback: local state saved ch=%s kind=%u ver=%llu bytes=%zu",
channel, kind, (unsigned long long)r.ver, len);
merkle_sync_changed(inst, MT_DATA_FEEDBACK, channel); fb_committed(inst, channel, &r);
}
return result;
}
/* Читает лист в полном лексикографическом порядке record_key. */
static int fb_page(void* ctx, const char* channel, uint64_t prefix, int av, const merkle_key_t* after,
uint8_t* data, size_t* len, merkle_key_t* next, int* more) {
struct UTUN_INSTANCE* inst = ctx; char table[40], sql[384];
if (fb_table(inst, channel, table, NULL) < 0 || *len < 2) {
struct UTUN_INSTANCE* inst = ctx; char table[40], sql[384]; uint64_t id;
if (fb_table(inst, channel, table, &id) < 0 || !len || *len < 2 || !after || !data || !next || !more) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid page capacity"); return -1;
}
merkle_key_t cursor = *after; memset(next, 0, sizeof(*next)); *more = 0;
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature,record_key FROM %s"
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature,record_key,bucket FROM %s"
" WHERE bucket=? AND (?=0 OR record_key>?) ORDER BY record_key", table);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) return fb_db_error(inst, channel, "page");
sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39)); sqlite3_bind_int(st, 2, av);
sqlite3_bind_blob(st, 3, cursor.bytes, 32, SQLITE_STATIC);
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39));
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, av);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, cursor.bytes, 32, SQLITE_STATIC);
if (rc != SQLITE_OK) {
int final = sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: page query failed ch=%s rc=%d finalize=%d db=%s",
channel, rc, final, sqlite3_errmsg(inst->topo_sqlite_db)); return -1;
}
size_t off = 2; unsigned count = 0; int step, result = 0;
while ((step = sqlite3_step(st)) == SQLITE_ROW) {
struct chat_feedback_record r; uint8_t wire[FB_WIRE_MAX];
if (fb_read_row(st, &r) < 0 || sqlite3_column_bytes(st, 6) != 32) { result = -1; break; }
int active = fb_read_active(inst, channel, id, st, &r);
if (active < 0) { result = -1; break; }
if (!active) continue;
int bytes = fb_encode(&r, wire); if (bytes < 0) { result = -1; break; }
if (off + bytes > *len) {
if (!count) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: record exceeds page capacity ch=%s", channel); result = -1; }
@@ -285,30 +368,45 @@ static int fb_page(void* ctx, const char* channel, uint64_t prefix, int av, cons
memcpy(next->bytes, sqlite3_column_blob(st, 6), 32);
}
if (result == 0 && !*more && step != SQLITE_DONE) result = fb_db_error(inst, channel, "page step");
sqlite3_finalize(st); data[0] = count >> 8; data[1] = count & 255; *len = off; return result;
int final = sqlite3_finalize(st);
if (final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: page finalize failed ch=%s rc=%d", channel, final); result = -1;
}
data[0] = count >> 8; data[1] = count & 255; *len = off; return result;
}
/* Хеширует подписанные тела: разные валидные подписи одного тела не меняют корень. */
static int fb_hash(void* ctx, const char* channel, uint8_t level, uint64_t prefix, EVP_MD_CTX* hash) {
struct UTUN_INSTANCE* inst = ctx; char table[40], sql[256];
if (fb_table(inst, channel, table, NULL) < 0 || level != MT_MAX_LEVEL) {
struct UTUN_INSTANCE* inst = ctx; char table[40], sql[256]; uint64_t id;
if (fb_table(inst, channel, table, &id) < 0 || level != MT_MAX_LEVEL) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid hash level=%u", level); return -1;
}
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature FROM %s WHERE bucket=? ORDER BY record_key", table);
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature,record_key,bucket"
" FROM %s WHERE bucket=? ORDER BY record_key", table);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) return fb_db_error(inst, channel, "hash");
sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39));
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)(prefix >> 39));
if (rc != SQLITE_OK) {
int final = sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: hash query failed ch=%s rc=%d finalize=%d db=%s",
channel, rc, final, sqlite3_errmsg(inst->topo_sqlite_db)); return -1;
}
int step, count = 0;
while ((step = sqlite3_step(st)) == SQLITE_ROW) {
struct chat_feedback_record r; uint8_t wire[FB_WIRE_MAX];
if (fb_read_row(st, &r) < 0) { count = -1; break; }
int active = fb_read_active(inst, channel, id, st, &r);
if (active < 0) { count = -1; break; }
if (!active) continue;
int bytes = fb_encode(&r, wire);
if (bytes < 0 || EVP_DigestUpdate(hash, wire, bytes - 64) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: leaf digest failed ch=%s", channel); count = -1; break;
}
count++;
}
sqlite3_finalize(st);
int final = sqlite3_finalize(st);
if (final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: hash finalize failed ch=%s rc=%d", channel, final); return -1;
}
if (count >= 0 && step != SQLITE_DONE) return fb_db_error(inst, channel, "hash step");
return count;
}
@@ -316,7 +414,10 @@ static int fb_hash(void* ctx, const char* channel, uint8_t level, uint64_t prefi
/* Вся страница применяется атомарно; события отправляются после её commit. */
static int fb_apply_page(void* ctx, const char* channel, uint64_t peer, const uint8_t* data, size_t len) {
struct UTUN_INSTANCE* inst = ctx; (void)peer;
if (len < 2) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: short page ch=%s", channel); return -1; }
if (!inst || !inst->topo_sqlite_db || !sqlite3_get_autocommit(inst->topo_sqlite_db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: page requires own transaction ch=%s", channel); return -1;
}
if (!data || len < 2) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: short/missing page ch=%s", channel); return -1; }
unsigned count = ((unsigned)data[0] << 8) | data[1]; size_t off = 2;
if (count > (len - 2) / FB_FIXED_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid page count=%u bytes=%zu", count, len); return -1;
@@ -330,7 +431,7 @@ static int fb_apply_page(void* ctx, const char* channel, uint64_t peer, const ui
if (!count) return 0;
uint8_t* changed = u_calloc(count, 1);
if (!changed) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: page allocation failed"); return -1; }
if (sqlite3_exec(inst->topo_sqlite_db, "SAVEPOINT feedback_update", NULL, NULL, NULL) != SQLITE_OK) {
if (sqlite3_exec(inst->topo_sqlite_db, "BEGIN IMMEDIATE /* feedback page */", NULL, NULL, NULL) != SQLITE_OK) {
u_free(changed); return fb_db_error(inst, channel, "page begin");
}
off = 2; int result = 0;
@@ -361,45 +462,127 @@ static const struct merkle_sync_data_ops fb_ops = {
.update_bucket_hash = fb_hash, .get_page = fb_page, .apply_items = fb_apply_page, .validate_peer = fb_peer
};
/* Перестраивает индекс после регистрации модели; подписи проверяются повторно. */
int chat_feedback_init(struct UTUN_INSTANCE* inst) {
if (merkle_sync_register(inst, MT_DATA_FEEDBACK, &fb_ops, inst) < 0) return -1;
sqlite3_stmt* channels = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT channel_id FROM channels", -1, &channels, NULL) != SQLITE_OK)
return fb_db_error(inst, "-", "init channels");
int step, result = 0, records = 0;
while ((step = sqlite3_step(channels)) == SQLITE_ROW) {
char channel[21]; snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(channels, 0));
char table[40], sql[256];
if (chat_feedback_ensure_channel(inst, channel) < 0 || fb_table(inst, channel, table, NULL) < 0) { result = -1; break; }
snprintf(sql, sizeof(sql), "SELECT message_id,node_id,kind,ver,data,signature,record_key,bucket FROM %s", table);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) {
result = fb_db_error(inst, channel, "init records"); break;
}
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
struct chat_feedback_record r; uint64_t id; uint8_t body[FB_WIRE_MAX + 32], key[32]; merkle_key_t record_key;
if (fb_read_row(st, &r) < 0 || fb_table(inst, channel, table, &id) < 0
|| member_sync_verified_ed25519(inst, channel, r.node_id, key) < 0) { result = -1; break; }
int bytes = fb_signed_bytes(id, &r, body);
if (bytes < 0 || sc_ed25519_verify(key, body, bytes, r.signature) != SC_OK || fb_key(id, &r, &record_key) < 0
|| merkle_sync_recompute_path(inst, MT_DATA_FEEDBACK, channel, fb_read64(record_key.bytes)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: rebuild failed ch=%s node=%016llx",
channel, (unsigned long long)r.node_id); result = -1; break;
}
if (sqlite3_column_bytes(st, 6) != 32 || memcmp(record_key.bytes, sqlite3_column_blob(st, 6), 32)
|| (uint64_t)sqlite3_column_int64(st, 7) != (fb_read64(record_key.bytes) >> 39)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: corrupt local index ch=%s node=%016llx",
channel, (unsigned long long)r.node_id); result = -1; break;
}
records++;
}
sqlite3_finalize(st);
if (result < 0) break;
if (rc != SQLITE_DONE) { result = fb_db_error(inst, channel, "init records step"); break; }
/* Один канал за раз: callbacks после commit вправе менять каналы и участников. */
static int fb_next_channel(struct UTUN_INSTANCE* inst, const char* after, char channel[21], int existing) {
sqlite3_stmt* st = NULL;
const char* sql = "SELECT channel_id FROM channels WHERE channel_id>? AND (?=0 OR EXISTS"
" (SELECT 1 FROM sqlite_master WHERE type='table' AND name='feedback_'||channel_id)) ORDER BY channel_id LIMIT 1";
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, after, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, existing);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
const char* text = (const char*)sqlite3_column_text(st, 0); int bytes = sqlite3_column_bytes(st, 0);
if (!text || bytes <= 0 || bytes >= 21) rc = SQLITE_CORRUPT;
else { memcpy(channel, text, bytes); channel[bytes] = 0; }
}
sqlite3_finalize(channels);
if (result == 0 && step != SQLITE_DONE) return fb_db_error(inst, "-", "init channels step");
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "feedback: initialization result=%d records=%d", result, records); return result;
int final = sqlite3_finalize(st);
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) return fb_db_error(inst, "-", "channel scan");
return rc == SQLITE_ROW;
}
/* Пересчитывает только листья, зависящие от изменённого автора/ключа пригласившего. */
int chat_feedback_member_update(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!merkle_sync_registered(inst, MT_DATA_FEEDBACK)) return 0;
if (sqlite3_get_autocommit(inst->topo_sqlite_db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: member update requires transaction node=%016llx",
(unsigned long long)node_id); return -1;
}
char channel[21] = "", next[21]; int scan;
while ((scan = fb_next_channel(inst, channel, next, 1)) > 0) {
strcpy(channel, next);
char table[40], sql[384]; if (fb_table(inst, channel, table, NULL) < 0) return -1;
snprintf(sql, sizeof(sql), "SELECT DISTINCT f.bucket FROM %s f LEFT JOIN peers_%s p ON p.node_id=f.node_id"
" WHERE f.node_id=?1 OR p.signed_by=?1 ORDER BY f.bucket", table, channel);
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
unsigned buckets = 0;
if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
sqlite3_int64 bucket = sqlite3_column_int64(st, 0);
if (bucket < 0 || bucket >= (INT64_C(1) << 25)) { rc = SQLITE_CORRUPT; break; }
if (merkle_sync_recompute_path(inst, MT_DATA_FEEDBACK, channel, (uint64_t)bucket << 39) < 0) {
rc = SQLITE_ERROR; break;
}
buckets++;
}
int final = sqlite3_finalize(st);
if (rc != SQLITE_DONE || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: member index failed ch=%s dependency=%016llx step=%d finalize=%d",
channel, (unsigned long long)node_id, rc, final); return -1;
}
if (buckets) DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "feedback: member index updated ch=%s dependency=%016llx buckets=%u",
channel, (unsigned long long)node_id, buckets);
}
return scan < 0 ? -1 : 0;
}
/* Адресные события читаются по одному без cursor через пользовательский callback. */
void chat_feedback_member_committed(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!merkle_sync_registered(inst, MT_DATA_FEEDBACK)) return;
if (!sqlite3_get_autocommit(inst->topo_sqlite_db)) { fb_db_error(inst, "-", "member notify before commit"); return; }
char channel[21] = "", next[21];
while (fb_next_channel(inst, channel, next, 1) > 0) {
strcpy(channel, next);
/* Даже без локального feedback ключ может разрешить приём ранее отложенной страницы. */
merkle_sync_changed(inst, MT_DATA_FEEDBACK, channel);
merkle_sync_revalidate_peers(inst, MT_DATA_FEEDBACK, channel);
if (!merkle_sync_registered(inst, MT_DATA_FEEDBACK)) return;
char table[40], sql[512]; if (fb_table(inst, channel, table, NULL) < 0) return;
snprintf(sql, sizeof(sql), "SELECT DISTINCT f.message_id,f.kind FROM %s f LEFT JOIN peers_%s p ON p.node_id=f.node_id"
" WHERE (f.node_id=?1 OR p.signed_by=?1) AND (?2=0 OR f.message_id>?3 OR (f.message_id=?3 AND f.kind>?4))"
" ORDER BY f.message_id,f.kind LIMIT 1", table, channel);
struct chat_feedback_record r = {0}; int after = 0;
for (;;) {
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 2, after);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, r.message_id, 32, SQLITE_TRANSIENT);
if (rc == SQLITE_OK) rc = sqlite3_bind_int(st, 4, r.kind);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
int kind = sqlite3_column_int(st, 1);
if (sqlite3_column_bytes(st, 0) != 32 || (kind != CHAT_FEEDBACK_REACTION && kind != CHAT_FEEDBACK_ANSWER))
rc = SQLITE_CORRUPT;
else { memcpy(r.message_id, sqlite3_column_blob(st, 0), 32); r.kind = kind; }
}
int final = sqlite3_finalize(st);
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { fb_db_error(inst, channel, "member notify"); return; }
if (rc == SQLITE_DONE) break;
after = 1; fb_committed(inst, channel, &r);
if (!merkle_sync_registered(inst, MT_DATA_FEEDBACK)) return;
}
}
}
/* Перестраивает активный индекс после purge MEMBERS, атомарно для всех каналов. */
int chat_feedback_init(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->topo_sqlite_db || !sqlite3_get_autocommit(inst->topo_sqlite_db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: init requires database outside transaction"); return -1;
}
if (merkle_sync_register(inst, MT_DATA_FEEDBACK, &fb_ops, inst) < 0) return -1;
if (sqlite3_exec(inst->topo_sqlite_db, "BEGIN IMMEDIATE /* feedback init */", NULL, NULL, NULL) != SQLITE_OK)
return fb_db_error(inst, "-", "init begin");
char channel[21] = "", next[21]; int scan, result = 0; unsigned buckets = 0;
while ((scan = fb_next_channel(inst, channel, next, 0)) > 0) {
strcpy(channel, next);
char table[40], sql[128];
if (chat_feedback_ensure_channel(inst, channel) < 0 || fb_table(inst, channel, table, NULL) < 0) { result = -1; break; }
snprintf(sql, sizeof(sql), "SELECT DISTINCT bucket FROM %s ORDER BY bucket", table);
sqlite3_stmt* st = NULL; int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
sqlite3_int64 bucket = sqlite3_column_int64(st, 0);
if (bucket < 0 || bucket >= (INT64_C(1) << 25)) { rc = SQLITE_CORRUPT; break; }
if (merkle_sync_recompute_path(inst, MT_DATA_FEEDBACK, channel, (uint64_t)bucket << 39) < 0) {
rc = SQLITE_ERROR; break;
}
buckets++;
}
int final = sqlite3_finalize(st);
if (rc != SQLITE_DONE || final != SQLITE_OK) { result = fb_db_error(inst, channel, "init buckets"); break; }
}
if (scan < 0) result = -1;
result = fb_finish_transaction(inst, "-", result);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "feedback: initialization result=%d buckets=%u", result, buckets); return result;
}
+37 -15
View File
@@ -1,9 +1,13 @@
/* Подписанные состояния реакций и ответов, отдельная таблица feedback_<channel>.
* Каждый (message_id, node_id, kind) хранит последнее полное состояние с ver.
* Пустое data — сохраняемая отмена. Слой хранит и реплицирует подписанные байты;
* Пустое data — сохраняемая отмена. Неактивные/ожидающие авторы сохраняются в БД,
* но не входят в Merkle, выдачу и результаты. Допуск проверяет member_sync;
* интерпретацию ответа относительно опроса выполняет вызывающий модуль.
* Все операции — в uasync-потоке. init вызывается после member_sync_init;
* общий Merkle-движок освобождается member_sync_destroy. */
* общий Merkle-движок освобождается member_sync_destroy.
* apply/set/приём страницы/init владеют своей BEGIN IMMEDIATE/COMMIT; внешняя транзакция запрещена.
* Запись и Merkle-индекс фиксируются вместе, уведомление — после commit. Неудачный rollback
* с открытой транзакцией запрашивает stop ядра; владелец закрывает БД вне callback. */
#ifndef CHAT_FEEDBACK_H
#define CHAT_FEEDBACK_H
@@ -12,36 +16,54 @@
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
#define CHAT_FEEDBACK_REACTION 1
#define CHAT_FEEDBACK_ANSWER 2
#define CHAT_FEEDBACK_DATA_MAX 2048
#define CHAT_MESSAGE_ID_SIZE 32
struct chat_feedback_record {
uint8_t message_id[CHAT_MESSAGE_ID_SIZE];
uint64_t node_id;
uint8_t message_id[CHAT_MESSAGE_ID_SIZE]; /* Неизменяемый ID исходного сообщения. */
uint64_t node_id; /* Автор этого состояния,1..INT64_MAX. */
uint64_t ver; /* 1..INT64_MAX; отдельная версия для каждого kind */
uint8_t kind;
uint16_t data_len;
uint8_t data[CHAT_FEEDBACK_DATA_MAX];
uint8_t signature[64];
uint8_t kind; /* REACTION либо ANSWER. */
uint16_t data_len; /* Длина полного состояния,0 отменяет. */
uint8_t data[CHAT_FEEDBACK_DATA_MAX]; /* Канонические байты codec из chat_poll.h. */
uint8_t signature[64]; /* Ed25519 автора над domain/каналом/полями/data. */
};
/* Производный ID неизменяемого сообщения; 0/-1. Числа хешируются как BE64. */
int chat_message_id(uint64_t channel_id, uint64_t timestamp, const uint8_t signature[64], uint8_t id[32]);
/* Регистрирует FEEDBACK и восстанавливает производный индекс из подписанных записей; 0/-1. */
/* Постоянный ID: канал, время создания, автор и случайный nonce[16]; 0/-1. Числа — BE64. */
int chat_message_id(uint64_t channel_id, uint64_t timestamp, uint64_t author, const uint8_t nonce[16], uint8_t id[32]);
/* Регистрирует FEEDBACK и восстанавливает индекс допущенных записей; 0/-1.
* Исключение или отсутствие зависимости автора не блокирует запуск и не удаляет историю. */
int chat_feedback_init(struct UTUN_INSTANCE* inst);
/* После изменения участника/его доверенного ключа пересчитать feedback автора и его
* зависимых приглашённых во всех каналах. Вызывается внутри транзакции MEMBERS;
* до регистрации FEEDBACK — no-op. 0/-1, ошибка требует rollback внешней транзакции. */
int chat_feedback_member_update(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* После внешнего commit разбудить сессии и обновить затронутые результаты.
* Обработчики событий вызываются без живого SQLite cursor; до регистрации — no-op. */
void chat_feedback_member_committed(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подготовить таблицу канала; 0/-1. Не создаёт членство или сетевую группу. */
int chat_feedback_ensure_channel(struct UTUN_INSTANCE* inst, const char* channel);
/* Прочитать состояние: 1 найдено, 0 отсутствует, -1 ошибка. */
/* Прочитать сохранённое состояние, включая неактивное:1 найдено,0 отсутствует,-1 ошибка.
* out меняется только при успехе1;ошибка bind/step/finalize не считается отсутствием. */
int chat_feedback_get(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
uint64_t node_id, uint8_t kind, struct chat_feedback_record* out);
/* Проверить и применить подписанную запись: 1 изменилось, 0 повтор/старая, -1 ошибка.
/* Проверить и применить подписанную запись: 1 изменилось, 0 повтор/старая/автор недоступен, -1 ошибка.
* Одинаковый ver разрешается сравнением data; подпись проверяется до сравнения. */
int chat_feedback_apply(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_feedback_record* record);
/* Подписать локальное полное состояние с ver+1; NULL/0 отменяет. 1/0/-1 как apply.
* out_ver необязателен. Семантику data проверяет вызывающий модуль до этого вызова. */
/* Подписать локальное полное состояние с ver+1; NULL/0 отменяет.1/0/-1 как apply.
* Допуск/прежняя версия/подпись/запись выполняются под собственной блокировкой,включая no-op.
* out_ver необязателен,меняется только после commit. Канонический формат проверяет codec,
* семантику ответа относительно опроса проверяет вызывающий до этого вызова. */
int chat_feedback_set(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
uint8_t kind, const uint8_t* data, size_t len, uint64_t* out_ver);
#ifdef __cplusplus
}
#endif
#endif
File diff suppressed because it is too large. Load diff
+21 -4
View File
@@ -3,13 +3,21 @@
*
* JSON line protocol:
* Request: {"id":N,"cmd":"<command>",...params}
* Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
* Response: {"id":N,"ok":true,"data":<JSON>} | {"id":N,"ok":false,"error":"..."}
* Event: {"event":"<type>",...}
*
* Команды каналов, DM, звонков/рации и настроек — в tools/chatcli_commands.txt;
* Команды каналов, DM и взаимодействий — в tools/chatcli_commands.txt;
* диспетчер реализации — chat_headless_control.c. Аудио PCM использует отдельные call/radio_headless сокеты.
* init регистрирует listener и события chat_event, destroy закрывает клиентов и снимает подписку.
* Все вызовы и команды исполняются в uasync-потоке экземпляра.
* init требует готовый chat_core и ua==inst->ua. Listener IPv4,до64 клиентов,строка до16383байт без LF.
* До команды проверяется весь строгий JSON-object:до32 уникальных ключей длиной<=31байта,
* id — int32,строковые параметры не обрезаются,Unicode escapes декодируются,скрытый NUL запрещён.
* Массивы опросов передаются общему feedback API;ошибка разбора/типов получает id=0,ошибка команды сохраняет id.
* Исходящая очередь до32768байт:короткие записи/EAGAIN сохраняют порядок,переполнение закрывает клиента с WARN.
* quit дописывает bye перед закрытием. attach отвечает после сохранения/отказа публикации,не после доставки пирам;
* его completion освобождается также после disconnect. connect/call подтверждают передачу запроса ядру;
* подписчикам приходят connect_result/call_error и состояния звонка,а не гарантия готовности связи из ответа команды.
* Прежний chat_event handler вызывается после headless;destroy восстанавливает его,если handler всё ещё принадлежит headless.
* Все вызовы/команды/completion — в uasync;stop/destroy вне callbacks этой службы. Диагностика — категория control.
*/
#ifndef CHAT_HEADLESS_CONTROL_H
#define CHAT_HEADLESS_CONTROL_H
@@ -20,10 +28,19 @@
struct UASYNC;
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* 0 — запущен/повтор с тем же bind,-1 — отказ с полной очисткой. port=0 выбирает ОС. */
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst);
// port=0 при init выбирает ОС; getter возвращает 0 для остановленной службы или при ошибке.
uint16_t chat_headless_control_get_port(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
}
#endif
#endif /* CHAT_HEADLESS_CONTROL_H */
+40 -25
View File
@@ -8,6 +8,7 @@
#include "chat_join.h"
#include "member_sync.h"
#include "chat_event.h"
#include "chat_sync.h"
#include "../utun_instance.h"
#include "../routing_layer/topo_node_sqlite.h"
@@ -59,11 +60,11 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
}
static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
if (!channel_id || !join_key || !inviter_id) {
if (!inst || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID);
return -1;
}
_keys_purge_expired(inst);
for (struct join_key_entry* e = inst->join_keys; e; e = e->next) {
if (e->channel_id != channel_id || e->join_key != join_key) continue;
if (e->inviter_id != inviter_id) {
@@ -103,13 +104,17 @@ static void _keys_free_all(struct UTUN_INSTANCE* inst) {
int chat_join_serialize_member(const struct join_member_data* m,
uint8_t* out, size_t out_sz, size_t* out_len) {
if (!m || !out || !out_len || !m->x25519 || !m->ed25519) return -1;
if (!m || !out || !out_len || !m->x25519 || !m->ed25519) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid member serialization arguments"); return -1;
}
size_t ul = strnlen(m->userinfo, sizeof(m->userinfo));
if (ul > MS_USERINFO_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "chat_join: profile exceeds limit bytes=%zu", ul); return -1;
}
size_t need = 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2 + ul;
if (need > out_sz) return -1;
if (need > out_sz) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: member buffer too small need=%zu capacity=%zu", need, out_sz); return -1;
}
size_t off = 0;
memcpy(out + off, &m->node_id, 8); off += 8;
memcpy(out + off, m->x25519, 32); off += 32;
@@ -125,8 +130,11 @@ int chat_join_serialize_member(const struct join_member_data* m,
return 0;
}
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m) {
if (!data || !m || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) return -1;
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* output) {
if (!data || !output || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid/short member block bytes=%zu", len); return -1;
}
struct join_member_data parsed, *m = &parsed;
memset(m, 0, sizeof(*m));
size_t off = 0;
memcpy(&m->node_id, data + off, 8); off += 8;
@@ -142,6 +150,7 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d
}
memcpy(m->userinfo, data + off, ul);
m->userinfo[ul] = '\0';
*output = parsed;
return 0;
}
@@ -150,6 +159,9 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d
void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t from_node, uint64_t join_key,
const uint8_t* member_data, size_t member_len) {
if (!inst || inst->join_stopping || !group_id || group_id > INT64_MAX || !from_node) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid/stopped process request", CJ_ID); return;
}
struct join_member_data m;
if (chat_join_parse_member(member_data, member_len, &m) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request parse failed ch=%llu from=%016llx", CJ_ID, (unsigned long long)group_id, (unsigned long long)from_node);
@@ -188,6 +200,18 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
/* Локальная запись доверенная: до неё проверяем также профиль и уже известный ключ идентичности. */
struct ms_member_rec record = {.node_id = m.node_id, .x25519 = m.x25519, .ed25519 = m.ed25519,
.join_sig = m.join_sig, .join_ts = m.join_ts, .update_sig = m.update_sig, .update_ts = m.update_ts,
.userinfo = m.userinfo};
uint8_t trusted_ed[32];
int lookup = topo_node_sqlite_get_ed25519_pubkey(db, m.node_id, trusted_ed);
if (lookup == TOPO_SQLITE_LOOKUP_ERROR || (lookup == 0 && memcmp(trusted_ed, m.ed25519, sizeof(trusted_ed))) ||
member_sync_verify_local_record(db, ch_id, &record) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member validation failed ch=%s node=%016llx lookup=%d", CJ_ID,
ch_id, (unsigned long long)m.node_id, lookup); return;
}
/* 3. добавить мембера + подписать его pubkey (дерево приглашений) */
uint8_t tree_sig[64];
if (member_sync_sign_pubkey(inst->my_ed25519_privkey, m.x25519, tree_sig) != 0) {
@@ -204,22 +228,8 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
if (rc & MS_APPLY_CHANGED) {
/* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* jc = instance_find_conn(inst, m.node_id);
if (jc) {
topo_group_new_conn(g, jc);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: joiner 0x%016llx added to group ch=%s for BGP",
CJ_ID, (unsigned long long)m.node_id, ch_id);
}
}
/* уведомить GUI инвайтера об успешном присоединении джойнера */
uint8_t cevt[20]; memcpy(cevt, &m.node_id, 8);
int r = 0; memcpy(cevt + 8, &r, 4);
memcpy(cevt + 12, &group_id, 8);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
/* member_sync уже запускает сессию после commit; GUI получает общий формат результата. */
chat_sync_join_result(inst, 0, group_id, m.node_id, CHAT_JOIN_OK);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me",
@@ -354,7 +364,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* ── Жизненный цикл ── */
int chat_join_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: init without instance", CJ_ID); return -1; }
inst->join_stopping = 0;
if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID);
@@ -382,7 +392,7 @@ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, u
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID);
return NULL;
}
@@ -415,6 +425,9 @@ struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
if (!inst || !channel_id || channel_id > INT64_MAX || !join_key) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key lookup", CJ_ID); return 0;
}
struct join_key_entry* e = _keys_find(inst, channel_id, join_key);
return e ? e->inviter_id : 0;
}
@@ -422,7 +435,9 @@ uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_i
int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t inviter_id, uint64_t join_key,
const struct join_member_data* m) {
if (!inst || !m || inviter_id == 0) return -1;
if (!inst || inst->join_stopping || !m || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid forward arguments", CJ_ID); return -1;
}
uint8_t mbuf[MS_USERINFO_MAX + 256]; size_t mlen = 0;
if (chat_join_serialize_member(m, mbuf, sizeof(mbuf), &mlen) != 0) {
+12 -5
View File
@@ -15,7 +15,7 @@
* │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J)
* │ │ валиден ключ → только pending, БЕЗ BGP new_conn
* │◀── REQUEST_FWD ───────────┤ │
* │ verif join_sig, node_id=derive(x25519) │
* │ verif JOIN,профиля,известного Ed25519,node_id=derive(x25519) │
* │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join"))
* │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │
* │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C
@@ -64,7 +64,9 @@
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* описан в chat_sync.h. Для INFO_REQ/RESP, REQUEST, READY и ERROR payload начинается
* с attempt_id:8BE; C сохраняет его в waiter и возвращает в ответах. Это ID попытки,
* не join_key и не request_id интерфейса. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
*
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
@@ -113,11 +115,14 @@ struct join_member_data {
char userinfo[MS_USERINFO_MAX + 1]; /* JSON, null-terminated */
};
/* Сериализовать member-блок (без join_key). Возвращает 0 и *out_len, -1 если не влез. */
/* Сериализовать member-блок (без join_key). 0 и *out_len,-1 при неверных аргументах/размере.
* На отказе out/out_len сохраняются; подписи NULL кодируются 64 нулями,ключи обязательны.
* Числа native,кроме userinfo_len:u16BE;userinfo<=1024 байт без NUL на проводе. */
int chat_join_serialize_member(const struct join_member_data* m,
uint8_t* out, size_t out_sz, size_t* out_len);
/* Разобрать member-блок. Указатели ссылаются на data (не копируют). 0=ок, -1=ошибка. */
/* Разобрать весь member-блок:проверить длину/отсутствие скрытого NUL. 0/-1,на отказе m сохраняется.
* Ключи/подписи заимствованы у data,userinfo копируется;криптография здесь не проверяется. */
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m);
/* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */
@@ -145,7 +150,9 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
const struct join_member_data* m);
/* Инвайтер (локально, когда connection==инвайтер, или из recv REQUEST_FWD):
* верификация join_sig + добавление мембера + подпись дерева. */
* Проверка ключа,идентичности,JOIN,подписи профиля при наличии и известного Ed25519;
* затем доверенная локальная запись с подписью приглашения. Join-only допускается.
* Подключение группы инициирует member_sync после commit;при изменении CONNECT_RESULT имеет request_id=0. */
void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t from_node, uint64_t join_key,
const uint8_t* member_data, size_t member_len);
+130
View File
@@ -0,0 +1,130 @@
#include "chat_join_port.h"
#include "chat_sync.h"
#include "invite_link.h"
#include "../utun_instance.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include <pthread.h>
#include <string.h>
struct join_command {
struct posted_task post; /* первый член: uasync освобождает всю команду */
struct join_command* next;
struct chat_join_port* port;
uint64_t request, channel, peer, offer;
size_t len;
int completed;
char link[];
};
struct chat_join_port {
pthread_mutex_t lock;
unsigned refs;
uint64_t active_request;
struct UTUN_INSTANCE* inst; /* только callback в uasync обращается к instance */
struct UASYNC* ua; /* жив до close; producer публикует под lock */
struct join_command* commands;
};
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst) {
struct chat_join_port* p = u_calloc(1, sizeof(*p));
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port allocation failed"); return NULL; }
if (pthread_mutex_init(&p->lock, NULL)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port mutex initialization failed"); u_free(p); return NULL;
}
p->refs = 1; p->inst = inst; p->ua = inst->ua; return p;
}
void chat_join_port_retain(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); p->refs++; pthread_mutex_unlock(&p->lock);
}
void chat_join_port_release(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); unsigned refs = --p->refs; pthread_mutex_unlock(&p->lock);
if (!refs) { pthread_mutex_destroy(&p->lock); u_free(p); }
}
static void execute(void* arg) {
struct join_command* command = arg;
struct chat_join_port* p = command->port;
pthread_mutex_lock(&p->lock);
struct join_command** link = &p->commands;
while (*link && *link != command) link = &(*link)->next;
if (*link) *link = command->next;
struct UTUN_INSTANCE* inst = p->inst;
if (!command->completed && inst && !p->active_request) p->active_request = command->request;
pthread_mutex_unlock(&p->lock);
if (!command->completed && inst) {
int result = command->offer ? chat_sync_offer_link(inst, command->request, command->offer, command->link, command->len)
: chat_sync_join_link(inst, command->request, command->link, command->len);
if (result) {
chat_join_port_complete(p, command->request);
chat_sync_join_result(inst, command->request, command->channel, command->peer, result);
}
}
chat_join_port_release(p); /* память command освободит uasync */
}
static int submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len, uint64_t offer) {
if (!p || !request || !link || !len || len >= INVITE_LINK_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command invalid or port unavailable"); return CHAT_CONNECT_INVALID;
}
struct InviteData invite;
if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID;
if (!offer && invite.kind != INVITE_KIND_CHANNEL) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server link requires selecting a local channel to add"); return CHAT_CONNECT_INVALID;
}
struct join_command* command = u_calloc(1, sizeof(*command) + len);
if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join command allocation failed"); return CHAT_CONNECT_ERROR; }
command->post.callback = execute; command->post.arg = command; command->port = p;
command->offer = offer; command->request = request; command->channel = offer ? offer : invite.channelId; command->peer = invite.nodeId;
command->len = len; memcpy(command->link, link, len);
pthread_mutex_lock(&p->lock);
if (!p->inst) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command rejected: port closed"); return CHAT_CONNECT_CANCELLED;
}
if (p->active_request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join request already active"); return CHAT_CONNECT_BUSY;
}
for (struct join_command* c = p->commands; c; c = c->next) if (c->request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "duplicate pending join request"); return CHAT_CONNECT_BUSY;
}
p->refs++; command->next = p->commands; p->commands = command;
uasync_post_reserved(p->ua, &command->post);
pthread_mutex_unlock(&p->lock);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join command queued request=%llu", (unsigned long long)request);
return 0;
}
void chat_join_port_close(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock);
struct UTUN_INSTANCE* inst = p->inst;
struct UASYNC* ua = p->ua;
struct join_command* commands = p->commands;
p->commands = NULL; p->inst = NULL; p->ua = NULL;
pthread_mutex_unlock(&p->lock);
while (commands) {
struct join_command* c = commands; commands = c->next;
c->completed = 1;
uint64_t request = c->request, channel = c->channel, peer = c->peer;
/* Успешная отмена снимает ссылку команды; уже начатый execute снимает её самостоятельно. */
if (uasync_cancel_post(ua, &c->post) == ERR_OK) chat_join_port_release(p);
chat_sync_join_result(inst, request, channel, peer, CHAT_CONNECT_CANCELLED);
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join port closed");
}
void chat_join_port_complete(struct chat_join_port* p, uint64_t request) {
if (!p) return;
pthread_mutex_lock(&p->lock);
if (p->active_request == request) p->active_request = 0;
pthread_mutex_unlock(&p->lock);
}
int chat_join_port_submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len) {
return submit(p, request, link, len, 0);
}
int chat_join_port_offer(struct chat_join_port* p, uint64_t request, uint64_t channel, const char* link, size_t len) {
if (!channel) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "offer missing channel"); return CHAT_CONNECT_INVALID; }
return submit(p, request, link, len, channel);
}
+32
View File
@@ -0,0 +1,32 @@
/* Потокобезопасный вход команд JOIN для GUI/JNI. Контекст удерживается фронтендом
* от bind до unbind; close в uasync отменяет команды и запрещает дальнейшие submit.
* После close порт живёт до release последних ссылок, не содержит живого instance. */
#ifndef CHAT_JOIN_PORT_H
#define CHAT_JOIN_PORT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct UTUN_INSTANCE;
struct chat_join_port;
/* Создать порт в uasync; одна ссылка принадлежит chat_sync, NULL при ошибке. */
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst);
/* Получить отдельную ссылку на порт текущего чата; только uasync, NULL до init. */
struct chat_join_port* chat_sync_join_port(struct UTUN_INSTANCE* inst);
/* Управление отдельной ссылкой. retain требует уже удерживаемой ссылки. */
void chat_join_port_retain(struct chat_join_port* port);
void chat_join_port_release(struct chat_join_port* port);
/* uasync: снять корреляцию завершённой активной команды до уведомления. */
void chat_join_port_complete(struct chat_join_port* port, uint64_t request);
/* uasync: закрыть приём и завершить ожидающие команды CANCELLED; повтор допустим. */
void chat_join_port_close(struct chat_join_port* port);
/* Любой поток: скопировать ссылку и принять команду. 0 — будет один результат с request;
* <0 — команда не принята, результата не будет. request != 0; link заимствован до возврата. */
int chat_join_port_submit(struct chat_join_port* port, uint64_t request, const char* link, size_t len);
/* Те же гарантии для приглашения адресата ссылки в собственный channel. */
int chat_join_port_offer(struct chat_join_port* port, uint64_t request, uint64_t channel, const char* link, size_t len);
#ifdef __cplusplus
}
#endif
#endif
+120 -9
View File
@@ -4,6 +4,13 @@
#include "chat_setting.h"
#include "../utun_instance.h"
#include "../local_transport/protocol.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_group_connect.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/sock_match.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../transport_layer/node_conn_direct.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include <string.h>
@@ -13,6 +20,16 @@ void chat_local_peers_trampoline(void* arg) {
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "device list request without instance"); return; }
struct local_peer peers[LOCAL_TRANSPORT_PEERS_MAX];
size_t count = local_transport_peers(inst->local_transport, peers, LOCAL_TRANSPORT_PEERS_MAX), offset = 2;
if (inst->bluetooth_discovery) {
const struct chat_local_provider* bt = inst->bluetooth_discovery;
struct local_peer bluetooth[LOCAL_TRANSPORT_PEERS_MAX];
size_t n = bt->peers(bt->ctx, bluetooth, LOCAL_TRANSPORT_PEERS_MAX);
for (size_t i = 0; i < n && count < LOCAL_TRANSPORT_PEERS_MAX; i++) {
size_t j;
for (j = 0; j < count && peers[j].node_id != bluetooth[i].node_id; j++);
if (j == count) peers[count++] = bluetooth[i];
}
}
uint8_t data[2 + LOCAL_TRANSPORT_PEERS_MAX * (8 + 1 + LOCAL_TRANSPORT_NAME_MAX - 1)];
data[0] = (uint8_t)(count >> 8); data[1] = (uint8_t)count;
for (size_t i = 0; i < count; i++) {
@@ -25,7 +42,7 @@ void chat_local_peers_trampoline(void* arg) {
static void peers_changed(void* arg) { chat_local_peers_trampoline(arg); }
static int valid_link(const char* link, size_t len) {
int chat_local_valid_link(const char* link, size_t len) {
struct InviteData invite = {0};
char error[128];
if (!len || len > LOCAL_TRANSPORT_DATA_MAX || memchr(link, 0, len) || len < 7 || memcmp(link, "utun://", 7)) {
@@ -41,13 +58,15 @@ static int valid_link(const char* link, size_t len) {
return 1;
}
static int received(void* arg, const struct local_peer* peer, const uint8_t* link, size_t len) {
struct UTUN_INSTANCE* inst = arg;
if (!chat_setting_get_int(inst, "local_invites_enabled", 1)) {
int chat_local_receive(struct UTUN_INSTANCE* inst, const struct local_peer* peer, const uint8_t* link, size_t len, int bluetooth) {
if (!inst || !peer || !link || strnlen(peer->name, sizeof(peer->name)) == sizeof(peer->name)) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "invalid invitation receive arguments"); return -1;
}
if (!chat_setting_get_int(inst, bluetooth ? "bluetooth_invites_enabled" : "local_invites_enabled", bluetooth ? 0 : 1)) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "local invitation rejected peer=%016llx reason=receiving disabled",
(unsigned long long)peer->node_id); return LOCAL_DELIVERY_FORBIDDEN;
}
if (!inst->chat_event_handler || !valid_link((const char*)link, len)) {
if (!inst->chat_event_handler || !chat_local_valid_link((const char*)link, len)) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "local invitation rejected peer=%016llx handler=%d",
(unsigned long long)peer->node_id, inst->chat_event_handler != NULL); return -1;
}
@@ -59,12 +78,23 @@ static int received(void* arg, const struct local_peer* peer, const uint8_t* lin
return 0;
}
static void delivered(void* arg, uint64_t request, uint64_t peer, int status) {
void chat_local_delivered(void* arg, uint64_t request, uint64_t peer, int status) {
uint8_t data[17];
local_put64(data, request); local_put64(data + 8, peer); data[16] = (uint8_t)status;
chat_event_post(arg, CHAT_EVT_LOCAL_DELIVERY, data, sizeof(data));
}
static int received(void* arg, const struct local_peer* peer, const uint8_t* link, size_t len) {
return chat_local_receive(arg, peer, link, len, 0);
}
void chat_local_set_bluetooth(struct UTUN_INSTANCE* inst, const struct chat_local_provider* provider) {
if (!inst || (provider && (!provider->ctx || !provider->peers || !provider->send))) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "invalid Bluetooth discovery provider"); return;
}
inst->bluetooth_discovery = provider;
chat_local_peers_trampoline(inst);
}
static void activity_changed(void* arg, int active) {
uint8_t value = active != 0;
chat_event_post(arg, CHAT_EVT_LOCAL_DISCOVERY_ACTIVE, &value, 1);
@@ -76,9 +106,81 @@ static void activity(struct UTUN_INSTANCE* inst, int active, void* arg) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "failed to apply discovery activity node=%016llx", (unsigned long long)inst->node_id);
}
/* Только принимающие рабочие endpoints этого IP; скрытые и SOCKS/probe-сокеты не публикуются. */
static int route_sockets(void* arg, const struct sockaddr_storage* address, uint32_t index,
struct local_route_socket* out, size_t capacity) {
struct UTUN_INSTANCE* inst = arg;
size_t count = 0;
for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) {
if (sock->type == CFG_SERVER_TYPE_PRIVATE || !sock_matches_lan(sock, address, index)) continue;
uint16_t port = ss_get_port(&sock->local_addr);
if (!port || !sock->generation) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN socket omitted name=%s id=%u port=%u generation=%016llx",
sock->name, sock->sock_id, port, (unsigned long long)sock->generation); continue;
}
if (count == capacity) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "too many LAN sockets interface=%u capacity=%zu", index, capacity); return -1;
}
out[count++] = (struct local_route_socket){ .socket_id = sock->sock_id, .generation = sock->generation,
.port = port, .protocol = sock->is_tcp ? LOCAL_ROUTE_TCP | (sock->reality_enabled ? LOCAL_ROUTE_REALITY : 0) : LOCAL_ROUTE_UDP };
}
return (int)count;
}
/* Ключи не покидают ядро; wire magic/version/type отделяют эту подпись от других протоколов. */
static int route_sign(void* arg, const uint8_t* data, size_t len, uint8_t signature[64]) {
struct UTUN_INSTANCE* inst = arg;
if (inst->node_id != sc_derive_node_id_from_pubkey(inst->my_keys.public_key) ||
sc_ed25519_sign(inst->my_ed25519_privkey, data, len, signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN endpoint signing failed node=%016llx", (unsigned long long)inst->node_id); return -1;
}
return 0;
}
/* Отсутствие ключа не мешает открытому discovery, но не даёт адрес для NCD. */
static int route_verify(void* arg, uint64_t peer, const uint8_t* data, size_t len, const uint8_t signature[64]) {
struct UTUN_INSTANCE* inst = arg;
uint8_t key[32];
struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, peer);
if (node && node->timestamp) memcpy(key, node->ed25519_public_key, sizeof(key));
else if (!inst->topo_sqlite_db || topo_node_sqlite_get_ed25519_pubkey(inst->topo_sqlite_db, peer, key)) {
DEBUG_DEBUG(DEBUG_CATEGORY_DISCOVERY, "LAN route ignored peer=%016llx reason=unknown signing key", (unsigned long long)peer); return 1;
}
if (sc_ed25519_verify(key, data, len, signature) != SC_OK) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN route signature rejected peer=%016llx", (unsigned long long)peer); return -1;
}
return 0;
}
/* Общие адреса узла нельзя заменять, пока доступен прямой транспорт или любой BGP-путь. */
static int route_offline(void* arg, uint64_t peer) {
struct UTUN_INSTANCE* inst = arg;
struct ETCP_CONN* conn = inst->connections ? instance_find_conn(inst, peer) : NULL;
if (conn && conn->links_up && !conn->close_requested && conn->state != 2) return 0;
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
e; e = e->next) {
if (topo_node_best_path(topo_node_find_by_id((struct TOPO_GROUP*)e, peer))) return 0;
}
return 1;
}
/* Обновление помогает уже удерживаемому NCD; право создавать соединение остаётся у сервисов. */
static void routes_changed(void* arg, uint64_t peer) {
struct UTUN_INSTANCE* inst = arg;
if (inst->connections && node_conn_direct_update_local(inst, peer) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN address application failed peer=%016llx", (unsigned long long)peer);
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
e; e = e->next) {
struct TOPO_GROUP* group = (struct TOPO_GROUP*)e;
if (group->group_type == TOPO_GROUP_TYPE_CHAT && !group->stopping) topo_group_connect_changed(group);
}
}
int chat_local_start(struct UTUN_INSTANCE* inst) {
if (inst->local_transport || inst->client_type == CLIENT_TYPE_SERVER) return 0;
struct local_transport_callbacks callbacks = { peers_changed, received, delivered, activity_changed };
struct local_transport_callbacks callbacks = { .peers_changed = peers_changed, .received = received, .delivery = chat_local_delivered,
.activity_changed = activity_changed, .route_sockets = route_sockets, .route_sign = route_sign,
.route_verify = route_verify, .route_offline = route_offline, .routes_changed = routes_changed };
inst->local_transport = local_transport_create(inst->ua, inst->node_id, inst->name, &callbacks, inst);
if (!inst->local_transport) return -1;
if (chat_local_apply_settings(inst) < 0) { chat_local_stop(inst); return -1; }
@@ -107,7 +209,16 @@ void chat_local_send_trampoline(void* arg) {
if (!req) return;
size_t len = strnlen(req->link, sizeof(req->link));
if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "invitation send without instance"); u_free(req); return; }
if (!valid_link(req->link, len) || local_transport_send(req->inst->local_transport, req->peer_id,
req->request_id, req->link, len) < 0) delivered(req->inst, req->request_id, req->peer_id, 0);
int result = -1;
if (chat_local_valid_link(req->link, len)) {
struct local_peer peers[LOCAL_TRANSPORT_PEERS_MAX];
size_t count = local_transport_peers(req->inst->local_transport, peers, LOCAL_TRANSPORT_PEERS_MAX);
for (size_t i = 0; i < count; i++) if (peers[i].node_id == req->peer_id) {
result = local_transport_send(req->inst->local_transport, req->peer_id, req->request_id, req->link, len); break;
}
const struct chat_local_provider* bt = req->inst->bluetooth_discovery;
if (result < 0 && bt) result = bt->send(bt->ctx, req->peer_id, req->request_id, req->link, len);
}
if (result < 0) chat_local_delivered(req->inst, req->request_id, req->peer_id, LOCAL_DELIVERY_FAILED);
u_free(req);
}
Loaded 100 of 1041 files, more files were not shown because too many files have changed in this diff. Show more