diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 070e0f74b8..5033315c11 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -2130,6 +2130,7 @@ else() include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake) generate_dbus(Telegram org.freedesktop.portal. XdpBackground ${third_party_loc}/xdg-desktop-portal/data/org.freedesktop.portal.Background.xml) + generate_dbus(Telegram org.freedesktop.portal. FlatpakPortal ${src_loc}/platform/linux/org.freedesktop.portal.Flatpak.xml) generate_dbus(Telegram org.freedesktop. XdgNotifications ${src_loc}/platform/linux/org.freedesktop.Notifications.xml) if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 276fe1b73c..fa81899673 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -31,9 +31,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include +#if !defined Q_OS_WIN && !defined Q_OS_MAC +#include "base/platform/linux/base_linux_xdp_utilities.h" + +#include +#endif // !Q_OS_WIN && !Q_OS_MAC + extern "C" { #include #include @@ -59,6 +66,12 @@ namespace { constexpr auto kUpdaterTimeout = 10 * crl::time(1000); constexpr auto kMaxResponseSize = 1024 * 1024; +#if !defined Q_OS_WIN && !defined Q_OS_MAC +constexpr auto kFlatpakPortalService = "org.freedesktop.portal.Flatpak"; +constexpr auto kFlatpakPortalObjectPath = "/org/freedesktop/portal/Flatpak"; +constexpr auto kFlatpakUpdated = "/app/.updated"_cs; +#endif // !Q_OS_WIN && !Q_OS_MAC + #ifdef TDESKTOP_DISABLE_AUTOUPDATE bool UpdaterIsDisabled = true; #else // TDESKTOP_DISABLE_AUTOUPDATE @@ -80,6 +93,11 @@ using VersionChar = wchar_t; using Loader = MTP::AbstractDedicatedLoader; +#if !defined Q_OS_WIN && !defined Q_OS_MAC +using namespace gi::repository; +namespace GObject = gi::repository::GObject; +#endif // !Q_OS_WIN && !Q_OS_MAC + struct BIODeleter { void operator()(BIO *value) { BIO_free(value); @@ -98,6 +116,8 @@ public: virtual void start() = 0; + virtual bool poll() const; + rpl::producer> ready() const; rpl::producer<> failed() const; @@ -217,6 +237,40 @@ private: }; +#if !defined Q_OS_WIN && !defined Q_OS_MAC +class FlatpakChecker : public Checker { +public: + FlatpakChecker(bool testing); + + void start() override; + + bool poll() const override; + + ~FlatpakChecker(); + +private: + FlatpakPortal::Flatpak _interface; + FlatpakPortal::FlatpakUpdateMonitor _monitor; + QFileSystemWatcher _watcher; + ulong _updateAvailableSignal = 0; + +}; + +class FlatpakLoader : public Loader { +public: + FlatpakLoader(FlatpakPortal::FlatpakUpdateMonitor monitor); + + ~FlatpakLoader(); + +private: + void startLoading() override; + + FlatpakPortal::FlatpakUpdateMonitor _monitor; + ulong _progressSignal = 0; + +}; +#endif // !Q_OS_WIN && !Q_OS_MAC + std::shared_ptr GetUpdaterInstance() { if (const auto result = UpdaterInstance.lock()) { return result; @@ -271,6 +325,10 @@ QString ExtractFilename(const QString &url) { bool UnpackUpdate(const QString &filepath) { #ifndef TDESKTOP_DISABLE_AUTOUPDATE + if (filepath.isEmpty()) { + return true; + } + QFile input(filepath); if (!input.open(QIODevice::ReadOnly)) { LOG(("Update Error: cant read updates file!")); @@ -633,6 +691,10 @@ rpl::producer<> Checker::failed() const { return _failed.events(); } +bool Checker::poll() const { + return true; +} + bool Checker::testing() const { return _testing; } @@ -1050,6 +1112,161 @@ Fn MtpChecker::failHandler() { }; } +#if !defined Q_OS_WIN && !defined Q_OS_MAC +FlatpakChecker::FlatpakChecker(bool testing) +: Checker(testing) +, _watcher({u"/app"_q}) { + FlatpakPortal::FlatpakProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + kFlatpakPortalService, + kFlatpakPortalObjectPath, + crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) { + auto result = FlatpakPortal::FlatpakProxy::new_for_bus_finish(res); + if (!result) { + Gio::DBusErrorNS_::strip_remote_error(result.error()); + LOG(("Update Error: %1").arg(result.error().message_().c_str())); + return; + } + + _interface = *result; + _interface.call_create_update_monitor( + GLib::Variant::new_array( + GLib::VariantType::new_("{sv}"), + {}), + [=](GObject::Object, Gio::AsyncResult res) { + const auto result = _interface.call_create_update_monitor_finish( + res); + + if (!result) { + Gio::DBusErrorNS_::strip_remote_error(result.error()); + LOG(("Update Error: %1").arg( + result.error().message_().c_str())); + fail(); + return; + } + + FlatpakPortal::FlatpakUpdateMonitorProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + kFlatpakPortalService, + std::get<1>(*result), + crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) { + using FlatpakPortal::FlatpakUpdateMonitorProxy; + auto result = FlatpakUpdateMonitorProxy::new_for_bus_finish( + res); + + if (!result) { + Gio::DBusErrorNS_::strip_remote_error(result.error()); + LOG(("Update Error: %1").arg( + result.error().message_().c_str())); + fail(); + return; + } + + _monitor = *result; + _updateAvailableSignal + = _monitor.signal_update_available().connect([=]( + FlatpakPortal::FlatpakUpdateMonitor, + GLib::Variant updateInfo) { + done(std::make_shared(_monitor)); + }); + })); + }); + })); + + QObject::connect( + &_watcher, + &QFileSystemWatcher::directoryChanged, + [=](const QString &path) { + start(); + }); +} + +void FlatpakChecker::start() { + if (QFileInfo::exists(kFlatpakUpdated.utf16())) { + done(std::make_shared(_monitor)); + } +} + +bool FlatpakChecker::poll() const { + return false; +} + +FlatpakChecker::~FlatpakChecker() { + if (_monitor) { + _monitor.disconnect(_updateAvailableSignal); + _monitor.call_close(nullptr); + } +} + +FlatpakLoader::FlatpakLoader(FlatpakPortal::FlatpakUpdateMonitor monitor) +: Loader({}, kChunkSize) +, _monitor(monitor) { + if (!_monitor) { + return; + } + + _progressSignal = _monitor.signal_progress().connect([=]( + FlatpakPortal::FlatpakUpdateMonitor, + GLib::Variant info) { + auto dict = GLib::VariantDict::new_(info); + switch (dict.lookup_value("status").get_uint32()) { + case 0: { + const auto n_ops = dict.lookup_value("n_ops").get_uint32(); + const auto op = dict.lookup_value("op").get_uint32(); + const auto progress = dict.lookup_value("progress").get_uint32(); + threadSafeProgress({ + int64( + std::round((op + (progress / 100.)) / n_ops * 104857600)), + 104857600, + true, + }); + } break; + case 1: + case 2: threadSafeReady(); break; + case 3: { + LOG(("Update Error: %1").arg( + dict.lookup_value("error_message").get_string( + nullptr).c_str())); + threadSafeFailed(); + } break; + } + }); +} + +void FlatpakLoader::startLoading() { + if (QFileInfo::exists(kFlatpakUpdated.utf16())) { + threadSafeReady(); + } + + if (!_monitor) { + return; + } + + _monitor.call_update( + base::Platform::XDP::ParentWindowID(), + GLib::Variant::new_array( + GLib::VariantType::new_("{sv}"), + {}), + crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) { + const auto result = _monitor.call_close_finish(res); + if (!result) { + Gio::DBusErrorNS_::strip_remote_error(result.error()); + LOG(("Update Error: %1").arg( + result.error().message_().c_str())); + threadSafeFailed(); + } + })); +} + +FlatpakLoader::~FlatpakLoader() { + if (_monitor) { + _monitor.disconnect(_progressSignal); + } +} +#endif // !Q_OS_WIN && !Q_OS_MAC + } // namespace bool UpdaterDisabled() { @@ -1079,6 +1296,7 @@ public: State state() const; int already() const; int size() const; + bool percent() const; void setMtproto(base::weak_ptr session); @@ -1123,6 +1341,7 @@ private: rpl::event_stream<> _ready; Implementation _httpImplementation; Implementation _mtpImplementation; + Implementation _flatpakImplementation; std::shared_ptr _activeLoader; bool _usingMtprotoLoader = (cAlphaVersion() != 0); base::weak_ptr _session; @@ -1231,9 +1450,16 @@ int Updater::already() const { return _activeLoader ? _activeLoader->alreadySize() : 0; } +bool Updater::percent() const { + return _activeLoader ? _activeLoader->preferPercent() : 0; +} + void Updater::stop() { _httpImplementation = Implementation(); _mtpImplementation = Implementation(); + _flatpakImplementation = Implementation{ + std::move(_flatpakImplementation.checker) + }; _activeLoader = nullptr; _action = Action::Waiting; } @@ -1267,7 +1493,15 @@ void Updater::start(bool forceWait) { return; } - if (sendRequest) { + if (KSandbox::isFlatpak()) { +#if !defined Q_OS_WIN && !defined Q_OS_MAC + if (!_flatpakImplementation.checker) { + startImplementation( + &_flatpakImplementation, + std::make_unique(_testing)); + } +#endif // !Q_OS_WIN && !Q_OS_MAC + } else if (sendRequest) { startImplementation( &_httpImplementation, std::make_unique(_testing)); @@ -1317,7 +1551,7 @@ void Updater::startImplementation( void Updater::checkerDone( not_null which, std::shared_ptr loader) { - which->checker = nullptr; + if (which->checker->poll()) which->checker = nullptr; which->loader = std::move(loader); tryLoaders(); @@ -1388,7 +1622,14 @@ bool Updater::tryLoaders() { _isLatest.fire({}); } }; - if (_mtpImplementation.failed && _httpImplementation.failed) { + if (KSandbox::isFlatpak()) { + if (_flatpakImplementation.failed) { + _failed.fire({}); + return false; + } else { + tryOne(_flatpakImplementation); + } + } else if (_mtpImplementation.failed && _httpImplementation.failed) { _failed.fire({}); return false; } else if (!_mtpImplementation.loader) { @@ -1491,6 +1732,10 @@ int UpdateChecker::size() const { return _updater->size(); } +bool UpdateChecker::percent() const { + return _updater->percent(); +} + //QString winapiErrorWrap() { // WCHAR errMsg[2048]; // DWORD errorCode = GetLastError(); diff --git a/Telegram/SourceFiles/core/update_checker.h b/Telegram/SourceFiles/core/update_checker.h index 4e32c30002..458ffc75a9 100644 --- a/Telegram/SourceFiles/core/update_checker.h +++ b/Telegram/SourceFiles/core/update_checker.h @@ -46,6 +46,7 @@ public: State state() const; int already() const; int size() const; + bool percent() const; private: const std::shared_ptr _updater; diff --git a/Telegram/SourceFiles/mtproto/dedicated_file_loader.cpp b/Telegram/SourceFiles/mtproto/dedicated_file_loader.cpp index 057b055e94..1a63c5507e 100644 --- a/Telegram/SourceFiles/mtproto/dedicated_file_loader.cpp +++ b/Telegram/SourceFiles/mtproto/dedicated_file_loader.cpp @@ -155,14 +155,22 @@ AbstractDedicatedLoader::AbstractDedicatedLoader( int chunkSize) : _filepath(filepath) , _chunkSize(chunkSize) { + progress() | rpl::on_next([=](Progress progress) { + QMutexLocker lock(&_sizesMutex); + _alreadySize = progress.already; + _totalSize = progress.size; + _preferPercent = progress.percent; + }, lifetime()); } void AbstractDedicatedLoader::start() { - if (!validateOutput() - || (!_output.isOpen() && !_output.open(QIODevice::Append))) { - QFile(_filepath).remove(); - threadSafeFailed(); - return; + if (!_filepath.isEmpty()) { + if (!validateOutput() + || (!_output.isOpen() && !_output.open(QIODevice::Append))) { + QFile(_filepath).remove(); + threadSafeFailed(); + return; + } } LOG(("Update Info: Starting loading '%1' from %2 offset." @@ -181,6 +189,10 @@ int64 AbstractDedicatedLoader::totalSize() const { return _totalSize; } +bool AbstractDedicatedLoader::preferPercent() const { + return _preferPercent; +} + rpl::producer AbstractDedicatedLoader::ready() const { return _ready.events(); } @@ -194,6 +206,9 @@ rpl::producer<> AbstractDedicatedLoader::failed() const { } void AbstractDedicatedLoader::wipeFolder() { + if (_filepath.isEmpty()) { + return; + } QFileInfo info(_filepath); const auto dir = info.dir(); const auto all = dir.entryInfoList(QDir::Files); diff --git a/Telegram/SourceFiles/mtproto/dedicated_file_loader.h b/Telegram/SourceFiles/mtproto/dedicated_file_loader.h index 4a4e7eb24a..652feb250f 100644 --- a/Telegram/SourceFiles/mtproto/dedicated_file_loader.h +++ b/Telegram/SourceFiles/mtproto/dedicated_file_loader.h @@ -54,6 +54,7 @@ public: struct Progress { int64 already = 0; int64 size = 0; + bool percent = false; inline bool operator<(const Progress &other) const { return (already < other.already) @@ -70,6 +71,7 @@ public: int64 alreadySize() const; int64 totalSize() const; + bool preferPercent() const; rpl::producer progress() const; rpl::producer ready() const; @@ -81,6 +83,8 @@ public: protected: void threadSafeFailed(); + void threadSafeProgress(Progress progress); + void threadSafeReady(); // Single threaded. void writeChunk(bytes::const_span data, int totalSize); @@ -89,8 +93,6 @@ private: virtual void startLoading() = 0; bool validateOutput(); - void threadSafeProgress(Progress progress); - void threadSafeReady(); QString _filepath; int _chunkSize = 0; @@ -98,6 +100,7 @@ private: QFile _output; int64 _alreadySize = 0; int64 _totalSize = 0; + bool _preferPercent = false; mutable QMutex _sizesMutex; rpl::event_stream _progress; rpl::event_stream _ready; diff --git a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp index 869b79e76f..d31f2838e5 100644 --- a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webview/platform/linux/webview_linux_webkitgtk.h" #include +#include #include #ifdef __GLIBC__ @@ -45,24 +46,23 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { return false; } - const auto justRelaunch = action == UpdaterLaunch::JustRelaunch; + const auto justRelaunch = action == UpdaterLaunch::JustRelaunch + || KSandbox::isInside(); + if (action == UpdaterLaunch::PerformUpdate) { _updating = true; } std::vector argumentsList; - // What we are launching. - const auto launching = justRelaunch - ? (cExeDir() + cExeName()) - : cWriteProtected() - ? GLib::find_program_in_path("run0") - ? u"run0"_q - : u"pkexec"_q - : (cExeDir() + u"Updater"_q); - argumentsList.push_back(launching.toStdString()); - - if (justRelaunch) { + if (KSandbox::isFlatpak() && _updating) { + argumentsList.push_back("flatpak-spawn"); + argumentsList.push_back("--latest-version"); + argumentsList.push_back((cExeDir() + cExeName()).toStdString()); + } else if (justRelaunch) { + // What we are launching. + const auto launching = (cExeDir() + cExeName()); + argumentsList.push_back(launching.toStdString()); // argv[0] that is passed to what we are launching. // It should be added explicitly in case of FILE_AND_ARGV_ZERO_. const auto argv0 = !arguments().isEmpty() @@ -70,9 +70,13 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { : launching; argumentsList.push_back(argv0.toStdString()); } else if (cWriteProtected()) { - // Elevated process that run0/pkexec should launch. - const auto elevated = cWorkingDir() + u"tupdates/temp/Updater"_q; - argumentsList.push_back(elevated.toStdString()); + argumentsList.push_back(GLib::find_program_in_path("run0") + ? "run0" + : "pkexec"); + argumentsList.push_back( + cWorkingDir().toStdString() + "tupdates/temp/Updater"); + } else { + argumentsList.push_back(cExeDir().toStdString() + "Updater"); } if (Logs::DebugEnabled()) { @@ -122,7 +126,9 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { initialWorkingDir().toStdString(), argumentsList, {}, - GLib::SpawnFlags::FILE_AND_ARGV_ZERO_, + KSandbox::isFlatpak() && _updating + ? GLib::SpawnFlags::SEARCH_PATH_ + : GLib::SpawnFlags::FILE_AND_ARGV_ZERO_, nullptr, nullptr, nullptr); diff --git a/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Flatpak.xml b/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Flatpak.xml new file mode 100644 index 0000000000..4f2139355a --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Flatpak.xml @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index abe2197966..b970da0b01 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -470,7 +470,10 @@ void InstallLauncher() { "DESKTOPINTEGRATION"); // don't update desktop file for alpha version or if updater is disabled - if (cAlphaVersion() || Core::UpdaterDisabled() || DisabledByEnv) { + if (cAlphaVersion() + || Core::UpdaterDisabled() + || KSandbox::isInside() + || DisabledByEnv) { return; } diff --git a/Telegram/SourceFiles/settings/sections/settings_advanced.cpp b/Telegram/SourceFiles/settings/sections/settings_advanced.cpp index ece7d62581..1a99cc92f3 100644 --- a/Telegram/SourceFiles/settings/sections/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_advanced.cpp @@ -78,6 +78,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/platform/platform_spellcheck.h" #endif // !TDESKTOP_DISABLE_SPELLCHECK +#include + namespace Settings { namespace { @@ -1014,7 +1016,7 @@ void BuildUpdateSection(SectionBuilder &builder, bool atTop) { auto install = (Ui::SettingsButton*)nullptr; auto check = (Ui::SettingsButton*)nullptr; builder.scope([&] { - install = cAlphaVersion() + install = (cAlphaVersion() || KSandbox::isInside()) ? nullptr : builder.addButton({ .id = u"advanced/install_beta"_q, @@ -1050,11 +1052,32 @@ void BuildUpdateSection(SectionBuilder &builder, bool atTop) { update->moveToLeft(0, 0); }, update->lifetime()); - const auto showDownloadProgress = [=](int64 ready, int64 total) { + const auto showDownloadProgress = [=]( + int64 ready, + int64 total, + bool preferPercent) { + const auto formatted = [&] { + if (!preferPercent) { + return Ui::FormatDownloadText(ready, total); + } + const auto percent = (total > 0) + ? std::clamp((ready * 100) / float64(total), 0., 100.) + : 0.; + auto result = QString::number(percent, 'f', 2); + if (result.contains('.')) { + while (result.endsWith('0')) { + result.chop(1); + } + if (result.endsWith('.')) { + result.chop(1); + } + } + return result + '%'; + }(); texts->fire(tr::lng_settings_downloading_update( tr::now, lt_progress, - Ui::FormatDownloadText(ready, total))); + formatted)); downloading->fire(true); }; const auto setDefaultStatus = [=]( @@ -1063,7 +1086,10 @@ void BuildUpdateSection(SectionBuilder &builder, bool atTop) { const auto state = checker.state(); switch (state) { case State::Download: - showDownloadProgress(checker.already(), checker.size()); + showDownloadProgress( + checker.already(), + checker.size(), + checker.percent()); break; case State::Ready: texts->fire(tr::lng_settings_update_ready(tr::now)); @@ -1120,7 +1146,10 @@ void BuildUpdateSection(SectionBuilder &builder, bool atTop) { }, options->lifetime()); checker.progress( ) | rpl::on_next([=](Core::UpdateChecker::Progress progress) { - showDownloadProgress(progress.already, progress.size); + showDownloadProgress( + progress.already, + progress.size, + progress.percent); }, options->lifetime()); checker.failed() | rpl::on_next([=] { options->setAttribute(Qt::WA_TransparentForMouseEvents, false); @@ -1344,7 +1373,7 @@ void SetupUpdate(not_null container) { container, object_ptr(container))); const auto inner = options->entity(); - const auto install = cAlphaVersion() + const auto install = (cAlphaVersion() || KSandbox::isInside()) ? nullptr : inner->add(object_ptr