/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * Copyright the Collabora Online contributors. * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * The main entry point for the LibreOfficeKit process serving * a document editing session. */ #include #include #include #include #ifdef __linux__ #include #include #include #include #include #endif #ifdef __FreeBSD__ #include #define FTW_CONTINUE 0 #define FTW_STOP (-1) #define FTW_SKIP_SUBTREE 0 #define FTW_ACTIONRETVAL 0 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOK_USE_UNSTABLE_API #include #include #include #include #include #include #include "ChildSession.hpp" #include #include #include #include #include #include "KitHelper.hpp" #include "Kit.hpp" #include #include #include #include #include #include #include #include "Watermark.hpp" #include "RenderTiles.hpp" #include "KitWebSocket.hpp" #include "SetupKitEnvironment.hpp" #include #include #include #if !MOBILEAPP #include #include #include #endif #if MOBILEAPP #include "COOLWSD.hpp" #endif #ifdef IOS #include "ios.h" #endif #define LIB_SOFFICEAPP "lib" "sofficeapp" ".so" #define LIB_MERGED "lib" "mergedlo" ".so" using Poco::Exception; using Poco::File; using Poco::JSON::Object; using Poco::JSON::Parser; using Poco::URI; #ifndef BUILDING_TESTS using Poco::Path; #endif using namespace COOLProtocol; extern "C" { void dump_kit_state(void); /* easy for gdb */ } #if !MOBILEAPP // A Kit process hosts only a single document in its lifetime. class Document; static Document *singletonDocument = nullptr; static std::unique_ptr threadCounter; static std::unique_ptr fdCounter; int getCurrentThreadCount() { if (threadCounter) return threadCounter->count(); else return -1; } #endif _LibreOfficeKit* loKitPtr = nullptr; /// Used for test code to accelerating waiting until idle and to /// flush sockets with a 'processtoidle' -> 'idle' reply. static std::chrono::steady_clock::time_point ProcessToIdleDeadline; #ifndef BUILDING_TESTS static bool AnonymizeUserData = false; static uint64_t AnonymizationSalt = 82589933; #endif static bool EnableWebsocketURP = false; static int URPStartCount = 0; bool isURPEnabled() { return EnableWebsocketURP; } /// When chroot is enabled, this is blank as all /// the paths inside the jail, relative to it's jail. /// E.g. /tmp/user/docs/... /// However, without chroot, the jail path is /// absolute in the system root. /// I.e. ChildRoot/JailId/tmp/user/docs/... /// We need to know where the jail really is /// because WSD doesn't know if chroot will succeed /// or fail, but it assumes the document path to /// be relative to the root of the jail (i.e. chroot /// expected to succeed). If it fails, or when caps /// are disabled, file paths would be relative to the /// system root, not the jail. static std::string JailRoot; static int URPtoLoFDs[2] { -1, -1 }; static int URPfromLoFDs[2] { -1, -1 }; // Abnormally we get LOK events from another thread, which must be // push safely into our main poll loop to process to keep all // socket buffer & event processing in a single, thread. bool pushToMainThread(LibreOfficeKitCallback cb, int type, const char *p, void *data); static LokHookFunction2* initFunction = nullptr; namespace { // for later consistency checking. static std::string UserDirPath; static std::string InstDirPath; std::string pathFromFileURL(const std::string &uri) { std::string decoded; Poco::URI::decode(uri, decoded); if (decoded.rfind("file://", 0) != 0) { LOG_ERR("Asked to load a very unusual file path: '" << uri << "' -> '" << decoded << "'"); return std::string(); } return decoded.substr(7); } void consistencyCheckFileExists(const std::string &uri) { std::string path = pathFromFileURL(uri); if (path.empty()) return; FileUtil::Stat stat(path); if (!stat.good() && stat.isFile()) LOG_ERR("Fatal system error: created file passed into document doesn't exist: '" << path << "'"); else LOG_TRC("File path '" << path << "' exists of length " << stat.size()); consistencyCheckJail(); } #if !defined(BUILDING_TESTS) && !MOBILEAPP enum class LinkOrCopyType { All, LO }; LinkOrCopyType linkOrCopyType; std::string sourceForLinkOrCopy; Poco::Path destinationForLinkOrCopy; bool forceInitialCopy; // some stackable file-systems have very slow first hard link creation std::string linkableForLinkOrCopy; // Place to stash copies that we can hard-link from std::chrono::time_point linkOrCopyStartTime; bool linkOrCopyVerboseLogging = false; unsigned linkOrCopyFileCount = 0; // Track to help quantify the link-or-copy performance. constexpr unsigned SlowLinkOrCopyLimitInSecs = 2; // After this many seconds, start spamming the logs. bool detectSlowStackingFileSystem([[maybe_unused]] const std::string& directory) { #ifdef __linux__ #ifndef OVERLAYFS_SUPER_MAGIC // From linux/magic.h. #define OVERLAYFS_SUPER_MAGIC 0x794c7630 #endif struct statfs fs; if (::statfs(directory.c_str(), &fs) != 0) { LOG_SYS("statfs failed on '" << directory << "'"); return false; } switch (fs.f_type) { // case FUSE_SUPER_MAGIC: ? case OVERLAYFS_SUPER_MAGIC: return true; default: return false; } #else return false; #endif } /// Returns the LinkOrCopyType as a human-readable string (for logging). std::string linkOrCopyTypeString(LinkOrCopyType type) { switch (type) { case LinkOrCopyType::LO: return "LibreOffice"; case LinkOrCopyType::All: return "all"; default: assert(!"Unknown LinkOrCopyType."); return "unknown"; } } bool shouldCopyDir(const char *path) { switch (linkOrCopyType) { case LinkOrCopyType::LO: return strcmp(path, "program/wizards") != 0 && strcmp(path, "sdk") != 0 && strcmp(path, "debugsource") != 0 && strcmp(path, "share/basic") != 0 && strncmp(path, "share/extensions/dict-", // preloaded sizeof("share/extensions/dict")) != 0 && strcmp(path, "share/Scripts/java") != 0 && strcmp(path, "share/Scripts/javascript") != 0 && strcmp(path, "share/config/wizard") != 0 && strcmp(path, "readmes") != 0 && strcmp(path, "help") != 0; default: // LinkOrCopyType::All return true; } } bool shouldLinkFile(const char *path) { switch (linkOrCopyType) { case LinkOrCopyType::LO: { if (strstr(path, "LICENSE") || strstr(path, "EULA") || strstr(path, "CREDITS") || strstr(path, "NOTICE")) return false; const char* dot = strrchr(path, '.'); if (!dot) return true; if (!strcmp(dot, ".dbg")) return false; if (!strcmp(dot, ".so")) { // NSS is problematic ... if (strstr(path, "libnspr4") || strstr(path, "libplds4") || strstr(path, "libplc4") || strstr(path, "libnss3") || strstr(path, "libnssckbi") || strstr(path, "libnsutil3") || strstr(path, "libssl3") || strstr(path, "libsoftokn3") || strstr(path, "libsqlite3") || strstr(path, "libfreeblpriv3")) return true; // As is Python ... if (strstr(path, "python-core")) return true; // otherwise drop the rest of the code. return false; } const char *vers; if ((vers = strstr(path, ".so."))) // .so.[digit]+ { for(int i = sizeof (".so."); vers[i] != '\0'; ++i) if (!isdigit(vers[i]) && vers[i] != '.') return true; return false; } return true; } default: // LinkOrCopyType::All return true; } } void linkOrCopyFile(const char* fpath, const std::string& newPath) { ++linkOrCopyFileCount; if (linkOrCopyVerboseLogging) LOG_INF("Linking file \"" << fpath << "\" to \"" << newPath << '"'); if (!forceInitialCopy) { // first try a simple hard-link if (link(fpath, newPath.c_str()) == 0) return; } // else always copy before linking to linkable/ // incrementally build our 'linkable/' copy nearby static bool canChown = true; // only if we can get permissions right if ((forceInitialCopy || errno == EXDEV) && canChown) { // then copy somewhere closer and hard link from there if (!forceInitialCopy) LOG_TRC("link(\"" << fpath << "\", \"" << newPath << "\") failed: " << strerror(errno) << ". Will try to link template."); std::string linkableCopy = linkableForLinkOrCopy + fpath; if (::link(linkableCopy.c_str(), newPath.c_str()) == 0) return; if (errno == ENOENT) { File(Path(linkableCopy).parent()).createDirectories(); if (!FileUtil::copy(fpath, linkableCopy, /*log=*/false, /*throw_on_error=*/false)) LOG_TRC("Failed to create linkable copy [" << fpath << "] to [" << linkableCopy.c_str() << "]"); else { // Match system permissions, so a file we can write is not shared across jails. struct stat ownerInfo; if (::stat(fpath, &ownerInfo) != 0 || ::chown(linkableCopy.c_str(), ownerInfo.st_uid, ownerInfo.st_gid) != 0) { LOG_ERR("Failed to stat or chown " << ownerInfo.st_uid << ":" << ownerInfo.st_gid << " " << linkableCopy << ": " << strerror(errno) << " missing cap_chown?, disabling linkable"); unlink(linkableCopy.c_str()); canChown = false; } else if (::link(linkableCopy.c_str(), newPath.c_str()) == 0) return; } } LOG_TRC("link(\"" << linkableCopy << "\", \"" << newPath << "\") failed: " << strerror(errno) << ". Cannot create linkable copy."); } static bool warned = false; if (!warned) { LOG_ERR("link(\"" << fpath << "\", \"" << newPath.c_str() << "\") failed: " << strerror(errno) << ". Very slow copying path triggered."); warned = true; } else LOG_TRC("link(\"" << fpath << "\", \"" << newPath.c_str() << "\") failed: " << strerror(errno) << ". Will copy."); if (!FileUtil::copy(fpath, newPath.c_str(), /*log=*/false, /*throw_on_error=*/false)) { LOG_FTL("Failed to copy or link [" << fpath << "] to [" << newPath << "]. Exiting."); Util::forcedExit(EX_SOFTWARE); } } int linkOrCopyFunction(const char *fpath, const struct stat* sb, int typeflag, struct FTW* /*ftwbuf*/) { if (strcmp(fpath, sourceForLinkOrCopy.c_str()) == 0) { LOG_TRC("nftw: Skipping redundant path: " << fpath); return FTW_CONTINUE; } if (!linkOrCopyVerboseLogging) { const auto durationInSecs = std::chrono::duration_cast( std::chrono::steady_clock::now() - linkOrCopyStartTime); if (durationInSecs.count() > SlowLinkOrCopyLimitInSecs) { LOG_WRN("Linking/copying files from " << sourceForLinkOrCopy << " to " << destinationForLinkOrCopy.toString() << " is taking too much time. Enabling verbose link/copy logging."); linkOrCopyVerboseLogging = true; } } assert(fpath[strlen(sourceForLinkOrCopy.c_str())] == '/'); const char *relativeOldPath = fpath + strlen(sourceForLinkOrCopy.c_str()) + 1; const Poco::Path newPath(destinationForLinkOrCopy, Poco::Path(relativeOldPath)); switch (typeflag) { case FTW_F: case FTW_SLN: Poco::File(newPath.parent()).createDirectories(); if (shouldLinkFile(relativeOldPath)) linkOrCopyFile(fpath, newPath.toString()); break; case FTW_D: { struct stat st; if (stat(fpath, &st) == -1) { LOG_SYS("nftw: stat(\"" << fpath << "\") failed"); return FTW_STOP; } if (!shouldCopyDir(relativeOldPath)) { LOG_TRC("nftw: Skipping redundant path: " << relativeOldPath); return FTW_SKIP_SUBTREE; } Poco::File(newPath).createDirectories(); struct utimbuf ut; ut.actime = st.st_atime; ut.modtime = st.st_mtime; if (utime(newPath.toString().c_str(), &ut) == -1) { LOG_SYS("nftw: utime(\"" << newPath.toString() << "\") failed"); return FTW_STOP; } } break; case FTW_SL: { const std::size_t size = sb->st_size; std::vector target(size + 1); char* target_data = target.data(); const ssize_t written = readlink(fpath, target_data, size); if (written <= 0 || static_cast(written) > size) { LOG_SYS("nftw: readlink(\"" << fpath << "\") failed"); Util::forcedExit(EX_SOFTWARE); } target_data[written] = '\0'; Poco::File(newPath.parent()).createDirectories(); if (symlink(target_data, newPath.toString().c_str()) == -1) { LOG_SYS("nftw: symlink(\"" << target_data << "\", \"" << newPath.toString() << "\") failed"); return FTW_STOP; } } break; case FTW_DNR: LOG_ERR("nftw: Cannot read directory '" << fpath << '\''); return FTW_STOP; case FTW_NS: LOG_ERR("nftw: stat failed for '" << fpath << '\''); return FTW_STOP; default: LOG_FTL("nftw: unexpected typeflag: '" << typeflag); assert(!"nftw: unexpected typeflag."); break; } return FTW_CONTINUE; } void linkOrCopy(std::string source, const Poco::Path& destination, const std::string& linkable, LinkOrCopyType type) { std::string resolved = FileUtil::realpath(source); if (resolved != source) { LOG_DBG("linkOrCopy: Using real path [" << resolved << "] instead of original link [" << source << "]."); source = std::move(resolved); } LOG_INF("linkOrCopy " << linkOrCopyTypeString(type) << " from [" << source << "] to [" << destination.toString() << "]."); linkOrCopyType = type; sourceForLinkOrCopy = source; if (sourceForLinkOrCopy.back() == '/') sourceForLinkOrCopy.pop_back(); destinationForLinkOrCopy = destination; linkableForLinkOrCopy = linkable; linkOrCopyFileCount = 0; linkOrCopyStartTime = std::chrono::steady_clock::now(); forceInitialCopy = detectSlowStackingFileSystem(destination.toString()); if (nftw(source.c_str(), linkOrCopyFunction, 10, FTW_ACTIONRETVAL|FTW_PHYS) == -1) { LOG_ERR("linkOrCopy: nftw() failed for '" << source << '\''); } if (linkOrCopyVerboseLogging) { linkOrCopyVerboseLogging = false; const auto ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - linkOrCopyStartTime).count(); const double seconds = (ms + 1) / 1000.; // At least 1ms to avoid div-by-zero. const auto rate = linkOrCopyFileCount / seconds; LOG_INF("Linking/Copying of " << linkOrCopyFileCount << " files from " << source << " to " << destinationForLinkOrCopy.toString() << " finished in " << seconds << " seconds, or " << rate << " files / second."); } } #if CODE_COVERAGE std::string childRootForGCDAFiles; std::string sourceForGCDAFiles; std::string destForGCDAFiles; int linkGCDAFilesFunction(const char* fpath, const struct stat*, int typeflag, struct FTW* /*ftwbuf*/) { if (strcmp(fpath, sourceForGCDAFiles.c_str()) == 0) { LOG_TRC("nftw: Skipping redundant path: " << fpath); return FTW_CONTINUE; } if (fpath.starts_with(childRootForGCDAFiles)) { LOG_TRC("nftw: Skipping childRoot subtree: " << fpath); return FTW_SKIP_SUBTREE; } assert(fpath[strlen(sourceForGCDAFiles.c_str())] == '/'); const char* relativeOldPath = fpath + strlen(sourceForGCDAFiles.c_str()) + 1; const Poco::Path newPath(destForGCDAFiles, Poco::Path(relativeOldPath)); switch (typeflag) { case FTW_F: case FTW_SLN: { const char* dot = strrchr(relativeOldPath, '.'); if (dot && !strcmp(dot, ".gcda")) { Poco::File(newPath.parent()).createDirectories(); if (link(fpath, newPath.toString().c_str()) != 0) { LOG_SYS("nftw: Failed to link [" << fpath << "] -> [" << newPath.toString() << ']'); } } } break; case FTW_D: case FTW_SL: break; case FTW_DNR: LOG_ERR("nftw: Cannot read directory '" << fpath << '\''); break; case FTW_NS: LOG_ERR("nftw: stat failed for '" << fpath << '\''); break; default: LOG_FTL("nftw: unexpected typeflag: '" << typeflag); assert(!"nftw: unexpected typeflag."); break; } return FTW_CONTINUE; } /// Link .gcda (gcov) files from the src directory into the jail. /// We need this so we can easily extract the profile data from within /// the jail. Otherwise, we lose coverage info of the kit process. void linkGCDAFiles(const std::string& destPath) { Poco::Path sourcePathInJail(destPath); const auto sourcePath = std::string(DEBUG_ABSSRCDIR); sourcePathInJail.append(sourcePath); Poco::File(sourcePathInJail).createDirectories(); LOG_INF("Linking .gcda files from " << sourcePath << " -> " << sourcePathInJail.toString()); const auto childRootPtr = std::getenv("BASE_CHILD_ROOT"); if (childRootPtr == nullptr || strlen(childRootPtr) == 0) { LOG_ERR("Cannot collect code-coverage stats for the Kit processes. BASE_CHILD_ROOT " "envar missing."); return; } // Trim the trailing /. const std::string childRoot = childRootPtr; const size_t last = childRoot.find_last_not_of('/'); if (last != std::string::npos) childRootForGCDAFiles = childRoot.substr(0, last + 1); else childRootForGCDAFiles = childRoot; sourceForGCDAFiles = sourcePath; destForGCDAFiles = sourcePathInJail.toString() + '/'; LOG_INF("nftw .gcda files from " << sourceForGCDAFiles << " -> " << destForGCDAFiles << " (" << childRootForGCDAFiles << ')'); if (nftw(sourcePath.c_str(), linkGCDAFilesFunction, 10, FTW_ACTIONRETVAL | FTW_PHYS) == -1) { LOG_ERR("linkGCDAFiles: nftw() failed for '" << sourcePath << '\''); } } #endif #ifndef __FreeBSD__ void dropCapability(cap_value_t capability) { cap_t caps; cap_value_t cap_list[] = { capability }; caps = cap_get_proc(); if (caps == nullptr) { LOG_SFL("cap_get_proc() failed"); Util::forcedExit(EX_SOFTWARE); } char *capText = cap_to_text(caps, nullptr); LOG_TRC("Capabilities first: " << capText); cap_free(capText); if (cap_set_flag(caps, CAP_EFFECTIVE, N_ELEMENTS(cap_list), cap_list, CAP_CLEAR) == -1 || cap_set_flag(caps, CAP_PERMITTED, N_ELEMENTS(cap_list), cap_list, CAP_CLEAR) == -1) { LOG_SFL("cap_set_flag() failed"); Util::forcedExit(EX_SOFTWARE); } if (cap_set_proc(caps) == -1) { LOG_SFL("cap_set_proc() failed"); Util::forcedExit(EX_SOFTWARE); } capText = cap_to_text(caps, nullptr); LOG_TRC("Capabilities now: " << capText); cap_free(capText); cap_free(caps); } #endif // __FreeBSD__ #endif // BUILDING_TESTS } // namespace Document::Document(const std::shared_ptr& loKit, const std::string& jailId, const std::string& docKey, const std::string& docId, const std::string& url, std::shared_ptr tileQueue, const std::shared_ptr& websocketHandler, unsigned mobileAppDocId) : _loKit(loKit), _jailId(jailId), _docKey(docKey), _docId(docId), _url(url), _obfuscatedFileId(Util::getFilenameFromURL(docKey)), _tileQueue(std::move(tileQueue)), _websocketHandler(websocketHandler), _isBgSaveProcess(false), _haveDocPassword(false), _isDocPasswordProtected(false), _docPasswordType(DocumentPasswordType::ToView), _stop(false), _deltaGen(new DeltaGenerator()), _editorId(-1), _editorChangeWarning(false), _lastMemTrimTime(std::chrono::steady_clock::now()), _mobileAppDocId(mobileAppDocId), _inputProcessingEnabled(true), _duringLoad(0) { LOG_INF("Document ctor for [" << _docKey << "] url [" << anonymizeUrl(_url) << "] on child [" << _jailId << "] and id [" << _docId << "]."); assert(_loKit); #if !MOBILEAPP assert(singletonDocument == nullptr); singletonDocument = this; #endif } Document::~Document() { LOG_INF("~Document dtor for [" << _docKey << "] url [" << anonymizeUrl(_url) << "] on child [" << _jailId << "] and id [" << _docId << "]. There are " << _sessions.size() << " views."); // Wait for the callback worker to finish. _stop = true; _tileQueue->put("eof"); for (const auto& session : _sessions) { session.second->resetDocManager(); } #ifdef IOS DocumentData::deallocate(_mobileAppDocId); #endif } /// Post the message - in the unipoll world we're in the right thread anyway bool Document::postMessage(const char* data, int size, const WSOpCode code) const { if (_isBgSaveProcess) { auto socket = _saveProcessParent.lock(); if (socket) { LOG_TRC("postMessage forwarding to parent of save process: " << getAbbreviatedMessage(data, size)); return socket->sendMessage(data, size, code, /*flush=*/true) > 0; } else LOG_TRC("Failed to forward to parent of save process: connection closed."); return false; } LOG_TRC("postMessage called with: " << getAbbreviatedMessage(data, size)); if (!_websocketHandler) { LOG_ERR("Child Doc: Bad socket while sending [" << getAbbreviatedMessage(data, size) << "]."); return false; } _websocketHandler->sendMessage(data, size, code, /*flush=*/true); return true; } bool Document::createSession(const std::string& sessionId) { #if defined(BUILDING_TESTS) LOG_ERR("createSession stubbed for tests for " << sessionId); return false; #else try { if (_sessions.find(sessionId) != _sessions.end()) { LOG_ERR("Session [" << sessionId << "] on url [" << anonymizeUrl(_url) << "] already exists."); return true; } LOG_INF("Creating " << (_sessions.empty() ? "first" : "new") << " session for url: " << anonymizeUrl(_url) << " for sessionId: " << sessionId << " on jailId: " << _jailId); auto session = std::make_shared( _websocketHandler, sessionId, _jailId, JailRoot, *this); _sessions.emplace(sessionId, session); _deltaGen->setSessionCount(_sessions.size()); const int viewId = session->getViewId(); _lastUpdatedAt[viewId] = std::chrono::steady_clock::now(); _speedCount[viewId] = 0; LOG_DBG("Have " << _sessions.size() << " active sessions after creating " << session->getId()); LOG_INF("New session [" << sessionId << "]"); updateActivityHeader(); return true; } catch (const std::exception& ex) { LOG_ERR("Exception while creating session [" << sessionId << "] on url [" << anonymizeUrl(_url) << "] - '" << ex.what() << "'."); return false; } #endif } std::size_t Document::purgeSessions() { std::vector> deadSessions; std::size_t num_sessions = 0; { // If there are no live sessions, we don't need to do anything at all and can just // bluntly exit, no need to clean up our own data structures. Also, there is a bug that // causes the deadSessions.clear() call below to crash in some situations when the last // session is being removed. for (auto it = _sessions.cbegin(); it != _sessions.cend(); ) { if (it->second->isCloseFrame()) { LOG_DBG("Removing session [" << it->second->getId() << ']'); deadSessions.push_back(it->second); it = _sessions.erase(it); } else { ++it; } } num_sessions = _sessions.size(); if (!Util::isMobileApp() && num_sessions == 0) { LOG_FTL("Document [" << anonymizeUrl(_url) << "] has no more views, exiting bluntly."); flushAndExit(EX_OK); } } if (deadSessions.size() > 0 ) LOG_TRC("Purging " << deadSessions.size() << " dead sessions, with " << num_sessions << " active sessions."); // Don't destroy sessions while holding our lock. // We may deadlock if a session is waiting on us // during callback initiated while handling a command // and the dtor tries to take its lock (which is taken). deadSessions.clear(); return num_sessions; } /// Set Document password for given URL void Document::setDocumentPassword(int passwordType) { // Log whether the document is password protected and a password is provided LOG_INF("setDocumentPassword: passwordProtected=" << _isDocPasswordProtected << " passwordProvided=" << _haveDocPassword); if (_isDocPasswordProtected && _haveDocPassword) { // it means this is the second attempt with the wrong password; abort the load operation _loKit->setDocumentPassword(_jailedUrl.c_str(), nullptr); return; } // One thing for sure, this is a password protected document _isDocPasswordProtected = true; if (passwordType == LOK_CALLBACK_DOCUMENT_PASSWORD) _docPasswordType = DocumentPasswordType::ToView; else if (passwordType == LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY) _docPasswordType = DocumentPasswordType::ToModify; LOG_INF("Calling _loKit->setDocumentPassword"); if (_haveDocPassword) _loKit->setDocumentPassword(_jailedUrl.c_str(), _docPassword.c_str()); else _loKit->setDocumentPassword(_jailedUrl.c_str(), nullptr); LOG_INF("setDocumentPassword returned."); } void Document::renderTiles(TileCombined &tileCombined) { // Find a session matching our view / render settings. const auto session = _sessions.findByCanonicalId(tileCombined.getNormalizedViewId()); if (!session) { LOG_ERR("Session is not found. Maybe exited after rendering request."); return; } if (!_loKitDocument) { LOG_ERR("Tile rendering requested before loading document."); return; } if (_loKitDocument->getViewsCount() <= 0) { LOG_ERR("Tile rendering requested without views."); return; } // if necessary select a suitable rendering view eg. with 'show non-printing chars' if (tileCombined.getNormalizedViewId()) _loKitDocument->setView(session->getViewId()); const auto blenderFunc = [&](unsigned char* data, int offsetX, int offsetY, std::size_t pixmapWidth, std::size_t pixmapHeight, int pixelWidth, int pixelHeight, LibreOfficeKitTileMode mode) { if (session->watermark()) session->watermark()->blending(data, offsetX, offsetY, pixmapWidth, pixmapHeight, pixelWidth, pixelHeight, mode); }; const auto postMessageFunc = [&](const char* buffer, std::size_t length) { postMessage(buffer, length, WSOpCode::Binary); }; if (!RenderTiles::doRender(_loKitDocument, *_deltaGen, tileCombined, _deltaPool, blenderFunc, postMessageFunc, _mobileAppDocId, session->getCanonicalViewId(), session->getDumpTiles())) { LOG_DBG("All tiles skipped, not producing empty tilecombine: message"); return; } } bool Document::sendFrame(const char* buffer, int length, WSOpCode opCode) { try { return postMessage(buffer, length, opCode); } catch (const Exception& exc) { LOG_ERR("Document::sendFrame: Exception: " << exc.displayText() << (exc.nested() ? "( " + exc.nested()->displayText() + ')' : "")); } return false; } void Document::trimIfInactive() { // Don't perturb memory un-necessarily if (_isBgSaveProcess) return; // FIXME: multi-document mobile optimization ? for (const auto& it : _sessions) { if (it.second->isActive()) { LOG_TRC("have active session, don't trim"); return; } } // TODO: be more clever - detect if we mutated the documen // recently, measure memory pressure etc. LOG_DBG("Sessions are all inactive - trim memory"); SigUtil::addActivity("trimIfInactive"); _loKit->trimMemory(4096); _deltaGen->dropCache(); } void Document::trimAfterInactivity() { // Don't perturb memory un-necessarily if (_isBgSaveProcess) return; if (std::chrono::duration_cast(std::chrono::steady_clock::now() - _lastMemTrimTime) < std::chrono::seconds(30)) { return; } LOG_TRC("Should we trim our caches ?"); double minInactivityMs = std::numeric_limits::max(); for (const auto& it : _sessions) { minInactivityMs = std::min(it.second->getInactivityMS(), minInactivityMs); } if (minInactivityMs >= 9999) { LOG_DBG("Trimming Core caches"); SigUtil::addActivity("trimAfterInactivity"); _loKit->trimMemory(1024); _lastMemTrimTime = std::chrono::steady_clock::now(); } } /* static */ void Document::GlobalCallback(const int type, const char* p, void* data) { if (SigUtil::getTerminationFlag()) return; // unusual LOK event from another thread, // pData - is Document with process' lifetime. if (pushToMainThread(GlobalCallback, type, p, data)) return; const std::string payload = p ? p : "(nil)"; Document* self = static_cast(data); if (type == LOK_CALLBACK_PROFILE_FRAME) { // We must send the trace data to the WSD process for output LOG_TRC("Document::GlobalCallback " << lokCallbackTypeToString(type) << ": " << payload.length() << " bytes."); self->sendTextFrame("traceevent: \n" + payload); return; } LOG_TRC("Document::GlobalCallback " << lokCallbackTypeToString(type) << " [" << payload << "]."); if (type == LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY || type == LOK_CALLBACK_DOCUMENT_PASSWORD) { // Mark the document password type. self->setDocumentPassword(type); return; } else if (type == LOK_CALLBACK_STATUS_INDICATOR_START || type == LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE || type == LOK_CALLBACK_STATUS_INDICATOR_FINISH) { for (auto& it : self->_sessions) { std::shared_ptr session = it.second; if (!session->isCloseFrame()) session->loKitCallback(type, payload); } return; } else if (type == LOK_CALLBACK_JSDIALOG || type == LOK_CALLBACK_HYPERLINK_CLICKED) { if (self->_sessions.size() == 1) { auto it = self->_sessions.begin(); std::shared_ptr session = it->second; if (session && !session->isCloseFrame()) { session->loKitCallback(type, payload); // TODO. It should filter some messages // before loading the document session->getProtocol()->enableProcessInput(true); return; } } } // Broadcast leftover status indicator callbacks to all clients self->broadcastCallbackToClients(type, payload); } /* static */ void Document::ViewCallback(const int type, const char* p, void* data) { if (SigUtil::getTerminationFlag()) return; // unusual LOK event from another thread. // pData - is CallbackDescriptors which share process' lifetime. if (pushToMainThread(ViewCallback, type, p, data)) return; CallbackDescriptor* descriptor = static_cast(data); assert(descriptor && "Null callback data."); assert(descriptor->getDoc() && "Null Document instance."); std::shared_ptr tileQueue = descriptor->getDoc()->getTileQueue(); assert(tileQueue && "Null TileQueue."); const std::string payload = p ? p : "(nil)"; LOG_TRC("Document::ViewCallback [" << descriptor->getViewId() << "] [" << lokCallbackTypeToString(type) << "] [" << payload << "]."); // when we examine the content of the JSON std::string targetViewId; if (type == LOK_CALLBACK_CELL_CURSOR) { StringVector tokens(StringVector::tokenize(payload, ',')); // Payload may be 'EMPTY'. if (tokens.size() == 4) { int cursorX = std::stoi(tokens[0]); int cursorY = std::stoi(tokens[1]); int cursorWidth = std::stoi(tokens[2]); int cursorHeight = std::stoi(tokens[3]); tileQueue->updateCursorPosition(0, 0, cursorX, cursorY, cursorWidth, cursorHeight); } } else if (type == LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR) { Poco::JSON::Parser parser; const Poco::Dynamic::Var result = parser.parse(payload); const auto& command = result.extract(); std::string rectangle = command->get("rectangle").toString(); StringVector tokens(StringVector::tokenize(rectangle, ',')); // Payload may be 'EMPTY'. if (tokens.size() == 4) { int cursorX = std::stoi(tokens[0]); int cursorY = std::stoi(tokens[1]); int cursorWidth = std::stoi(tokens[2]); int cursorHeight = std::stoi(tokens[3]); tileQueue->updateCursorPosition(0, 0, cursorX, cursorY, cursorWidth, cursorHeight); } } else if (type == LOK_CALLBACK_INVALIDATE_VIEW_CURSOR || type == LOK_CALLBACK_CELL_VIEW_CURSOR) { Poco::JSON::Parser parser; const Poco::Dynamic::Var result = parser.parse(payload); const auto& command = result.extract(); targetViewId = command->get("viewId").toString(); std::string part = command->get("part").toString(); std::string text = command->get("rectangle").toString(); StringVector tokens(StringVector::tokenize(text, ',')); // Payload may be 'EMPTY'. if (tokens.size() == 4) { int cursorX = std::stoi(tokens[0]); int cursorY = std::stoi(tokens[1]); int cursorWidth = std::stoi(tokens[2]); int cursorHeight = std::stoi(tokens[3]); tileQueue->updateCursorPosition(std::stoi(targetViewId), std::stoi(part), cursorX, cursorY, cursorWidth, cursorHeight); } } else if (type == LOK_CALLBACK_DOCUMENT_PASSWORD_RESET) { Document* document = dynamic_cast(descriptor->getDoc()); Poco::JSON::Object::Ptr object; if (document && JsonUtil::parseJSON(payload, object)) { std::string password = JsonUtil::getJSONValue(object, "password"); bool isToModify = JsonUtil::getJSONValue(object, "isToModify"); document->_isDocPasswordProtected = !password.empty(); document->_haveDocPassword = document->_isDocPasswordProtected; document->_docPassword = password; document->_docPasswordType = isToModify ? DocumentPasswordType::ToModify : DocumentPasswordType::ToView; } return; } else if (type == LOK_CALLBACK_VIEW_RENDER_STATE) { Document* document = dynamic_cast(descriptor->getDoc()); if (document) { std::shared_ptr session = document->findSessionByViewId(descriptor->getViewId()); if (session) { session->setViewRenderState(payload); document->invalidateCanonicalId(session->getId()); } else { LOG_ERR("Cannot find session for viewId: " << descriptor->getViewId()); } } else { // This shouldn't happen, but for consistency. LOG_ERR("Failed to downcast DocumentManagerInterface to Document"); } return; } // merge various callback types together if possible if (type == LOK_CALLBACK_INVALIDATE_TILES) { // all views have to be in sync tileQueue->put("callback all " + std::to_string(type) + ' ' + payload); } else tileQueue->put("callback " + std::to_string(descriptor->getViewId()) + ' ' + std::to_string(type) + ' ' + payload); LOG_TRC("Document::ViewCallback end."); } /// Load a document (or view) and register callbacks. bool Document::onLoad(const std::string& sessionId, const std::string& uriAnonym, const std::string& renderOpts) { LOG_INF("Loading url [" << uriAnonym << "] for session [" << sessionId << "] which has " << (_sessions.size() - 1) << " sessions."); _duringLoad++; // wouldn't it be nice to have a custom destructor to do this. std::unique_ptr> guard(nullptr, [&](void*) { _duringLoad--; }); // This shouldn't happen, but for sanity. const auto it = _sessions.find(sessionId); if (it == _sessions.end() || !it->second) { LOG_ERR("Cannot find session [" << sessionId << "] to load view for."); return false; } std::shared_ptr session = it->second; try { if (!load(session, renderOpts)) { return false; } } catch (const std::exception &exc) { LOG_ERR("Exception while loading url [" << uriAnonym << "] for session [" << sessionId << "]: " << exc.what()); session->sendTextFrameAndLogError("error: cmd=load kind=faileddocloading"); return false; } return true; } void Document::onUnload(const ChildSession& session) { const auto& sessionId = session.getId(); LOG_INF("Unloading session [" << sessionId << "] on url [" << anonymizeUrl(_url) << "]."); const int viewId = session.getViewId(); _tileQueue->removeCursorPosition(viewId); if (_loKitDocument == nullptr) { LOG_ERR("Unloading session [" << sessionId << "] without loKitDocument."); return; } _loKitDocument->setView(viewId); _loKitDocument->registerCallback(nullptr, nullptr); _loKit->registerCallback(nullptr, nullptr); int viewCount = _loKitDocument->getViewsCount(); if (viewCount == 1) { if (!Util::isMobileApp() && _sessions.empty()) { LOG_INF("Document [" << anonymizeUrl(_url) << "] has no more views, exiting bluntly."); flushAndExit(EX_OK); } LOG_INF("Document [" << anonymizeUrl(_url) << "] has no more views, but has " << _sessions.size() << " sessions still. Destroying the document."); #ifdef __ANDROID__ _loKitDocumentForAndroidOnly.reset(); #endif _loKitDocument.reset(); LOG_INF("Document [" << anonymizeUrl(_url) << "] session [" << sessionId << "] unloaded Document."); return; } else { _loKitDocument->destroyView(viewId); } // Since callback messages are processed on idle-timer, // we could receive callbacks after destroying a view. // Retain the CallbackDescriptor object, which is shared with Core. // Do not: _viewIdToCallbackDescr.erase(viewId); viewCount = _loKitDocument->getViewsCount(); LOG_INF("Document [" << anonymizeUrl(_url) << "] session [" << sessionId << "] unloaded view [" << viewId << "]. Have " << viewCount << " view" << (viewCount != 1 ? "s." : ".")); if (viewCount > 0) { // Broadcast updated view info notifyViewInfo(); } } void Document::updateActivityHeader() const { // pre-prepare and set details in case of a signal later std::stringstream ss; ss << "Session count: " << _sessions.size() << "\n"; for (const auto& it : _sessions) ss << "\t" << it.second->getActivityState() << "\n"; ss << "Commands:\n"; SigUtil::setActivityHeader(ss.str()); } bool Document::joinThreads() { if (!getLOKit()->joinThreads()) return false; if (SocketPoll::PollWatchdog) SocketPoll::PollWatchdog->joinThread(); _deltaPool.stop(); return true; } // Most threads are opportunisticaly created but some need to be started void Document::startThreads() { if (SocketPoll::PollWatchdog) SocketPoll::PollWatchdog->startThread(); } void Document::handleSaveMessage(const std::string &) { LOG_TRC("Check save message"); // if a bgsave process - now we can clean up. if (_isBgSaveProcess) { auto socket = _saveProcessParent.lock(); if (socket) { LOG_TRC("Shutting down bgsv child's socket to parent kit post save"); // We don't want to wait around for the parent's websocket socket->shutdownAfterWriting(); } else LOG_TRC("Shutting down already shutdown bgsv child's socket to parent kit post save"); // any further messages are not interesting. if (_tileQueue) _tileQueue->clear(); // cleanup any lingering file-system pieces _loKitDocument.reset(); // Next step in the chain is BgSaveChildWebSocketHandler::onDisconnect } } // need to hold a reference on session in case it exits during async save bool Document::forkToSave(const std::function &childSave, int viewId) { #if MOBILEAPP return false; #else // !MOBILEAPP if (!joinThreads()) { LOG_WRN("Failed to join threads before async save"); return false; } size_t threads = getCurrentThreadCount(); if (threads != 1) { LOG_WRN("Failed to ensure we have just one, we have: " << threads); return false; } #if 0 // TODO: compare FD count in a normal process with how // many we see open now. int expectFds = 2 // SocketPoll wakeups + 1; // socket to coolwsd int actualFds = fdCounter->count(); if (actualFds != expectFds) { LOG_WRN("Can't background save: " << actualFds << " fds open; expect " << expectFds); return false; } #endif const auto start = std::chrono::steady_clock::now(); // TODO: close URPtoLoFDs and URPfromLoFDs and test if (isURPEnabled()) { LOG_WRN("Can't background save with URP enabled"); return false; } // FIXME: only do one of these at a time ... // FIXME: defer and queue a 2nd save if queued during save ... std::shared_ptr parentSocket, childSocket; if (!StreamSocket::socketpair(parentSocket, childSocket)) return false; // To encode into the child process id for debugging static size_t numSaves = 0; numSaves++; const pid_t pid = fork(); if (!pid) // Child { // sort out thread local variables to get logging right from // as early as possible. Util::setThreadName("kitbgsv_" + Util::encodeId(_mobileAppDocId, 3) + "_" + Util::encodeId(numSaves, 3)); _isBgSaveProcess = true; SigUtil::addActivity("forked background save process: " + std::to_string(pid)); childSocket.reset(); // now we just have a single socket to our parent // Hard drop our previous connections to coolwsd and shared wakeups.x KitSocketPoll::cleanupChildProcess(); // close duplicate kit->wsd socket auto kitWs = std::static_pointer_cast(_websocketHandler); kitWs->shutdownForBackgroundSave(); // now send messages to the parent instead of the kit. auto parentWs = std::make_shared("bgsv_child_ws"); parentSocket->setHandler(parentWs); parentSocket->setWebSocket(); // avoid http upgrade. _saveProcessParent = parentWs; // hand parentSocket to the main poll KitSocketPoll::getMainPoll()->insertNewSocket(parentSocket); parentWs.reset(); getLOKit()->setForkedChild(true); const auto now = std::chrono::steady_clock::now(); LOG_TRC("Background save process " << getpid() << " fork took " << std::chrono::duration_cast(now - start).count() << "ms"); childSave(); SigUtil::addActivity("background save process shutdown"); // Wait now for an async save result from the core, // and head to handleSaveMessage } else // Still us { LOG_TRC("Spawned process " << pid << " to do background save"); parentSocket.reset(); // now we have a socket to the child: childSocket auto bgSaveChild = std::make_shared( "bgsv_kit_ws", pid, shared_from_this(), findSessionByViewId(viewId)); childSocket->setHandler(bgSaveChild); childSocket->setWebSocket(); // avoid http upgrade. KitSocketPoll::getMainPoll()->insertNewSocket(childSocket); getLOKit()->setForkedChild(false); startThreads(); } return true; #endif // !MOBILEAPP } void Document::notifyViewInfo() { // Get the list of view ids from the core const int viewCount = getLOKitDocument()->getViewsCount(); std::vector viewIds(viewCount); getLOKitDocument()->getViewIds(viewIds.data(), viewCount); const std::map viewInfoMap = getViewInfo(); const std::map viewColorsMap = getViewColors(); // Double check if list of viewids from core and our list matches, // and create an array of JSON objects containing id and username std::map viewStrings; // viewId -> public data string for (const auto& viewId : viewIds) { std::ostringstream oss; oss << "\"id\":" << viewId << ','; int color = 0; const auto itView = viewInfoMap.find(viewId); if (itView == viewInfoMap.end()) { LOG_ERR("No username found for viewId [" << viewId << "]."); oss << "\"username\":\"Unknown\","; } else { oss << "\"userid\":\"" << JsonUtil::escapeJSONValue(itView->second.getUserId()) << "\","; const std::string username = itView->second.getUserName(); oss << "\"username\":\"" << JsonUtil::escapeJSONValue(username) << "\","; if (!itView->second.getUserExtraInfo().empty()) oss << "\"userextrainfo\":" << itView->second.getUserExtraInfo() << ','; const bool readonly = itView->second.isReadOnly(); oss << "\"readonly\":\"" << readonly << "\","; const auto it = viewColorsMap.find(username); if (it != viewColorsMap.end()) { color = it->second; } } oss << "\"color\":" << color; viewStrings[viewId] = oss.str(); } // Broadcast updated viewinfo to all clients. Every view gets own userprivateinfo. for (const auto& it : _sessions) { std::ostringstream oss; oss << "viewinfo: ["; for (const auto& viewId : viewIds) { if (viewId == it.second->getViewId() && !it.second->getUserPrivateInfo().empty()) { oss << "{" << viewStrings[viewId]; oss << ",\"userprivateinfo\":" << it.second->getUserPrivateInfo(); oss << "},"; } else oss << "{" << viewStrings[viewId] << "},"; } if (viewCount > 0) oss.seekp(-1, std::ios_base::cur); // Remove last comma. oss << ']'; it.second->sendTextFrame(oss.str()); } } std::shared_ptr Document::findSessionByViewId(int viewId) { for (const auto& it : _sessions) { if (it.second->getViewId() == viewId) return it.second; } return nullptr; } void Document::invalidateCanonicalId(const std::string& sessionId) { auto it = _sessions.find(sessionId); if (it == _sessions.end()) { LOG_ERR("Session [" << sessionId << "] not found"); return; } std::shared_ptr session = it->second; int newCanonicalId = _sessions.createCanonicalId(getViewProps(session)); if (newCanonicalId == session->getCanonicalViewId()) return; session->setCanonicalViewId(newCanonicalId); const std::string viewRenderedState = session->getViewRenderState(); std::string stateName; if (!viewRenderedState.empty()) { stateName = viewRenderedState; } else { stateName = "Empty"; } std::string message = "canonicalidchange: viewid=" + std::to_string(session->getViewId()) + " canonicalid=" + std::to_string(newCanonicalId) + " viewrenderedstate=" + stateName; session->sendTextFrame(message); } std::string Document::getViewProps(const std::shared_ptr& session) { return session->getWatermarkText() + "|" + session->getViewRenderState(); } void Document::updateEditorSpeeds(int id, int speed) { int maxSpeed = -1, fastestUser = -1; auto now = std::chrono::steady_clock::now(); _lastUpdatedAt[id] = now; _speedCount[id] = speed; for (const auto& it : _sessions) { const std::shared_ptr session = it.second; int sessionId = session->getViewId(); auto duration = (_lastUpdatedAt[id] - now); std::chrono::milliseconds::rep durationInMs = std::chrono::duration_cast(duration).count(); if (_speedCount[sessionId] != 0 && durationInMs > 5000) { _speedCount[sessionId] = session->getSpeed(); _lastUpdatedAt[sessionId] = now; } if (_speedCount[sessionId] > maxSpeed) { maxSpeed = _speedCount[sessionId]; fastestUser = sessionId; } } // 0 for preventing selection of the first always // 1 for preventing new users from directly becoming editors if (_editorId != fastestUser && (maxSpeed != 0 && maxSpeed != 1)) { if (!_editorChangeWarning && _editorId != -1) { _editorChangeWarning = true; } else { _editorChangeWarning = false; _editorId = fastestUser; for (const auto& it : _sessions) it.second->sendTextFrame("editor: " + std::to_string(_editorId)); } } else _editorChangeWarning = false; } // Get the color value for all author names from the core std::map Document::getViewColors() { char* values = _loKitDocument->getCommandValues(".uno:TrackedChangeAuthors"); const std::string colorValues = std::string(values == nullptr ? "" : values); std::free(values); std::map viewColors; try { if (!colorValues.empty()) { Poco::JSON::Parser parser; Poco::JSON::Object::Ptr root = parser.parse(colorValues).extract(); if (root->get("authors").type() == typeid(Poco::JSON::Array::Ptr)) { Poco::JSON::Array::Ptr authorsArray = root->get("authors").extract(); for (auto& authorVar: *authorsArray) { Poco::JSON::Object::Ptr authorObj = authorVar.extract(); std::string authorName = authorObj->get("name").convert(); int colorValue = authorObj->get("color").convert(); viewColors[authorName] = colorValue; } } } } catch(const Exception& exc) { LOG_ERR("Poco Exception: " << exc.displayText() << (exc.nested() ? " (" + exc.nested()->displayText() + ')' : "")); } return viewColors; } std::string Document::getDefaultTheme(const std::shared_ptr& session) const { bool darkTheme; switch (_loKitDocument->getDocumentType()) { case LOK_DOCTYPE_TEXT: darkTheme = session->getTextDarkTheme() == "true"; break; case LOK_DOCTYPE_SPREADSHEET: darkTheme = session->getSpreadsheetDarkTheme() == "true"; break; case LOK_DOCTYPE_PRESENTATION: darkTheme = session->getPresentationDarkTheme() == "true"; break; case LOK_DOCTYPE_DRAWING: darkTheme = session->getDrawingDarkTheme() == "true"; break; default: darkTheme = false; break; } return darkTheme ? "Dark" : "Light"; } std::shared_ptr Document::load(const std::shared_ptr& session, const std::string& renderOpts) { const std::string sessionId = session->getId(); const std::string& uri = session->getJailedFilePath(); const std::string& uriAnonym = session->getJailedFilePathAnonym(); const std::string& userName = session->getUserName(); const std::string& userNameAnonym = session->getUserNameAnonym(); const std::string& docPassword = session->getDocPassword(); const bool haveDocPassword = session->getHaveDocPassword(); const std::string& lang = session->getLang(); const std::string& deviceFormFactor = session->getDeviceFormFactor(); const std::string& batchMode = session->getBatchMode(); const std::string& enableMacrosExecution = session->getEnableMacrosExecution(); const std::string& macroSecurityLevel = session->getMacroSecurityLevel(); const bool accessibilityState = session->getAccessibilityState(); const std::string& userTimezone = session->getTimezone(); if (!Util::isMobileApp()) consistencyCheckFileExists(uri); std::string options; if (!lang.empty()) options = "Language=" + lang; if (!deviceFormFactor.empty()) options += ",DeviceFormFactor=" + deviceFormFactor; if (!batchMode.empty()) options += ",Batch=" + batchMode; if (!enableMacrosExecution.empty()) options += ",EnableMacrosExecution=" + enableMacrosExecution; if (!macroSecurityLevel.empty()) options += ",MacroSecurityLevel=" + macroSecurityLevel; if (!userTimezone.empty()) options += ",Timezone=" + userTimezone; std::string spellOnline; if (!_loKitDocument) { // This is the first time we are loading the document LOG_INF("Loading new document from URI: [" << uriAnonym << "] for session [" << sessionId << "]."); _loKit->registerCallback(GlobalCallback, this); const int flags = LOK_FEATURE_DOCUMENT_PASSWORD | LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY | LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK | LOK_FEATURE_NO_TILED_ANNOTATIONS | LOK_FEATURE_RANGE_HEADERS | LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK; _loKit->setOptionalFeatures(flags); // Save the provided password with us and the jailed url _haveDocPassword = haveDocPassword; _docPassword = docPassword; _jailedUrl = uri; _isDocPasswordProtected = false; const char *pURL = uri.c_str(); LOG_DBG("Calling lokit::documentLoad(" << FileUtil::anonymizeUrl(pURL) << ", \"" << options << "\")."); const auto start = std::chrono::steady_clock::now(); _loKitDocument.reset(_loKit->documentLoad(pURL, options.c_str())); #ifdef __ANDROID__ _loKitDocumentForAndroidOnly = _loKitDocument; #endif const auto duration = std::chrono::steady_clock::now() - start; const auto elapsed = std::chrono::duration_cast(duration); LOG_DBG("Returned lokit::documentLoad(" << FileUtil::anonymizeUrl(pURL) << ") in " << elapsed); #ifdef IOS DocumentData::get(_mobileAppDocId).loKitDocument = _loKitDocument.get(); #endif if (!_loKitDocument || !_loKitDocument->get()) { LOG_ERR("Failed to load: " << uriAnonym << ", error: " << _loKit->getError()); // Checking if wrong password or no password was reason for failure. if (_isDocPasswordProtected) { LOG_INF("Document [" << uriAnonym << "] is password protected."); if (!_haveDocPassword) { LOG_INF("No password provided for password-protected document [" << uriAnonym << "]."); std::string passwordFrame = "passwordrequired:"; if (_docPasswordType == DocumentPasswordType::ToView) passwordFrame += "to-view"; else if (_docPasswordType == DocumentPasswordType::ToModify) passwordFrame += "to-modify"; session->sendTextFrameAndLogError("error: cmd=load kind=" + passwordFrame); } else { LOG_INF("Wrong password for password-protected document [" << uriAnonym << "]."); session->sendTextFrameAndLogError("error: cmd=load kind=wrongpassword"); } return nullptr; } session->sendTextFrameAndLogError("error: cmd=load kind=faileddocloading"); session->shutdownNormal(); LOG_FTL("Failed to load the document. Setting TerminationFlag"); SigUtil::setTerminationFlag(); return nullptr; } // Only save the options on opening the document. // No support for changing them after opening a document. _renderOpts = renderOpts; spellOnline = session->getSpellOnline(); } else { LOG_INF("Document with url [" << uriAnonym << "] already loaded. Need to create new view for session [" << sessionId << "]."); // Check if this document requires password if (_isDocPasswordProtected) { if (!haveDocPassword) { std::string passwordFrame = "passwordrequired:"; if (_docPasswordType == DocumentPasswordType::ToView) passwordFrame += "to-view"; else if (_docPasswordType == DocumentPasswordType::ToModify) passwordFrame += "to-modify"; session->sendTextFrameAndLogError("error: cmd=load kind=" + passwordFrame); return nullptr; } else if (docPassword != _docPassword) { session->sendTextFrameAndLogError("error: cmd=load kind=wrongpassword"); return nullptr; } } LOG_INF("Creating view to url [" << uriAnonym << "] for session [" << sessionId << "] with " << options << '.'); _loKitDocument->createView(options.c_str()); LOG_TRC("View to url [" << uriAnonym << "] created."); } std::string theme = getDefaultTheme(session); LOG_INF("Initializing for rendering session [" << sessionId << "] on document url [" << anonymizeUrl(_url) << "] with: [" << makeRenderParams(_renderOpts, userNameAnonym, spellOnline, theme) << "]."); // initializeForRendering() should be called before // registerCallback(), as the previous creates a new view in Impress. const std::string renderParams = makeRenderParams(_renderOpts, userName, spellOnline, theme); _loKitDocument->initializeForRendering(renderParams.c_str()); const int viewId = _loKitDocument->getView(); session->setViewId(viewId); _sessionUserInfo[viewId] = UserInfo(session->getViewUserId(), session->getViewUserName(), session->getViewUserExtraInfo(), session->getViewUserPrivateInfo(), session->isReadOnly()); _loKitDocument->setViewLanguage(viewId, lang.c_str()); _loKitDocument->setViewTimezone(viewId, userTimezone.c_str()); _loKitDocument->setAccessibilityState(viewId, accessibilityState); if (session->isReadOnly()) { _loKitDocument->setViewReadOnly(viewId, true); if (session->isAllowChangeComments()) { _loKitDocument->setAllowChangeComments(viewId, true); } } // viewId's monotonically increase, and CallbackDescriptors are never freed. _viewIdToCallbackDescr.emplace(viewId, std::unique_ptr(new CallbackDescriptor({ this, viewId }))); _loKitDocument->registerCallback(ViewCallback, _viewIdToCallbackDescr[viewId].get()); const int viewCount = _loKitDocument->getViewsCount(); LOG_INF("Document url [" << anonymizeUrl(_url) << "] for session [" << sessionId << "] loaded view [" << viewId << "]. Have " << viewCount << " view" << (viewCount != 1 ? "s." : ".")); session->initWatermark(); if (char* viewRenderState = _loKitDocument->getCommandValues(".uno:ViewRenderState")) { session->setViewRenderState(viewRenderState); free(viewRenderState); } invalidateCanonicalId(session->getId()); return _loKitDocument; } bool Document::forwardToChild(const std::string& prefix, const std::vector& payload) { assert(payload.size() > prefix.size()); // Remove the prefix and trim. std::size_t index = prefix.size(); for ( ; index < payload.size(); ++index) { if (payload[index] != ' ') { break; } } const char* data = payload.data() + index; std::size_t size = payload.size() - index; std::string name; std::string sessionId; if (COOLProtocol::parseNameValuePair(prefix, name, sessionId, '-') && name == "child") { const auto it = _sessions.find(sessionId); if (it != _sessions.end()) { std::shared_ptr session = it->second; static const std::string disconnect("disconnect"); if (size == disconnect.size() && strncmp(data, disconnect.data(), disconnect.size()) == 0) { if(session->getViewId() == _editorId) { _editorId = -1; } LOG_DBG("Removing ChildSession [" << sessionId << "]."); // Tell them we're going quietly. session->sendTextFrame("disconnected:"); _sessions.erase(it); const std::size_t count = _sessions.size(); LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") << " after removing ChildSession [" << sessionId << "]."); _deltaGen->setSessionCount(count); // No longer needed, and allow session dtor to take it. session.reset(); return true; } // No longer needed, and allow the handler to take it. if (session) { std::vector vect(size); vect.assign(data, data + size); // TODO this is probably wrong... session->handleMessage(vect); return true; } } std::string abbrMessage; #ifndef BUILDING_TESTS if (AnonymizeUserData) { abbrMessage = "..."; } else #endif { abbrMessage = getAbbreviatedMessage(data, size); } LOG_ERR("Child session [" << sessionId << "] not found to forward message: " << abbrMessage); } else { LOG_ERR("Failed to parse prefix of forward-to-child message: " << prefix); } return false; } namespace { template Object::Ptr makePropertyValue(const std::string& type, const T& val) { Object::Ptr obj = new Object(); obj->set("type", type); obj->set("value", val); return obj; } } /* static */ std::string Document::makeRenderParams(const std::string& renderOpts, const std::string& userName, const std::string& spellOnline, const std::string& theme) { Object::Ptr renderOptsObj; // Fill the object with renderoptions, if any if (!renderOpts.empty()) { Parser parser; Poco::Dynamic::Var var = parser.parse(renderOpts); renderOptsObj = var.extract(); } else { renderOptsObj = new Object(); } // Append name of the user, if any, who opened the document to rendering options if (!userName.empty()) { // userName must be decoded already. renderOptsObj->set(".uno:Author", makePropertyValue("string", userName)); } // By default we enable spell-checking, unless it's disabled explicitly. if (!spellOnline.empty()) { const bool bSet = (spellOnline != "false"); renderOptsObj->set(".uno:SpellOnline", makePropertyValue("boolean", bSet)); } if (!theme.empty()) renderOptsObj->set(".uno:ChangeTheme", makePropertyValue("string", theme)); if (renderOptsObj) { std::ostringstream ossRenderOpts; renderOptsObj->stringify(ossRenderOpts); return ossRenderOpts.str(); } return std::string(); } bool Document::isTileRequestInsideVisibleArea(const TileCombined& tileCombined) { const auto session = _sessions.findByCanonicalId(tileCombined.getNormalizedViewId()); if (!session) return false; for (const auto& rTile : tileCombined.getTiles()) { if (session->isTileInsideVisibleArea(rTile)) return true; } return false; } // poll is idle, are we ? void Document::checkIdle() { if (!processInputEnabled() || hasQueueItems()) { LOG_TRC("Nearly idle - but have more queued items to process"); return; // more to do } sendTextFrame("idle"); // get rid of idle check for now. ProcessToIdleDeadline = std::chrono::steady_clock::now() - std::chrono::milliseconds(10); } void Document::drainQueue() { try { std::vector tileRequests; while (processInputEnabled() && hasQueueItems()) { if (_stop || SigUtil::getTerminationFlag()) { LOG_INF("_stop or TerminationFlag is set, breaking Document::drainQueue of loop"); tileRequests.clear(); _deltaPool.stop(); break; } const TileQueue::Payload input = _tileQueue->pop(); LOG_TRC("Kit handling queue message: " << COOLProtocol::getAbbreviatedMessage(input)); const StringVector tokens = StringVector::tokenize(input.data(), input.size()); if (tokens.equals(0, "eof")) { LOG_INF("Received EOF. Finishing."); break; } if (tokens.equals(0, "tile")) { tileRequests.emplace_back(TileDesc::parse(tokens)); } else if (tokens.equals(0, "tilecombine")) { tileRequests.emplace_back(TileCombined::parse(tokens)); } else if (tokens.startsWith(0, "child-")) { forwardToChild(tokens[0], input); } else if (tokens.equals(0, "processtoidle")) { ProcessToIdleDeadline = std::chrono::steady_clock::now(); uint32_t timeoutUs = 0; if (tokens.getUInt32(1, "timeout", timeoutUs)) ProcessToIdleDeadline += std::chrono::microseconds(timeoutUs); } else if (tokens.equals(0, "callback")) { if (tokens.size() >= 3) { bool broadcast = false; int viewId = -1; const std::string& target = tokens[1]; if (target == "all") { broadcast = true; } else { viewId = std::stoi(target); } const int type = std::stoi(tokens[2]); // payload is the rest of the message const std::size_t offset = tokens[0].length() + tokens[1].length() + tokens[2].length() + 3; // + delims const std::string payload(input.data() + offset, input.size() - offset); // Forward the callback to the same view, demultiplexing is done by the LibreOffice core. bool isFound = false; for (const auto& it : _sessions) { ChildSession& session = *it.second; if (broadcast || (!broadcast && (session.getViewId() == viewId))) { if (!session.isCloseFrame()) { isFound = true; session.loKitCallback(type, payload); } else { LOG_ERR("Session-thread of session [" << session.getId() << "] for view [" << viewId << "] is not running. Dropping [" << lokCallbackTypeToString(type) << "] payload [" << payload << ']'); } if (!broadcast) { break; } } } if (!isFound) { LOG_ERR("Document::ViewCallback. Session [" << viewId << "] is no longer active to process [" << lokCallbackTypeToString(type) << "] [" << payload << "] message to Master Session."); } } else { LOG_ERR("Invalid callback message: [" << COOLProtocol::getAbbreviatedMessage(input) << "]."); } } else { LOG_ERR("Unexpected request: [" << COOLProtocol::getAbbreviatedMessage(input) << "]."); } } if (!tileRequests.empty()) { // Put requests that include tiles in the visible area to the front to handle those first std::partition(tileRequests.begin(), tileRequests.end(), [this](const TileCombined& req) { return isTileRequestInsideVisibleArea(req); }); for (auto& tileCombined : tileRequests) renderTiles(tileCombined); } } catch (const std::exception& exc) { LOG_FTL("drainQueue: Exception: " << exc.what()); if (!Util::isMobileApp()) flushAndExit(EX_SOFTWARE); } catch (...) { LOG_FTL("drainQueue: Unknown exception"); if (!Util::isMobileApp()) flushAndExit(EX_SOFTWARE); } } /// Return access to the lok::Document instance. std::shared_ptr Document::getLOKitDocument() { if (!_loKitDocument) { LOG_ERR("Document [" << _docKey << "] is not loaded."); throw std::runtime_error("Document " + _docKey + " is not loaded."); } return _loKitDocument; } /// Stops theads, flushes buffers, and exits the process. void Document::flushAndExit(int code) { flushTraceEventRecordings(); _deltaPool.stop(); if (!Util::isKitInProcess()) Util::forcedExit(code); else SigUtil::setTerminationFlag(); } void Document::dumpState(std::ostream& oss) { oss << "Kit Document:\n" << std::boolalpha << "\n\tpid: " << getpid() << "\n\tstop: " << _stop << "\n\tjailId: " << _jailId << "\n\tdocKey: " << _docKey << "\n\tdocId: " << _docId << "\n\turl: " << _url << "\n\tobfuscatedFileId: " << _obfuscatedFileId << "\n\tjailedUrl: " << _jailedUrl << "\n\trenderOpts: " << _renderOpts << "\n\thaveDocPassword: " << _haveDocPassword // not the pwd itself << "\n\tisDocPasswordProtected: " << _isDocPasswordProtected << "\n\tdocPasswordType: " << (int)_docPasswordType << "\n\teditorId: " << _editorId << "\n\teditorChangeWarning: " << _editorChangeWarning << "\n\tmobileAppDocId: " << _mobileAppDocId << "\n\tinputProcessingEnabled: " << _inputProcessingEnabled << "\n\tduringLoad: " << _duringLoad << "\n"; // dumpState: // TODO: _websocketHandler - but this is an odd one. _tileQueue->dumpState(oss); oss << "\tviewIdToCallbackDescr:"; for (const auto &it : _viewIdToCallbackDescr) { oss << "\n\t\tviewId: " << it.first << " editorId: " << it.second->getDoc()->getEditorId() << " mobileAppDocId: " << it.second->getDoc()->getMobileAppDocId(); } oss << "\n"; _deltaPool.dumpState(oss); _sessions.dumpState(oss); _deltaGen->dumpState(oss); oss << "\tlastUpdatedAt:"; for (const auto &it : _lastUpdatedAt) { auto ms = std::chrono::duration_cast( it.second.time_since_epoch()).count(); oss << "\n\t\tviewId: " << it.first << " last update time(ms): " << ms; } oss << "\n"; oss << "\tspeedCount:"; for (const auto &it : _speedCount) { oss << "\n\t\tviewId: " << it.first << " speed: " << it.second; } oss << "\n"; /// For showing disconnected user info in the doc repair dialog. oss << "\tsessionUserInfo:"; for (const auto &it : _sessionUserInfo) { oss << "\n\t\tviewId: " << it.first << " userId: " << it.second.getUserId() << " userName: " << it.second.getUserName() << " userExtraInfo: " << it.second.getUserExtraInfo() << " readOnly: " << it.second.isReadOnly(); } oss << "\n"; char *pState = nullptr; _loKit->dumpState("", &pState); oss << "lok state:\n"; if (pState) oss << pState; oss << "\n"; } #if !defined BUILDING_TESTS && !MOBILEAPP && !LIBFUZZER // When building the fuzzer we link COOLWSD.cpp into the same executable so the // Protected::emitOneRecording() there gets used. When building the unit tests the one in // TraceEvent.cpp gets used. static std::mutex traceEventLock; static std::vector traceEventRecords[2]; void flushTraceEventRecordings() { std::unique_lock lock(traceEventLock); for (size_t n = 0; n < 2; ++n) { std::vector &r = traceEventRecords[n]; if (r.empty()) continue; std::size_t totalLength = 32; // Provision for the command name. for (const auto& i: r) totalLength += i.length(); std::string recordings; recordings.reserve(totalLength); recordings.append(n == 0 ? "forcedtraceevent: \n" : "traceevent: \n"); for (const auto& i: r) recordings += i; singletonDocument->sendTextFrame(recordings); r.clear(); } } static void addRecording(const std::string &recording, bool force) { // This can be called before the config system is initialized. Guard against that, as calling // config::getBool() would cause an assertion failure. static bool configChecked = false; static bool traceEventsEnabled; if (!configChecked && config::isInitialized()) { traceEventsEnabled = config::getBool("trace_event[@enable]", false); configChecked = true; } if (configChecked && !traceEventsEnabled) return; // catch if this gets called in the ForKit process & skip. if (singletonDocument == nullptr) return; if (!TraceEvent::isRecordingOn() && !force) return; std::unique_lock lock(traceEventLock); traceEventRecords[force ? 0 : 1].push_back(recording + "\n"); } void TraceEvent::emitOneRecordingIfEnabled(const std::string &recording) { addRecording(recording, true); } void TraceEvent::emitOneRecording(const std::string &recording) { addRecording(recording, false); } #else void flushTraceEventRecordings() { } #endif #ifdef __ANDROID__ std::shared_ptr Document::_loKitDocumentForAndroidOnly = std::shared_ptr(); std::shared_ptr getLOKDocumentForAndroidOnly() { return Document::_loKitDocumentForAndroidOnly; } #endif KitSocketPoll::KitSocketPoll() : SocketPoll("kit") { #ifdef IOS terminationFlag = false; #endif mainPoll = this; } KitSocketPoll::~KitSocketPoll() { // Just to make it easier to set a breakpoint mainPoll = nullptr; } void KitSocketPoll::dumpGlobalState(std::ostream& oss) // static { if (mainPoll) { if (!mainPoll->_document) oss << "KitSocketPoll: no doc\n"; else { mainPoll->_document->dumpState(oss); mainPoll->dumpState(oss); } } else oss << "KitSocketPoll: none\n"; } std::shared_ptr KitSocketPoll::create() // static { std::shared_ptr result(new KitSocketPoll()); #ifdef IOS { std::unique_lock lock(KSPollsMutex); KSPolls.push_back(result); } KitSocketPoll::KSPollsCV.notify_all(); #endif return result; } /* static */ void KitSocketPoll::cleanupChildProcess() { mainPoll->closeAllSockets(); mainPoll->createWakeups(); } // process pending message-queue events. void KitSocketPoll::drainQueue() { SigUtil::checkDumpGlobalState(dump_kit_state); if (_document) _document->drainQueue(); } // called from inside poll, inside a wakeup void KitSocketPoll::wakeupHook() { _pollEnd = std::chrono::steady_clock::now(); } // a LOK compatible poll function merging the functions. // returns the number of events signalled int KitSocketPoll::kitPoll(int timeoutMicroS) { ProfileZone profileZone("KitSocketPoll::kitPoll"); if (SigUtil::getTerminationFlag()) { LOG_TRC("Termination of unipoll mainloop flagged"); return -1; } #if ENABLE_DEBUG static std::atomic reentries = 0; static int lastWarned = 1; ReEntrancyGuard guard(reentries); if (reentries != lastWarned) { LOG_ERR("non-async dialog triggered"); #if !MOBILEAPP if (singletonDocument && lastWarned < reentries) singletonDocument->alertNotAsync(); #endif lastWarned = reentries; } #endif // The maximum number of extra events to process beyond the first. int maxExtraEvents = 15; int eventsSignalled = 0; auto startTime = std::chrono::steady_clock::now(); // handle processtoidle waiting optimization bool checkForIdle = ProcessToIdleDeadline >= startTime; if (timeoutMicroS < 0) { // Flush at most 1 + maxExtraEvents, or return when nothing left. while (poll(std::chrono::microseconds::zero()) > 0 && maxExtraEvents-- > 0) ++eventsSignalled; } else { if (checkForIdle) timeoutMicroS = 0; // Flush at most maxEvents+1, or return when nothing left. _pollEnd = startTime + std::chrono::microseconds(timeoutMicroS); do { int realTimeout = timeoutMicroS; if (_document && _document->hasQueueItems()) realTimeout = 0; if (poll(std::chrono::microseconds(realTimeout)) <= 0) break; const auto now = std::chrono::steady_clock::now(); drainQueue(); timeoutMicroS = std::chrono::duration_cast(_pollEnd - now).count(); ++eventsSignalled; } while (timeoutMicroS > 0 && !SigUtil::getTerminationFlag() && maxExtraEvents-- > 0); } if (_document && checkForIdle && eventsSignalled == 0 && timeoutMicroS > 0 && !hasCallbacks() && !hasBuffered()) { auto remainingTime = ProcessToIdleDeadline - startTime; LOG_TRC( "Poll of " << timeoutMicroS << " vs. remaining time of: " << std::chrono::duration_cast(remainingTime).count()); // would we poll until then if we could ? if (remainingTime < std::chrono::microseconds(timeoutMicroS)) _document->checkIdle(); else LOG_TRC("Poll of would not close gap - continuing"); } drainQueue(); if (_document) _document->trimAfterInactivity(); if (!Util::isMobileApp()) { flushTraceEventRecordings(); if (_document && _document->purgeSessions() == 0) { LOG_INF("Last session discarded. Setting TerminationFlag"); SigUtil::setTerminationFlag(); return -1; } } // Report the number of events we processed. return eventsSignalled; } // unusual LOK event from another thread, push into our loop to process. bool KitSocketPoll::pushToMainThread(LibreOfficeKitCallback callback, int type, const char* p, void* data) // static { if (mainPoll && mainPoll->getThreadOwner() != std::this_thread::get_id()) { LOG_TRC("Unusual push callback to main thread"); std::shared_ptr pCopy; if (p) pCopy = std::make_shared(p, strlen(p)); mainPoll->addCallback([=] { LOG_TRC("Unusual process callback in main thread"); callback(type, pCopy ? pCopy->c_str() : nullptr, data); }); return true; } return false; } KitSocketPoll *KitSocketPoll::mainPoll = nullptr; bool pushToMainThread(LibreOfficeKitCallback cb, int type, const char *p, void *data) { return KitSocketPoll::pushToMainThread(cb, type, p, data); } #ifdef IOS std::mutex KitSocketPoll::KSPollsMutex; std::condition_variable KitSocketPoll::KSPollsCV; std::vector> KitSocketPoll::KSPolls; #endif void documentViewCallback(const int type, const char* payload, void* data) { Document::ViewCallback(type, payload, data); } /// Called by LOK main-loop the central location for data processing. int pollCallback(void* pData, int timeoutUs) { if (timeoutUs < 0) timeoutUs = SocketPoll::DefaultPollTimeoutMicroS.count(); #ifndef IOS if (!pData) return 0; else return reinterpret_cast(pData)->kitPoll(timeoutUs); #else std::unique_lock lock(KitSocketPoll::KSPollsMutex); std::vector> v; for (const auto &i : KitSocketPoll::KSPolls) { auto p = i.lock(); if (p) v.push_back(p); } if (v.empty()) { // Remove any stale elements from KitSocketPoll::KSPolls and // block until an element is added to KitSocketPoll::KSPolls KitSocketPoll::KSPolls.clear(); KitSocketPoll::KSPollsCV.wait(lock, []{ return KitSocketPoll::KSPolls.size(); }); } else { lock.unlock(); for (const auto &p : v) p->kitPoll(timeoutUs); } // We never want to exit the main loop return 0; #endif } /// Called by LOK main-loop void wakeCallback(void* pData) { #ifndef IOS if (!pData) return; else return reinterpret_cast(pData)->wakeup(); #else std::unique_lock lock(KitSocketPoll::KSPollsMutex); if (KitSocketPoll::KSPolls.empty()) return; std::vector> v; for (const auto &i : KitSocketPoll::KSPolls) { auto p = i.lock(); if (p) v.push_back(p); } lock.unlock(); for (const auto &p : v) p->wakeup(); #endif } #ifndef BUILDING_TESTS namespace { #if !MOBILEAPP void copyCertificateDatabaseToTmp(Poco::Path const& jailPath) { std::string aCertificatePathString = config::getString("certificates.database_path", ""); if (!aCertificatePathString.empty()) { auto aFileStat = FileUtil::Stat(aCertificatePathString); if (!aFileStat.exists() || !aFileStat.isDirectory()) { LOG_WRN("Certificate database wasn't copied into the jail as path '" << aCertificatePathString << "' doesn't exist"); return; } Poco::Path aCertificatePath(aCertificatePathString); Poco::Path aJailedCertDBPath(jailPath, "/tmp/certdb"); Poco::File(aJailedCertDBPath).createDirectories(); bool bCopied = false; for (const char* pFilename : { "cert8.db", "cert9.db", "secmod.db", "key3.db", "key4.db" }) { bool bResult = FileUtil::copy(Poco::Path(aCertificatePath, pFilename).toString(), Poco::Path(aJailedCertDBPath, pFilename).toString(), false, false); bCopied |= bResult; } if (bCopied) { LOG_INF("Certificate database files found in '" << aCertificatePathString << "' and were copied to the jail"); ::setenv("LO_CERTIFICATE_DATABASE_PATH", "/tmp/certdb", 1); } else { LOG_WRN("No Certificate database files could be found in path '" << aCertificatePathString << "'"); } } } #endif } void lokit_main( #if !MOBILEAPP const std::string& childRoot, const std::string& jailId, const std::string& sysTemplate, const std::string& loTemplate, bool noCapabilities, bool noSeccomp, bool queryVersion, bool displayVersion, #else int docBrokerSocket, const std::string& userInterface, #endif std::size_t numericIdentifier ) { #if !MOBILEAPP if (!Util::isKitInProcess()) { // Already set by COOLWSD.cpp SigUtil::setFatalSignals("kit startup of " COOLWSD_VERSION " " COOLWSD_VERSION_HASH); SigUtil::setUserSignals(); } // Reinitialize logging when forked. const bool logToFile = std::getenv("COOL_LOGFILE"); const char* logFilename = std::getenv("COOL_LOGFILENAME"); const char* logLevel = std::getenv("COOL_LOGLEVEL"); const char* logLevelStartup = std::getenv("COOL_LOGLEVEL_STARTUP"); const bool logColor = config::getBool("logging.color", true) && isatty(fileno(stderr)); std::map logProperties; if (logToFile && logFilename) { logProperties["path"] = std::string(logFilename); } Util::rng::reseed(); const std::string LogLevel = logLevel ? logLevel : "trace"; const std::string LogLevelStartup = logLevelStartup ? logLevelStartup : "trace"; const bool bTraceStartup = (std::getenv("COOL_TRACE_STARTUP") != nullptr); Log::initialize("kit", bTraceStartup ? LogLevelStartup : logLevel, logColor, logToFile, logProperties); if (bTraceStartup && LogLevel != LogLevelStartup) { LOG_INF("Setting log-level to [" << LogLevelStartup << "] and delaying " "setting to [" << LogLevel << "] until after Kit initialization."); } const char* pAnonymizationSalt = std::getenv("COOL_ANONYMIZATION_SALT"); if (pAnonymizationSalt) { AnonymizationSalt = std::stoull(std::string(pAnonymizationSalt)); AnonymizeUserData = true; } LOG_INF("User-data anonymization is " << (AnonymizeUserData ? "enabled." : "disabled.")); const char* pEnableWebsocketURP = std::getenv("ENABLE_WEBSOCKET_URP"); EnableWebsocketURP = std::string(pEnableWebsocketURP) == "true"; assert(!childRoot.empty()); assert(!sysTemplate.empty()); assert(!loTemplate.empty()); LOG_INF("Kit process for Jail [" << jailId << "] started."); std::string userdir_url; std::string instdir_path; int ProcSMapsFile = -1; // lokit's destroy typically throws from // framework/source/services/modulemanager.cxx:198 // So we insure it lives until std::_Exit is called. std::shared_ptr loKit; ChildSession::NoCapsForKit = noCapabilities; #endif // MOBILEAPP // Setup the OSL sandbox std::string allowedPaths; try { #if !MOBILEAPP const Path jailPath = Path::forDirectory(childRoot + '/' + jailId); const std::string jailPathStr = jailPath.toString(); JailUtil::createJailPath(jailPathStr); // initialize while we have access to /proc/self/task threadCounter.reset(new Util::ThreadCounter()); // initialize while we have access to /proc/self/fd fdCounter.reset(new Util::FDCounter()); if (!ChildSession::NoCapsForKit) { std::chrono::time_point jailSetupStartTime = std::chrono::steady_clock::now(); userdir_url = "file:///tmp/user"; instdir_path = '/' + std::string(JailUtil::LO_JAIL_SUBPATH) + "/program"; allowedPaths += ":r:/" + std::string(JailUtil::LO_JAIL_SUBPATH); Poco::Path jailLOInstallation(jailPath, JailUtil::LO_JAIL_SUBPATH); jailLOInstallation.makeDirectory(); const std::string loJailDestPath = jailLOInstallation.toString(); // The bind-mount implementation: inlined here to mirror // the fallback link/copy version bellow. const auto mountJail = [&]() -> bool { // Mount sysTemplate for the jail directory. LOG_INF("Mounting " << sysTemplate << " -> " << jailPathStr); if (!JailUtil::bind(sysTemplate, jailPathStr) || !JailUtil::remountReadonly(sysTemplate, jailPathStr)) { LOG_ERR("Failed to mount [" << sysTemplate << "] -> [" << jailPathStr << "], will link/copy contents."); return false; } // Mount loTemplate inside it. LOG_INF("Mounting " << loTemplate << " -> " << loJailDestPath); if (!FileUtil::Stat(loJailDestPath).exists()) { LOG_DBG("The mount-point [" << loJailDestPath << "] doesn't exist. Binding will likely fail"); } if (!JailUtil::bind(loTemplate, loJailDestPath) || !JailUtil::remountReadonly(loTemplate, loJailDestPath)) { LOG_WRN("Failed to mount [" << loTemplate << "] -> [" << loJailDestPath << "], will link/copy contents"); return false; } // tmpdir inside the jail for added sercurity. const std::string tempRoot = Poco::Path(childRoot, "tmp").toString(); const std::string tmpSubDir = Poco::Path(tempRoot, "cool-" + jailId).toString(); Poco::File(tmpSubDir).createDirectories(); const std::string jailTmpDir = Poco::Path(jailPath, "tmp").toString(); LOG_INF("Mounting random temp dir " << tmpSubDir << " -> " << jailTmpDir); if (!JailUtil::bind(tmpSubDir, jailTmpDir)) { LOG_ERR("Failed to mount [" << tmpSubDir << "] -> [" << jailTmpDir << "], will link/copy contents."); return false; } return true; }; // Copy (link) LO installation and other necessary files into it from the template. bool bindMount = JailUtil::isBindMountingEnabled(); if (bindMount) { #if CODE_COVERAGE // Code coverage is not supported with bind-mounting. LOG_ERR("Mounting is not compatible with code-coverage."); assert(!"Mounting is not compatible with code-coverage."); #endif // CODE_COVERAGE if (!mountJail()) { LOG_INF("Cleaning up jail before linking/copying."); JailUtil::tryRemoveJail(jailPathStr); bindMount = false; JailUtil::disableBindMounting(); } } if (!bindMount) { LOG_INF("Mounting is disabled, will link/copy " << sysTemplate << " -> " << jailPathStr); // Make sure we have the jail directory. JailUtil::createJailPath(jailPathStr); // Create a file to mark this a copied jail. JailUtil::markJailCopied(jailPathStr); const std::string linkablePath = childRoot + "/linkable"; linkOrCopy(sysTemplate, jailPath, linkablePath, LinkOrCopyType::All); linkOrCopy(loTemplate, loJailDestPath, linkablePath, LinkOrCopyType::LO); #if CODE_COVERAGE // Link the .gcda files. linkGCDAFiles(jailPathStr); #endif // Update the dynamic files inside the jail. if (!JailUtil::SysTemplate::updateDynamicFiles(jailPathStr)) { LOG_ERR( "Failed to update the dynamic files in the jail [" << jailPathStr << "]. If the systemplate directory is owned by a superuser or is " "read-only, running the installation scripts with the owner's account " "should update these files. Some functionality may be missing."); } } // Setup /tmp and set TMPDIR. ::setenv("TMPDIR", "/tmp", 1); allowedPaths += ":w:/tmp"; copyCertificateDatabaseToTmp(jailPath); // HOME must be writable, so create it in /tmp. constexpr const char* HomePathInJail = "/tmp/home"; Poco::File(Poco::Path(jailPath, HomePathInJail)).createDirectories(); ::setenv("HOME", HomePathInJail, 1); const auto ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - jailSetupStartTime); LOG_DBG("Initialized jail files in " << ms); // The bug is that rewinding and rereading /proc/self/smaps_rollup doubles the previous // values, so it only affects the case where we reuse the fd from opening smaps_rollup const bool brokenSmapsRollup = (std::getenv("COOL_DISABLE_SMAPS_ROLLUP") != nullptr); ProcSMapsFile = !brokenSmapsRollup ? open("/proc/self/smaps_rollup", O_RDONLY) : -1; if (ProcSMapsFile < 0) { if (!brokenSmapsRollup) LOG_WRN("Failed to open /proc/self/smaps_rollup. Memory stats will be slower"); ProcSMapsFile = open("/proc/self/smaps", O_RDONLY); if (ProcSMapsFile < 0) LOG_SYS("Failed to open /proc/self/smaps. Memory stats will be missing."); } LOG_INF("chroot(\"" << jailPathStr << "\")"); if (chroot(jailPathStr.c_str()) == -1) { LOG_SFL("chroot(\"" << jailPathStr << "\") failed"); Util::forcedExit(EX_SOFTWARE); } if (chdir("/") == -1) { LOG_SFL("chdir(\"/\") in jail failed"); Util::forcedExit(EX_SOFTWARE); } #ifndef __FreeBSD__ dropCapability(CAP_SYS_CHROOT); dropCapability(CAP_MKNOD); dropCapability(CAP_FOWNER); dropCapability(CAP_CHOWN); #endif LOG_DBG("Initialized jail nodes, dropped caps."); } else // noCapabilities set { LOG_WRN("Security warning: running without chroot jails is insecure."); LOG_INF("Using template [" << loTemplate << "] as install subpath directly, without chroot jail setup."); userdir_url = "file://" + jailPathStr + "tmp/user"; instdir_path = '/' + loTemplate + "/program"; allowedPaths += ":r:" + loTemplate; JailRoot = jailPathStr; std::string tmpPath = jailPathStr + "tmp"; ::setenv("TMPDIR", tmpPath.c_str(), 1); allowedPaths += ":w:" + tmpPath; LOG_DBG("Using tmpdir [" << tmpPath << "]"); // used by LO Migration::migrateSettingsIfNecessary() in startup code as config dir ::setenv("XDG_CONFIG_HOME", (tmpPath + "/.config").c_str(), 1); ::setenv("HOME", tmpPath.c_str(), 1); // overwrite coolkitconfig.xcu setting to fit into allowed paths ::setenv("LOK_WORKDIR", ("file://" + tmpPath).c_str(), 1); // Setup the OSL sandbox allowedPaths += ":r:" + pathFromFileURL(userdir_url); ::setenv("SAL_ALLOWED_PATHS", allowedPaths.c_str(), 1); #if ENABLE_DEBUG ::setenv("SAL_ABORT_ON_FORBIDDEN", "1", 1); #endif } LOG_DBG("Initializing LOK with instdir [" << instdir_path << "] and userdir [" << userdir_url << "]."); UserDirPath = pathFromFileURL(userdir_url); InstDirPath = instdir_path; LibreOfficeKit* kit = nullptr; { const char *instdir = instdir_path.c_str(); const char *userdir = userdir_url.c_str(); if (!Util::isKitInProcess()) kit = UnitKit::get().lok_init(instdir, userdir); if (!kit) { kit = (initFunction ? initFunction(instdir, userdir) : lok_init_2(instdir, userdir)); } loKit = std::make_shared(kit); if (!loKit) { LOG_FTL("LibreOfficeKit initialization failed. Exiting."); Util::forcedExit(EX_SOFTWARE); } } // Lock down the syscalls that can be used if (!Seccomp::lockdown(Seccomp::Type::KIT)) { if (!noSeccomp) { LOG_FTL("LibreOfficeKit seccomp security lockdown failed. Exiting."); Util::forcedExit(EX_SOFTWARE); } LOG_ERR("LibreOfficeKit seccomp security lockdown failed, but configured to continue. " "You are running in a significantly less secure mode."); } rlimit rlim = { 0, 0 }; if (getrlimit(RLIMIT_AS, &rlim) == 0) LOG_INF("RLIMIT_AS is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)"); else LOG_SYS("Failed to get RLIMIT_AS"); if (getrlimit(RLIMIT_STACK, &rlim) == 0) LOG_INF("RLIMIT_STACK is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)"); else LOG_SYS("Failed to get RLIMIT_STACK"); if (getrlimit(RLIMIT_FSIZE, &rlim) == 0) LOG_INF("RLIMIT_FSIZE is " << Util::getHumanizedBytes(rlim.rlim_max) << " (" << rlim.rlim_max << " bytes)"); else LOG_SYS("Failed to get RLIMIT_FSIZE"); if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) LOG_INF("RLIMIT_NOFILE is " << rlim.rlim_max << " files."); else LOG_SYS("Failed to get RLIMIT_NOFILE"); LOG_INF("Kit process for Jail [" << jailId << "] is ready."); std::string pathAndQuery(NEW_CHILD_URI); pathAndQuery.append("?jailid="); pathAndQuery.append(jailId); if (queryVersion) { char* versionInfo = loKit->getVersionInfo(); std::string versionString(versionInfo); if (displayVersion) std::cout << "office version details: " << versionString << std::endl; SigUtil::setVersionInfo(versionString); // Add some parameters we want to pass to the client. Could not figure out how to get // the configuration parameters from COOLWSD.cpp's initialize() or coolwsd.xml here, so // oh well, just have the value hardcoded in KitHelper.hpp. It isn't really useful to // "tune" it at end-user installations anyway, I think. auto versionJSON = Poco::JSON::Parser().parse(versionString).extract(); versionJSON->set("tunnelled_dialog_image_cache_size", std::to_string(LOKitHelper::tunnelledDialogImageCacheSize)); std::stringstream ss; versionJSON->stringify(ss); versionString = ss.str(); std::string encodedVersion; Poco::URI::encode(versionString, "?#/", encodedVersion); pathAndQuery.append("&version="); pathAndQuery.append(encodedVersion); free(versionInfo); } #else // MOBILEAPP #ifndef IOS // Was not done by the preload. // For iOS we call it in -[AppDelegate application: didFinishLaunchingWithOptions:] setupKitEnvironment(userInterface); #endif #if (defined(__linux__) && !defined(__ANDROID__)) || defined(__FreeBSD__) Poco::URI userInstallationURI("file", LO_PATH); LibreOfficeKit *kit = lok_init_2(LO_PATH "/program", userInstallationURI.toString().c_str()); #else #ifdef IOS // In the iOS app we call lok_init_2() just once, when the app starts static LibreOfficeKit *kit = lo_kit; #else static LibreOfficeKit *kit = lok_init_2(nullptr, nullptr); #endif #endif assert(kit); static std::shared_ptr loKit = std::make_shared(kit); assert(loKit); COOLWSD::LOKitVersion = loKit->getVersionInfo(); // Dummies const std::string jailId = "jailid"; #endif // MOBILEAPP auto mainKit = KitSocketPoll::create(); mainKit->runOnClientThread(); // We will do the polling on this thread. std::shared_ptr websocketHandler = std::make_shared("child_ws", loKit, jailId, mainKit, numericIdentifier); #if !MOBILEAPP std::vector shareFDs; if (ProcSMapsFile >= 0) shareFDs.push_back(ProcSMapsFile); if (isURPEnabled()) { if (pipe2(URPtoLoFDs, O_CLOEXEC) != 0 || pipe2(URPfromLoFDs, O_CLOEXEC | O_NONBLOCK) != 0) LOG_ERR("Failed to create urp pipe " << strerror(errno)); else { shareFDs.push_back(URPtoLoFDs[1]); shareFDs.push_back(URPfromLoFDs[0]); } } if (!mainKit->insertNewUnixSocket(MasterLocation, pathAndQuery, websocketHandler, &shareFDs)) { LOG_SFL("Failed to connect to WSD. Will exit."); Util::forcedExit(EX_SOFTWARE); } #else mainKit->insertNewFakeSocket(docBrokerSocket, websocketHandler); #endif LOG_INF("New kit client websocket inserted."); #if !MOBILEAPP if (bTraceStartup && LogLevel != LogLevelStartup) { LOG_INF("Kit initialization complete: setting log-level to [" << LogLevel << "] as configured."); Log::logger().setLevel(LogLevel); } #endif #ifndef IOS if (!LIBREOFFICEKIT_HAS(kit, runLoop)) { LOG_FTL("Kit is missing Unipoll API"); std::cout << "Fatal: out of date LibreOfficeKit - no Unipoll API\n"; Util::forcedExit(EX_SOFTWARE); } LOG_INF("Kit unipoll loop run"); loKit->runLoop(pollCallback, wakeCallback, mainKit.get()); LOG_INF("Kit unipoll loop run terminated."); #if MOBILEAPP SocketPoll::wakeupWorld(); #else // Trap the signal handler, if invoked, // to prevent exiting. LOG_INF("Kit process for Jail [" << jailId << "] finished."); // Let forkit handle the jail cleanup. #endif #else // IOS std::unique_lock lock(mainKit->terminationMutex); mainKit->terminationCV.wait(lock,[&]{ return mainKit->terminationFlag; } ); #endif // !IOS } catch (const Exception& exc) { LOG_ERR("Poco Exception: " << exc.displayText() << (exc.nested() ? " (" + exc.nested()->displayText() + ')' : "")); } catch (const std::exception& exc) { LOG_ERR("Exception: " << exc.what()); } #if !MOBILEAPP LOG_INF("Kit process for Jail [" << jailId << "] finished."); flushTraceEventRecordings(); // Wait for the signal handler, if invoked, to prevent exiting until done. SigUtil::waitSigHandlerTrap(); if (!Util::isKitInProcess()) Util::forcedExit(EX_OK); #endif } #ifdef IOS // In the iOS app we can have several documents open in the app process at the same time, thus // several lokit_main() functions running at the same time. We want just one LO main loop, though, // so we start it separately in its own thread. void runKitLoopInAThread() { std::thread([&] { Util::setThreadName("lokit_runloop"); std::shared_ptr loKit = std::make_shared(lo_kit); int dummy; loKit->runLoop(pollCallback, wakeCallback, &dummy); // Should never return assert(false); NSLog(@"loKit->runLoop() unexpectedly returned"); std::abort(); }).detach(); } #endif // IOS #endif // !BUILDING_TESTS void consistencyCheckJail() { static bool warned = false; if (!warned) { bool failedTmp, failedLo, failedUser; FileUtil::Stat tmp("/tmp"); if ((failedTmp = (!tmp.good() || !tmp.isDirectory()))) LOG_ERR("Fatal system error: Kit jail is missing its /tmp directory"); FileUtil::Stat lo(InstDirPath + "/unorc"); if ((failedLo = (!lo.good() || !lo.isFile()))) LOG_ERR("Fatal system error: Kit jail is missing its LibreOfficeKit directory at '" << InstDirPath << "'"); FileUtil::Stat user(UserDirPath); if ((failedUser = (!user.good() || !user.isDirectory()))) LOG_ERR("Fatal system error: Kit jail is missing its user directory at '" << UserDirPath << "'"); if (failedTmp || failedLo || failedUser) { LOG_ERR("A fatal system error indicates that, outside the control of COOL " "major structural changes have occured in our filesystem. These are " "potentially indicative of an operator damaging the system, and will " "inevitably cause document data-loss and/or malfunction."); warned = true; SigUtil::addActivity("Fatal, inconsistent jail detected."); assert(!"Fatal system error with jail setup."); } else LOG_TRC("Passed system consistency check"); } } /// Fetch the latest montonically incrementing wire-id TileWireId getCurrentWireId(bool increment) { return RenderTiles::getCurrentWireId(increment); } std::string anonymizeUrl(const std::string& url) { #ifndef BUILDING_TESTS return AnonymizeUserData ? Util::anonymizeUrl(url, AnonymizationSalt) : url; #else return url; #endif } static int receiveURPFromLO(void* pContext, const signed char* pBuffer, int bytesToWrite) { int bytesWritten = 0; while (bytesToWrite > 0) { int bytes = ::write(reinterpret_cast(pContext), pBuffer + bytesWritten, bytesToWrite); if (bytes <= 0) break; bytesToWrite -= bytes; bytesWritten += bytes; } return bytesWritten; } static int sendURPToLO(void* pContext, signed char* pBuffer, int bytesToRead) { int bytesRead = 0; while (bytesToRead > 0) { int bytes = ::read(reinterpret_cast(pContext), pBuffer + bytesRead, bytesToRead); if (bytes <= 0) break; bytesToRead -= bytes; bytesRead += bytes; } return bytesRead; } // temp workaround of changed signature of startURP. Compile detect // old signature and if so return nullptr extern "C" int (*ObsoleteStartURPSignature)(LibreOfficeKit*, void*, void**, int (*)(void* pContext, const signed char* pBuffer, int nLen), int (**)(void* pContext, const signed char* pBuffer, int nLen)); template void* doStartURP(T&, std::true_type) { (void)receiveURPFromLO; (void)sendURPToLO; return nullptr; } template void* doStartURP(T& LOKit, std::false_type) { return LOKit->startURP(reinterpret_cast(URPfromLoFDs[1]), reinterpret_cast(URPtoLoFDs[0]), receiveURPFromLO, sendURPToLO); } bool startURP(std::shared_ptr LOKit, void** ppURPContext) { if (!isURPEnabled()) { LOG_ERR("URP/WS: Attempted to start a URP session but URP is disabled"); return false; } if (URPStartCount > 0) { LOG_WRN("URP/WS: Not starting another URP session as one has already been opened for this " "kit instance"); return false; } *ppURPContext = doStartURP(LOKit, std::is_same() ); if (!*ppURPContext) { LOG_ERR("URP/WS: tried to start a URP session but core did not let us"); return false; } URPStartCount++; return true; } #if !MOBILEAPP /// Initializes LibreOfficeKit for cross-fork re-use. bool globalPreinit(const std::string &loTemplate) { const std::string libSofficeapp = loTemplate + "/program/" LIB_SOFFICEAPP; const std::string libMerged = loTemplate + "/program/" LIB_MERGED; std::string loadedLibrary; // we deliberately don't dlclose handle on success, make it // static so static analysis doesn't see this as a leak static void *handle; if (File(libMerged).exists()) { LOG_TRC("dlopen(" << libMerged << ", RTLD_GLOBAL|RTLD_NOW)"); handle = dlopen(libMerged.c_str(), RTLD_GLOBAL|RTLD_NOW); if (!handle) { LOG_FTL("Failed to load " << libMerged << ": " << dlerror()); return false; } loadedLibrary = libMerged; } else { if (File(libSofficeapp).exists()) { LOG_TRC("dlopen(" << libSofficeapp << ", RTLD_GLOBAL|RTLD_NOW)"); handle = dlopen(libSofficeapp.c_str(), RTLD_GLOBAL|RTLD_NOW); if (!handle) { LOG_FTL("Failed to load " << libSofficeapp << ": " << dlerror()); return false; } loadedLibrary = libSofficeapp; } else { LOG_FTL("Neither " << libSofficeapp << " or " << libMerged << " exist."); return false; } } LokHookPreInit2* preInit = reinterpret_cast(dlsym(handle, "lok_preinit_2")); if (!preInit) { LOG_FTL("No lok_preinit_2 symbol in " << loadedLibrary << ": " << dlerror()); dlclose(handle); return false; } initFunction = reinterpret_cast(dlsym(handle, "libreofficekit_hook_2")); if (!initFunction) { LOG_FTL("No libreofficekit_hook_2 symbol in " << loadedLibrary << ": " << dlerror()); } // Disable problematic components that may be present from a // desktop or developer's install if env. var not set. ::setenv("UNODISABLELIBRARY", "abp avmediagst avmediavlc cmdmail losessioninstall OGLTrans PresenterScreen " "syssh ucpftp1 ucpgio1 ucphier1 ucpimage updatecheckui updatefeed updchk" // Database "dbaxml dbmm dbp dbu deployment firebird_sdbc mork " "mysql mysqlc odbc postgresql-sdbc postgresql-sdbc-impl sdbc2 sdbt" // Java "javaloader javavm jdbc rpt rptui rptxml ", 0 /* no overwrite */); LOG_TRC("Invoking lok_preinit_2(" << loTemplate << "/program\", \"file:///tmp/user\")"); const auto start = std::chrono::steady_clock::now(); if (preInit((loTemplate + "/program").c_str(), "file:///tmp/user", &loKitPtr) != 0) { LOG_FTL("lok_preinit() in " << loadedLibrary << " failed"); dlclose(handle); return false; } LOG_DBG("After lok_preinit_2: loKitPtr=" << loKitPtr); LOG_TRC("Finished lok_preinit(" << loTemplate << "/program\", \"file:///tmp/user\") in " << std::chrono::duration_cast( std::chrono::steady_clock::now() - start)); return true; } /// Anonymize usernames. std::string anonymizeUsername(const std::string& username) { #ifndef BUILDING_TESTS return AnonymizeUserData ? Util::anonymize(username, AnonymizationSalt) : username; #else return username; #endif } #endif // !MOBILEAPP void dump_kit_state() { std::ostringstream oss; KitSocketPoll::dumpGlobalState(oss); const std::string msg = oss.str(); fprintf(stderr, "%s", msg.c_str()); LOG_TRC(msg); } #if defined __GLIBC__ # include void dump_malloc_state() { malloc_info(0, stderr); fflush(stderr); } #endif /* vim:set shiftwidth=4 softtabstop=4 expandtab: */