LCOV - code coverage report
Current view: top level - src/jamidht - sync_module.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 83.7 % 203 170
Test Date: 2026-08-23 08:52:56 Functions: 87.0 % 23 20

            Line data    Source code
       1              : /*
       2              :  *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3              :  *
       4              :  *  This program is free software: you can redistribute it and/or modify
       5              :  *  it under the terms of the GNU General Public License as published by
       6              :  *  the Free Software Foundation, either version 3 of the License, or
       7              :  *  (at your option) any later version.
       8              :  *
       9              :  *  This program is distributed in the hope that it will be useful,
      10              :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11              :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12              :  *  GNU General Public License for more details.
      13              :  *
      14              :  *  You should have received a copy of the GNU General Public License
      15              :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16              :  */
      17              : 
      18              : #include "sync_module.h"
      19              : 
      20              : #include "jamidht/conversation_module.h"
      21              : #include "jamidht/archive_account_manager.h"
      22              : #include "fileutils.h"
      23              : 
      24              : #include <dhtnet/multiplexed_socket.h>
      25              : #include <dhtnet/channel_utils.h>
      26              : #include <opendht/thread_pool.h>
      27              : 
      28              : #include <fstream>
      29              : 
      30              : namespace jami {
      31              : 
      32              : class SyncModule::Impl : public std::enable_shared_from_this<Impl>
      33              : {
      34              : public:
      35              :     Impl(const std::shared_ptr<JamiAccount>& account);
      36              : 
      37              :     std::weak_ptr<JamiAccount> account_;
      38              :     const std::string accountId_;
      39              : 
      40              :     // Sync connections
      41              :     std::recursive_mutex syncConnectionsMtx_;
      42              :     std::map<DeviceId /* deviceId */, std::vector<std::shared_ptr<dhtnet::ChannelSocket>>> syncConnections_;
      43              : 
      44              :     // Local sync-version tracking (never transmitted, see header). Used to
      45              :     // decide whether a sync connection must be (re)established with a device.
      46              :     std::filesystem::path versionPath_;
      47              :     mutable std::mutex versionMtx_;
      48              :     uint64_t localVersion_ {0};
      49              :     std::map<DeviceId, uint64_t> lastSynced_;
      50              : 
      51              :     void loadVersions();
      52              :     void saveVersions(); // versionMtx_ must be held
      53              :     uint64_t bumpVersion();
      54              :     uint64_t currentVersion() const;
      55              :     bool needsSync(const DeviceId& deviceId) const;
      56              :     void markSynced(const DeviceId& deviceId, uint64_t version);
      57              : 
      58              :     /**
      59              :      * Build SyncMsg and send it on socket
      60              :      * @param socket
      61              :      * @return true if the whole state was written without error
      62              :      */
      63              :     bool syncInfos(const std::shared_ptr<dhtnet::ChannelSocket>& socket, const std::shared_ptr<SyncMsg>& syncMsg);
      64              :     void onChannelShutdown(const std::shared_ptr<dhtnet::ChannelSocket>& socket, const DeviceId& device);
      65              : };
      66              : 
      67              : namespace {
      68              : // On-disk representation of the local sync-version state.
      69              : struct SyncVersionData
      70              : {
      71              :     uint64_t version {0};
      72              :     std::map<DeviceId, uint64_t> synced;
      73          373 :     MSGPACK_DEFINE_MAP(version, synced)
      74              : };
      75              : } // namespace
      76              : 
      77          708 : SyncModule::Impl::Impl(const std::shared_ptr<JamiAccount>& account)
      78          708 :     : account_(account)
      79         1416 :     , accountId_ {account->getAccountID()}
      80              : {
      81          708 :     versionPath_ = account->getPath() / "syncVersions";
      82          708 :     loadVersions();
      83          708 : }
      84              : 
      85              : void
      86          708 : SyncModule::Impl::loadVersions()
      87              : {
      88              :     try {
      89         1416 :         auto file = fileutils::loadFile(versionPath_);
      90            0 :         msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
      91            0 :         SyncVersionData data;
      92            0 :         oh.get().convert(data);
      93            0 :         std::lock_guard lk(versionMtx_);
      94            0 :         localVersion_ = data.version;
      95            0 :         lastSynced_ = std::move(data.synced);
      96          708 :     } catch (const std::exception&) {
      97              :         // No (or unreadable) file yet: start fresh. Every known device will be
      98              :         // considered out-of-date and synced once on first contact.
      99          708 :     }
     100          708 : }
     101              : 
     102              : void
     103          373 : SyncModule::Impl::saveVersions()
     104              : {
     105              :     // versionMtx_ must be held
     106              :     try {
     107          373 :         std::ofstream file(versionPath_, std::ios::trunc | std::ios::binary);
     108          373 :         SyncVersionData data;
     109          373 :         data.version = localVersion_;
     110          373 :         data.synced = lastSynced_;
     111          373 :         msgpack::pack(file, data);
     112          373 :     } catch (const std::exception& e) {
     113            0 :         JAMI_WARNING("[Account {}] Unable to save sync versions: {:s}", accountId_, e.what());
     114            0 :     }
     115          373 : }
     116              : 
     117              : uint64_t
     118          353 : SyncModule::Impl::bumpVersion()
     119              : {
     120          353 :     std::lock_guard lk(versionMtx_);
     121          353 :     ++localVersion_;
     122          353 :     saveVersions();
     123          353 :     return localVersion_;
     124          353 : }
     125              : 
     126              : uint64_t
     127          138 : SyncModule::Impl::currentVersion() const
     128              : {
     129          138 :     std::lock_guard lk(versionMtx_);
     130          138 :     return localVersion_;
     131          138 : }
     132              : 
     133              : bool
     134         1121 : SyncModule::Impl::needsSync(const DeviceId& deviceId) const
     135              : {
     136         1121 :     std::lock_guard lk(versionMtx_);
     137         1121 :     auto it = lastSynced_.find(deviceId);
     138              :     // Never synced, or synced at an older version than the current one.
     139         2242 :     return it == lastSynced_.end() || it->second < localVersion_;
     140         1121 : }
     141              : 
     142              : void
     143          138 : SyncModule::Impl::markSynced(const DeviceId& deviceId, uint64_t version)
     144              : {
     145          138 :     std::lock_guard lk(versionMtx_);
     146          138 :     auto& synced = lastSynced_[deviceId];
     147          138 :     if (synced < version) {
     148           20 :         synced = version;
     149           20 :         saveVersions();
     150              :     }
     151          138 : }
     152              : 
     153              : bool
     154          225 : SyncModule::Impl::syncInfos(const std::shared_ptr<dhtnet::ChannelSocket>& socket,
     155              :                             const std::shared_ptr<SyncMsg>& syncMsg)
     156              : {
     157          225 :     auto acc = account_.lock();
     158          225 :     if (!acc)
     159            0 :         return false;
     160          225 :     msgpack::sbuffer buffer(UINT16_MAX); // Use max pkt size
     161          225 :     std::error_code ec;
     162          225 :     if (!syncMsg) {
     163              :         // Send contacts infos
     164              :         // This message can be big. TODO rewrite to only take UINT16_MAX bytes max or split it multiple
     165              :         // messages. For now, write 3 messages (UINT16_MAX*3 should be enough for all information).
     166          184 :         if (auto info = acc->accountManager()->getInfo()) {
     167          184 :             if (info->contacts) {
     168          184 :                 SyncMsg msg;
     169          184 :                 msg.ds = info->contacts->getSyncData();
     170          184 :                 msgpack::pack(buffer, msg);
     171          184 :                 socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     172          184 :                 if (ec) {
     173            0 :                     JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     174            0 :                     return false;
     175              :                 }
     176          184 :             }
     177              :         }
     178          184 :         buffer.clear();
     179              :         // Sync conversations. Collaborative documents are excluded: replication
     180              :         // is a per-device choice, each device joins one by opening it.
     181          184 :         auto c = ConversationModule::convInfos(acc->getAccountID());
     182          304 :         for (auto it = c.begin(); it != c.end();) {
     183          120 :             if (it->second.mode == ConversationMode::DOCUMENT)
     184            8 :                 it = c.erase(it);
     185              :             else
     186          112 :                 ++it;
     187              :         }
     188          184 :         if (!c.empty()) {
     189           96 :             SyncMsg msg;
     190           96 :             msg.c = std::move(c);
     191           96 :             msgpack::pack(buffer, msg);
     192           96 :             socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     193           96 :             if (ec) {
     194            0 :                 JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     195            0 :                 return false;
     196              :             }
     197           96 :         }
     198          184 :         buffer.clear();
     199              :         // Sync requests
     200          184 :         auto cr = ConversationModule::convRequests(acc->getAccountID());
     201          184 :         if (!cr.empty()) {
     202            9 :             SyncMsg msg;
     203            9 :             msg.cr = std::move(cr);
     204            9 :             msgpack::pack(buffer, msg);
     205            9 :             socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     206            9 :             if (ec) {
     207            0 :                 JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     208            0 :                 return false;
     209              :             }
     210            9 :         }
     211          184 :         buffer.clear();
     212          184 :         auto convModule = acc->convModule(true);
     213          184 :         if (!convModule)
     214            0 :             return false;
     215              :         // Sync conversation's preferences
     216          184 :         auto p = convModule->convPreferences();
     217          184 :         if (!p.empty()) {
     218            3 :             SyncMsg msg;
     219            3 :             msg.p = std::move(p);
     220            3 :             msgpack::pack(buffer, msg);
     221            3 :             socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     222            3 :             if (ec) {
     223            0 :                 JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     224            0 :                 return false;
     225              :             }
     226            3 :         }
     227          184 :         buffer.clear();
     228              :         // Sync read's status
     229          183 :         auto ms = convModule->convMessageStatus();
     230          184 :         if (!ms.empty()) {
     231           38 :             SyncMsg msg;
     232           38 :             msg.ms = std::move(ms);
     233           38 :             msgpack::pack(buffer, msg);
     234           38 :             socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     235           38 :             if (ec) {
     236            0 :                 JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     237            0 :                 return false;
     238              :             }
     239           38 :         }
     240          184 :         buffer.clear();
     241              : 
     242          184 :     } else {
     243           41 :         msgpack::pack(buffer, *syncMsg);
     244           41 :         socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec);
     245           41 :         if (ec) {
     246            0 :             JAMI_ERROR("[Account {}] [device {}] {:s}", accountId_, socket->deviceId(), ec.message());
     247            0 :             return false;
     248              :         }
     249              :     }
     250          224 :     return true;
     251          224 : }
     252              : 
     253              : ////////////////////////////////////////////////////////////////
     254              : 
     255          708 : SyncModule::SyncModule(const std::shared_ptr<JamiAccount>& account)
     256          708 :     : pimpl_ {std::make_shared<Impl>(account)}
     257          708 : {}
     258              : 
     259              : void
     260          138 : SyncModule::Impl::onChannelShutdown(const std::shared_ptr<dhtnet::ChannelSocket>& socket, const DeviceId& device)
     261              : {
     262          138 :     std::lock_guard lk(syncConnectionsMtx_);
     263          138 :     auto connectionsIt = syncConnections_.find(device);
     264          138 :     if (connectionsIt == syncConnections_.end()) {
     265            0 :         JAMI_WARNING("[Account {}] [device {}] onChannelShutdown: no connection found.", accountId_, device.to_view());
     266            0 :         return;
     267              :     }
     268          138 :     auto& connections = connectionsIt->second;
     269          138 :     auto conn = std::find(connections.begin(), connections.end(), socket);
     270          138 :     if (conn != connections.end())
     271          138 :         connections.erase(conn);
     272          138 :     JAMI_LOG("[Account {}] [device {}] removed connection, remaining: {:d}",
     273              :              accountId_,
     274              :              device.to_view(),
     275              :              connections.size());
     276          138 :     if (connections.empty())
     277           70 :         syncConnections_.erase(connectionsIt);
     278          138 : }
     279              : 
     280              : void
     281          138 : SyncModule::cacheSyncConnection(std::shared_ptr<dhtnet::ChannelSocket>&& socket,
     282              :                                 const std::string& peerId,
     283              :                                 const DeviceId& device)
     284              : {
     285          138 :     std::lock_guard lk(pimpl_->syncConnectionsMtx_);
     286          138 :     pimpl_->syncConnections_[device].emplace_back(socket);
     287              : 
     288          138 :     socket->setOnRecv(dhtnet::buildMsgpackReader<SyncMsg>([acc = pimpl_->account_, device, peerId](SyncMsg&& msg) {
     289          371 :         auto account = acc.lock();
     290          370 :         if (!account)
     291            0 :             return std::make_error_code(std::errc::operation_canceled);
     292              : 
     293              :         try {
     294          370 :             if (auto manager = account->accountManager())
     295          371 :                 manager->onSyncData(std::move(msg.ds), false);
     296              : 
     297          371 :             if (!msg.c.empty() || !msg.cr.empty() || !msg.p.empty() || !msg.ld.empty() || !msg.ms.empty())
     298          172 :                 if (auto cm = account->convModule(true))
     299          172 :                     cm->onSyncData(msg, peerId, device.toString());
     300            0 :         } catch (const std::exception& e) {
     301            0 :             JAMI_WARNING("[Account {}] [device {}] [convInfo] error on sync: {:s}",
     302              :                          account->getAccountID(),
     303              :                          device.to_view(),
     304              :                          e.what());
     305            0 :         }
     306          371 :         return std::error_code();
     307          371 :     }));
     308          138 :     socket->onShutdown([w = pimpl_->weak_from_this(), device, s = std::weak_ptr(socket)](const std::error_code&) {
     309          138 :         if (auto shared = w.lock())
     310          138 :             shared->onChannelShutdown(s.lock(), device);
     311          138 :     });
     312              : 
     313              :     // Capture the version we are about to deliver before sending the full
     314              :     // state. On success, record that this device is synced up to that version
     315              :     // so we don't reconnect to it until something changes again. Captured
     316              :     // before the send so a concurrent change is never considered delivered.
     317          138 :     auto version = pimpl_->currentVersion();
     318          138 :     dht::ThreadPool::io().run([w = pimpl_->weak_from_this(), socket = std::move(socket), device, version]() {
     319          138 :         if (auto s = w.lock()) {
     320          138 :             if (s->syncInfos(socket, nullptr))
     321          138 :                 s->markSynced(device, version);
     322          138 :         }
     323          138 :     });
     324          138 : }
     325              : 
     326              : bool
     327           70 : SyncModule::isConnected(const DeviceId& deviceId) const
     328              : {
     329           70 :     std::lock_guard lk(pimpl_->syncConnectionsMtx_);
     330           70 :     auto it = pimpl_->syncConnections_.find(deviceId);
     331           70 :     if (it == pimpl_->syncConnections_.end())
     332           69 :         return false;
     333            1 :     return !it->second.empty();
     334           70 : }
     335              : 
     336              : void
     337         2831 : SyncModule::syncWithConnected(const std::shared_ptr<SyncMsg>& syncMsg, const DeviceId& deviceId)
     338              : {
     339         2831 :     std::lock_guard lk(pimpl_->syncConnectionsMtx_);
     340         2831 :     size_t count = 0;
     341         2918 :     for (const auto& [did, sockets] : pimpl_->syncConnections_) {
     342           87 :         if (not sockets.empty() and (!deviceId || deviceId == did)) {
     343           87 :             count++;
     344           87 :             dht::ThreadPool::io().run([w = pimpl_->weak_from_this(), s = sockets.back(), syncMsg] {
     345           87 :                 if (auto sthis = w.lock())
     346           87 :                     sthis->syncInfos(s, syncMsg);
     347           87 :             });
     348              :         }
     349              :     }
     350         2831 :     if (count == 0) {
     351         2744 :         JAMI_WARNING("[Account {}] [device {}] no sync connection.", pimpl_->accountId_, deviceId.toString());
     352              :     } else {
     353           87 :         JAMI_DEBUG("[Account {}] [device {}] syncing with {:d} devices", pimpl_->accountId_, deviceId.to_view(), count);
     354              :     }
     355         2831 : }
     356              : 
     357              : uint64_t
     358          353 : SyncModule::bumpVersion()
     359              : {
     360          353 :     return pimpl_->bumpVersion();
     361              : }
     362              : 
     363              : uint64_t
     364            0 : SyncModule::currentVersion() const
     365              : {
     366            0 :     return pimpl_->currentVersion();
     367              : }
     368              : 
     369              : bool
     370         1121 : SyncModule::needsSync(const DeviceId& deviceId) const
     371              : {
     372         1121 :     return pimpl_->needsSync(deviceId);
     373              : }
     374              : 
     375              : void
     376            0 : SyncModule::markSynced(const DeviceId& deviceId, uint64_t version)
     377              : {
     378            0 :     pimpl_->markSynced(deviceId, version);
     379            0 : }
     380              : 
     381              : } // namespace jami
        

Generated by: LCOV version 2.0-1