/* -*- 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/. */ /* * Implementation of main server application logic. * Classes: COOLWSD */ #include #include #include "COOLWSD.hpp" #if !MOBILEAPP && ENABLE_DEBUG /* Default host used in the start test URI */ #define COOLWSD_TEST_HOST "localhost" /* Default cool UI used in the admin console URI */ #define COOLWSD_TEST_ADMIN_CONSOLE "/browser/dist/admin/admin.html" /* Default cool UI used in for monitoring URI */ #define COOLWSD_TEST_METRICS "/cool/getMetrics" /* Page that lists the documents this server can open, and opens the one picked */ #define COOLWSD_TEST_DOCUMENT_PICKER "/browser/" COOLWSD_VERSION_HASH "/documents.html" #endif // This is the main source for the coolwsd program. COOL uses several coolwsd processes: one main // parent process that listens on the TCP port and accepts connections from COOL clients, and a // number of child processes, each which handles a viewing (editing) session for one document. #include #include #include #if ENABLE_FEATURE_LOCK #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if ENABLE_SSL #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #include #include #endif using Poco::Util::LayeredConfiguration; using Poco::Util::Option; /// Port for external clients to connect to int ClientPortNumber = 0; #if !MOBILEAPP /// UDS address for kits to connect to. UnxSocketPath MasterLocation; std::string COOLWSD::BuyProductUrl; std::string COOLWSD::LatestVersion; std::mutex COOLWSD::FetchUpdateMutex; std::mutex COOLWSD::RemoteConfigMutex; #endif /// The DocBrokers container; used from elsewhere as well. std::map> DocBrokers; std::mutex DocBrokersMutex; ///< Protects DocBrokers. namespace { /// Protocols to listen on Socket::Type ClientPortProto = Socket::Type::All; /// INET address to listen on ServerSocket::Type ClientListenAddr = ServerSocket::Type::Public; // Tracks the set of prisoners / children waiting to be used. std::mutex NewChildrenMutex; std::condition_variable NewChildrenCV; std::vector> NewChildren; std::atomic TotalOutstandingForks(0); std::map OutstandingForks; std::map LastForkRequestTimes; using SubForKitMap = std::map>; SubForKitMap SubForKitProcs; std::map LastSubForKitBrokerExitTimes; std::set OutstandingSubForKitSpawns; std::map LastSubForKitSpawnRequestTimes; Poco::AutoPtr KitXmlConfig; std::string LoggableConfigEntries; #if MOBILEAPP // Or can this be retrieved in some other way? int PrisonerServerSocketFD; #else // MOBILEAPP std::shared_ptr FetchHttpSession; #if ENABLE_DEBUG std::chrono::milliseconds careerSpanMs(std::chrono::milliseconds::zero()); #endif #endif // !MOBILEAPP /// The timeout for a child to spawn, initially high, then reset to the default. std::atomic ChildSpawnTimeoutMs = std::chrono::milliseconds(CHILD_SPAWN_TIMEOUT_MS); } // namespace std::atomic COOLWSD::NumConnections; std::unordered_set COOLWSD::EditFileExtensions; std::string COOLWSD::ViewModeFileExtensions; extern "C" { void dump_state(void); /* easy for gdb */ static void forwardSigUsr2(); } void COOLWSD::appendAllowedHostsFrom(const LayeredConfiguration& conf, const std::string& root, std::vector& allowed, bool onlyAllowed) { for (const std::string& path : ConfigUtil::getIndexedKeys(conf, root, "host")) { if (onlyAllowed && !conf.getBool(path + "[@allow]", false)) continue; std::string host = ConfigUtil::getConfigValue(conf, path, ""); if (!host.empty()) { LOG_INF_S("Adding trusted LOK_ALLOW host: [" << host << ']'); allowed.push_back(std::move(host)); } } } namespace { std::string removeProtocolAndPort(const std::string& host) { std::string result; // protocol size_t pos = host.find("//"); if (pos != std::string::npos) result = host.substr(pos + 2); else result = host; // port pos = result.find(':'); if (pos != std::string::npos) { if (pos == 0) return std::string(); result = result.substr(0, pos); } return result; } } void COOLWSD::appendAllowedAliasGroups(const LayeredConfiguration& conf, std::vector& allowed) { for (size_t i = 0;; i++) { const std::string path = "storage.wopi.alias_groups.group[" + std::to_string(i) + ']'; if (!conf.has(path + ".host")) { break; } std::string host = conf.getString(path + ".host", ""); bool allow = conf.getBool(path + ".host[@allow]", false); if (!allow) { break; } host = removeProtocolAndPort(host); if (!host.empty()) { LOG_INF_S("Adding trusted LOK_ALLOW host: [" << host << ']'); allowed.push_back(std::move(host)); } for (size_t j = 0;; j++) { const std::string aliasPath = path + ".alias[" + std::to_string(j) + ']'; if (!conf.has(aliasPath)) { break; } std::string alias = ConfigUtil::getConfigValue(conf, aliasPath, ""); alias = removeProtocolAndPort(alias); if (!alias.empty()) { LOG_INF_S("Adding trusted LOK_ALLOW alias: [" << alias << ']'); allowed.push_back(std::move(alias)); } } } } /// Internal implementation to alert all clients /// connected to any document. void COOLWSD::alertAllUsersInternal(const std::string& msg) { if (Util::isMobileApp()) return; std::lock_guard docBrokersLock(DocBrokersMutex); LOG_INF("Alerting all users: [" << msg << ']'); SigUtil::addActivity("alert all users: " + msg); if (UnitWSD::get().filterAlertAllusers(msg)) return; for (const auto& brokerIt : DocBrokers) { std::shared_ptr docBroker = brokerIt.second; docBroker->addCallback([msg, docBroker](){ docBroker->alertAllUsers(msg); }); } } #if !MOBILEAPP void COOLWSD::syncUsersBrowserSettings(const std::string& userId, const pid_t childPid, const std::string& json) { if (Util::isMobileApp()) return; std::lock_guard docBrokersLock(DocBrokersMutex); LOG_INF("Syncing browsersettings for all the users"); for (const auto& brokerIt : DocBrokers) { std::shared_ptr docBroker = brokerIt.second; if (docBroker->getPid() == childPid) continue; docBroker->addCallback([userId, json, docBroker]() { docBroker->syncBrowserSettings(userId, json); }); } } #endif void COOLWSD::alertUserInternal(const std::string& dockey, const std::string& msg) { if (Util::isMobileApp()) return; std::lock_guard docBrokersLock(DocBrokersMutex); LOG_INF("Alerting document users with dockey: [" << dockey << ']' << " msg: [" << msg << ']'); const auto brokerIt = DocBrokers.find(dockey); if (brokerIt != DocBrokers.end()) { std::shared_ptr docBroker = brokerIt->second; docBroker->addCallback([msg, docBroker]() { docBroker->alertAllUsers(msg); }); } } void COOLWSD::writeTraceEventRecording(const char *data, std::size_t nbytes) { static std::mutex traceEventFileMutex; std::unique_lock lock(traceEventFileMutex); fwrite(data, nbytes, 1, COOLWSD::TraceEventFile); } void COOLWSD::writeTraceEventRecording(const std::string &recording) { writeTraceEventRecording(recording.data(), recording.length()); } #if !MOBILEAPP ssize_t COOLWSD::getInteractiveDocBrokerCount() { std::lock_guard docBrokersLock(DocBrokersMutex); return DocBrokers.size() - ConvertToBroker::getInstanceCount(); } int COOLWSD::getOutstandingForksCount() { return TotalOutstandingForks; } #endif void COOLWSD::checkSessionLimitsAndWarnClients() { #if !MOBILEAPP if constexpr (ConfigUtil::isSupportKeyEnabled()) return; if (COOLWSD::MaxDocuments >= 10000) return; if (getInteractiveDocBrokerCount() > static_cast(COOLWSD::MaxDocuments) || COOLWSD::NumConnections >= COOLWSD::MaxConnections) { std::ostringstream oss; oss << "info: cmd=socket kind=limitreached params=" << COOLWSD::MaxDocuments << "," << COOLWSD::MaxConnections; const std::string info = oss.str(); LOG_INF("Sending client 'limitreached' message: " << info); try { Util::alertAllUsers(info); } catch (const std::exception& ex) { LOG_ERR("Error while shutting down socket on reaching limit: " << ex.what()); } } #endif } void COOLWSD::checkDiskSpaceAndWarnClients(const bool cacheLastCheck) { #if !MOBILEAPP try { const std::string fs = FileUtil::checkDiskSpaceOnRegisteredFileSystems(cacheLastCheck); if (!fs.empty()) { LOG_WRN("Filesystem [" << fs << "] is dangerously low on disk space"); COOLWSD::alertAllUsersInternal("error: cmd=internal kind=diskfull"); } } catch (const std::exception& exc) { LOG_ERR("Exception while checking disk-space and warning clients: " << exc.what()); } #else (void) cacheLastCheck; #endif } #if !MOBILEAPP static void closeSubForKit(const std::string& configId, const std::shared_ptr& subForKit); #endif namespace { /// Forget a subforkit. With askToExit, no document uses its configuration any more: the subforkit /// and its spare kits are asked to exit, and the shared settings of the configuration are removed. /// Otherwise the subforkit is killed, and the shared settings stay for the subforkit that replaces /// it. SubForKitMap::iterator dropSubForKit(SubForKitMap::iterator it, bool askToExit) { // copy as it will be used after erase() std::string configId = it->first; LastSubForKitBrokerExitTimes.erase(configId); OutstandingForks.erase(configId); OutstandingSubForKitSpawns.erase(configId); LastSubForKitSpawnRequestTimes.erase(configId); #if !MOBILEAPP if (askToExit) { closeSubForKit(configId, it->second); const std::string sharedPresets = Poco::Path(COOLWSD::ChildRoot, JailUtil::CHILDROOT_TMP_SHARED_PRESETS_PATH).toString(); FileUtil::removeFile(Poco::Path(sharedPresets, Uri::encode(configId)).toString(), true); } #else (void)askToExit; #endif it = SubForKitProcs.erase(it); UNITWSD_CALL(killSubForKit(configId)); return it; } } /// Remove dead and idle DocBrokers. /// The client of idle document should've greyed-out long ago. void COOLWSD::cleanupDocBrokers() { Util::assertIsLocked(DocBrokersMutex); const size_t count = DocBrokers.size(); std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); std::set activeConfigs; for (auto it = DocBrokers.begin(); it != DocBrokers.end(); ) { std::shared_ptr docBroker = it->second; // Time out DocBrokers that never loaded docBroker->timeoutNotLoaded(now); // Remove only when not alive. if (!docBroker->isAlive()) { LastSubForKitBrokerExitTimes[docBroker->getConfigId()] = now; LOG_INF("Removing DocumentBroker for docKey [" << it->first << "], " << docBroker.use_count() << " references"); docBroker->dispose(); it = DocBrokers.erase(it); continue; } activeConfigs.insert(docBroker->getConfigId()); ++it; } if (count != DocBrokers.size()) { LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after cleanup" << // Use a capture-default even though nothing is captured, but which stops this // lambda from converting to a function pointer, which would otherwise collide with // the std::ostream manipulator operator<< overloads: // [-loplugin:unusedcapturedefault] [&](auto& log) { int i = 1; for (const auto& pair : DocBrokers) { log << "\nDocumentBroker #" << i++ << " [" << pair.first << ']'; } }); CONFIG_STATIC const std::chrono::seconds IdleServerSettingsTimeoutSecs = ConfigUtil::getConfigValue("serverside_config.idle_timeout_secs", 3600); // consider shutting down unused subforkits // Always drop those older than IdleServerSettingsTimeoutSecs, and cap // the remainder to a reasonable number of recently-used entries. CONFIG_STATIC const size_t MaxRecentlyUsedSubForKits = []{ size_t value = ConfigUtil::getConfigValue("serverside_config.max_idle_subforkits", 5); if (value < 1) { LOG_WRN("max_idle_subforkits is 0, clamping to 1."); value = 1; } return value; }(); // idle candidates: pair of (idle duration, configId) std::vector> idleCandidates; for (const auto& [configId, subForKitProc] : SubForKitProcs) { if (configId.empty()) { // ignore primordial forkit } else if (activeConfigs.contains(configId)) { LOG_DBG("subforkit " << configId << " has active document, keep it"); } else if (OutstandingForks[configId] > 0) { LOG_DBG("subforkit " << configId << " has a pending fork underway, keep it"); } else { auto idleDuration = now - LastSubForKitBrokerExitTimes[configId]; idleCandidates.emplace_back(idleDuration, configId); } } // sort shortest-idle first so we keep the most recently used std::sort(idleCandidates.begin(), idleCandidates.end()); size_t recentlyUsedKept = 0; for (const auto& [idleDuration, configId] : idleCandidates) { if (idleDuration >= IdleServerSettingsTimeoutSecs) { LOG_DBG("subforkit " << configId << " is unused, dropping it"); auto it = SubForKitProcs.find(configId); assert(it != SubForKitProcs.end()); dropSubForKit(it, /*askToExit=*/true); } else if (recentlyUsedKept >= MaxRecentlyUsedSubForKits) { LOG_DBG("subforkit " << configId << " recently used but excess idle subforkit, dropping it"); auto it = SubForKitProcs.find(configId); assert(it != SubForKitProcs.end()); dropSubForKit(it, /*askToExit=*/true); } else { LOG_DBG("subforkit " << configId << " recently used, keep it"); ++recentlyUsedKept; } } #if !MOBILEAPP && ENABLE_DEBUG if (COOLWSD::SingleKit && DocBrokers.empty()) { LOG_DBG("Setting ShutdownRequestFlag: No more docs left in single-kit mode."); SigUtil::requestShutdown(); } #endif } } #if !MOBILEAPP /// Forks as many children as requested. static void forkChildren(const std::string& configId, const int number) { if (Util::isKitInProcess()) return; LOG_TRC("Request forkit to spawn " << number << " new child(ren)"); Util::assertIsLocked(NewChildrenMutex); if (number > 0) { COOLWSD::checkDiskSpaceAndWarnClients(false); const std::string message = "spawn " + std::to_string(number) + '\n'; LOG_DBG("MasterToForKit: " << message.substr(0, message.length() - 1)); if (COOLWSD::sendMessageToForKit(message, configId)) { TotalOutstandingForks += number; OutstandingForks[configId] += number; LastForkRequestTimes[configId] = std::chrono::steady_clock::now(); } } } static bool queueMessageToForKit(const std::string& message); bool COOLWSD::ensureSubForKit(const std::string& configId) { if (Util::isKitInProcess()) return false; LOG_TRC("Request forkit to spawn subForKit " << configId); auto it = SubForKitProcs.find(configId); if (it != SubForKitProcs.end()) { LOG_TRC("subForKit " << configId << " already running"); return false; } const auto now = std::chrono::steady_clock::now(); if (OutstandingSubForKitSpawns.contains(configId)) { const auto durationMs = std::chrono::duration_cast( now - LastSubForKitSpawnRequestTimes[configId]); if (durationMs < ChildSpawnTimeoutMs.load()) { LOG_TRC("subForKit " << configId << " spawn pending for " << durationMs << ", waiting"); return false; } LOG_WRN("subForKit " << configId << " not responsive for " << durationMs << " while spawning. Resetting."); } COOLWSD::checkDiskSpaceAndWarnClients(false); OutstandingSubForKitSpawns.insert(configId); LastSubForKitSpawnRequestTimes[configId] = now; const std::string answerMessage = "addforkit " + configId + '\n'; LOG_DBG("MasterToForKit: " << answerMessage.substr(0, answerMessage.length() - 1)); return queueMessageToForKit(answerMessage); } /// Cleans up dead children. static void cleanupChildren() { if (Util::isKitInProcess()) return; Util::assertIsLocked(NewChildrenMutex); const int count = NewChildren.size(); for (int i = count - 1; i >= 0; --i) { if (!NewChildren[i]->isAlive()) { LOG_WRN("Removing dead spare child [" << NewChildren[i]->getPid() << "]."); NewChildren.erase(NewChildren.begin() + i); } } if (static_cast(NewChildren.size()) != count) SigUtil::addActivity("removed " + std::to_string(count - NewChildren.size()) + " children"); } /// Decides how many children need spawning and spawns. static void rebalanceChildren(const std::string& configId, int64_t balance) { Util::assertIsLocked(NewChildrenMutex); // Remove dead children first so the available count is accurate. cleanupChildren(); int64_t available = 0; for (const auto& elem : NewChildren) { if (elem->getConfigId() == configId) ++available; } LOG_TRC("Rebalance children to " << balance << ", have " << available << " and " << OutstandingForks[configId] << " outstanding requests"); const auto duration = std::chrono::steady_clock::now() - LastForkRequestTimes[configId]; const auto durationMs = std::chrono::duration_cast(duration); if (OutstandingForks[configId] != 0 && durationMs >= ChildSpawnTimeoutMs.load()) { // Children taking too long to spawn. // Forget we had requested any, and request anew. LOG_WRN("ForKit not responsive for " << durationMs << " forking " << OutstandingForks[configId] << " children. Resetting."); TotalOutstandingForks -= OutstandingForks[configId]; OutstandingForks[configId] = 0; } if (OutstandingForks[configId] != 0) { LOG_DBG("prespawnChildren [" << configId << "]: Have " << OutstandingForks[configId] << " outstanding fork requests. Time since last request: " << durationMs); return; } balance -= available; if (balance > 0) { LOG_DBG("prespawnChildren [" << configId << "]: Have " << available << " spare " << (available == 1 ? "child" : "children") << " (total: " << NewChildren.size() << "), forking " << balance << " more. Time since last request: " << durationMs); forkChildren(configId, balance); } } /// Proactively spawn children processes /// to load documents with alacrity. static void prespawnChildren() { // Rebalance if not forking already. std::unique_lock lock(NewChildrenMutex, std::defer_lock); if (lock.try_lock()) { rebalanceChildren("", COOLWSD::NumPreSpawnedChildren); } } #else // MOBILEAPP void prespawnChildren(); #endif // MOBILEAPP static size_t addNewChild(std::shared_ptr child) { assert(child && "Adding null child"); const auto pid = child->getPid(); const std::string& configId = child->getConfigId(); std::unique_lock lock(NewChildrenMutex); --TotalOutstandingForks; --OutstandingForks[configId]; // Prevent from going -ve if we have unexpected children. if (OutstandingForks[configId] < 0) { ++TotalOutstandingForks; ++OutstandingForks[configId]; } if (COOLWSD::IsBindMountingEnabled) { // Reset the child-spawn timeout to the default, now that we're set. // But only when mounting is enabled. Otherwise, copying is always slow. ChildSpawnTimeoutMs = std::chrono::milliseconds(CHILD_TIMEOUT_MS); } LOG_TRC("Adding a new child " << pid << " with config " << configId << " to NewChildren, have " << OutstandingForks[configId] << " outstanding requests"); SigUtil::addActivity("added child " + std::to_string(pid)); NewChildren.emplace_back(std::move(child)); const size_t count = NewChildren.size(); lock.unlock(); LOG_INF("Have " << count << " spare " << (count == 1 ? "child" : "children") << " after adding [" << pid << "]. Notifying."); // cf. filtering in notified waiter NewChildrenCV.notify_all(); return count; } #if !MOBILEAPP namespace { #if ENABLE_DEBUG inline std::string getLaunchBase(bool asAdmin = false) { std::ostringstream oss; oss << " "; oss << ((ConfigUtil::isSslEnabled() || ConfigUtil::isSSLTermination()) ? "https://" : "http://"); if (asAdmin) { auto user = ConfigUtil::getConfigValue("admin_console.username", ""); auto passwd = ConfigUtil::getConfigValue("admin_console.password", ""); if (user.empty() || passwd.empty()) return std::string(); oss << user << ':' << passwd << '@'; } oss << COOLWSD_TEST_HOST ":"; oss << ClientPortNumber; return oss.str(); } inline std::string getServiceURI(const std::string &sub, bool asAdmin = false) { std::ostringstream oss; oss << getLaunchBase(asAdmin); oss << COOLWSD::ServiceRoot; oss << sub; return oss.str(); } #endif } // anonymous namespace #endif // MOBILEAPP #if !MOBILEAPP std::atomic COOLWSD::ForKitProcId(-1); std::shared_ptr COOLWSD::ForKitProc; bool COOLWSD::NoCapsForKit = false; bool COOLWSD::RequireLandlock = false; bool COOLWSD::LandlockAvailable = false; bool COOLWSD::CapabilityJailUnusable = false; bool COOLWSD::NoSeccomp = false; bool COOLWSD::AdminEnabled = true; bool COOLWSD::UnattendedRun = false; bool COOLWSD::SignalParent = false; bool COOLWSD::UseEnvVarOptions = false; std::string COOLWSD::RouteToken; #if ENABLE_DEBUG bool COOLWSD::SingleKit = false; bool COOLWSD::ForceCaching = false; #endif bool COOLWSD::WASMEnabled = false; Util::UnorderedStringMap COOLWSD::Uri2WasmModeMap; #endif std::string COOLWSD::SysTemplate; std::string COOLWSD::LoTemplate = LO_PATH; std::string COOLWSD::CleanupChildRoot; std::string COOLWSD::ChildRoot; std::string COOLWSD::ServerName; std::string COOLWSD::FileServerRoot; std::string COOLWSD::ServiceRoot; std::string COOLWSD::TmpFontDir; std::string COOLWSD::LOKitVersion; std::string COOLWSD::LOKitVersionNumber; std::string COOLWSD::LOKitVersionHash; std::string COOLWSD::ConfigFile = #if defined(MACOS) && MOBILEAPP getResourcePath("coolwsd", "xml"); #else COOLWSD_CONFIGDIR "/coolwsd.xml"; #endif bool COOLWSD::EnableTraceEventLogging = false; bool COOLWSD::EnableAccessibility = false; bool COOLWSD::EnableMountNamespaces = false; FILE *COOLWSD::TraceEventFile = nullptr; std::string COOLWSD::LogLevel = "trace"; std::string COOLWSD::LogLevelStartup = "trace"; std::string COOLWSD::LogDisabledAreas = "Socket,WebSocket,Admin,Pixel"; std::string COOLWSD::LogToken; std::string COOLWSD::MostVerboseLogLevelSettableFromClient = "notice"; std::string COOLWSD::LeastVerboseLogLevelSettableFromClient = "fatal"; std::string COOLWSD::UserInterface = "default"; bool COOLWSD::AnonymizeUserData = false; bool COOLWSD::CheckCoolUser = true; bool COOLWSD::CleanupOnly = false; ///< If we should cleanup and exit. bool COOLWSD::ProbeOnly = false; ///< If we should health-check the running server and exit. #if ENABLE_DEBUG bool COOLWSD::FindFreePort = false; ///< If we should find a free port to listen on. #endif bool COOLWSD::IsProxyPrefixEnabled = false; unsigned COOLWSD::MaxConnections; unsigned COOLWSD::MaxDocuments; std::string COOLWSD::HardwareResourceWarning = "ok"; std::string COOLWSD::OverrideWatermark; std::chrono::steady_clock::time_point COOLWSD::StartTime; bool COOLWSD::IsBindMountingEnabled = true; bool COOLWSD::IndirectionServerEnabled = false; // If you add global state please update dumpState below too static std::string UnitTestLibrary; unsigned int COOLWSD::NumPreSpawnedChildren = 0; std::unique_ptr COOLWSD::TraceDumper; namespace { class PrisonPoll : public TerminatingPoll { public: PrisonPoll() : TerminatingPoll("prisoner_poll") {} /// Check prisoners are still alive and balanced. void wakeupHook() override; #if !MOBILEAPP // Resets the forkit process object void setForKitProcess(const std::shared_ptr& forKitProc, LOG_CAPTURE_CALLER_DECLARATION) { assertCorrectThread(LOG_PASS_PARENT_CALLER); _forKitProc = forKitProc; if (forKitProc && !_queuedSendMessages.empty()) { for (const auto& msg : _queuedSendMessages) sendMessageToForKit(msg, forKitProc); _queuedSendMessages.clear(); } } void sendMessageToForKit(const std::string& msg, const std::weak_ptr& proc, bool queueIfUnavailable = false) { if (ProcUtil::getThreadId() == getThreadOwner()) { // Speed up sending the message if the request comes from owner thread doSendMessage(msg, proc, queueIfUnavailable); } else { // Put the message in the owner's thread queue to be send later // because WebSocketHandler is not thread safe and otherwise we // should synchronize inside WebSocketHandler. addCallback([this, msg, proc, queueIfUnavailable]{ doSendMessage(msg, proc, queueIfUnavailable); }); } } void sendMessageToForKit(const std::string& msg) { sendMessageToForKit(msg, _forKitProc, false); } void queueMessageToForKit(const std::string& msg) { sendMessageToForKit(msg, _forKitProc, true); } private: std::weak_ptr _forKitProc; std::vector _queuedSendMessages; void doSendMessage(const std::string& msg, const std::weak_ptr& proc, bool queueIfUnavailable) { std::shared_ptr forKitProc = proc.lock(); if (forKitProc) forKitProc->sendTextFrame(msg); else if (queueIfUnavailable) { _queuedSendMessages.push_back(msg); LOG_TRC("queued forkit message: [" << msg << "]. " << _queuedSendMessages.size() << " in queue."); } else LOG_DBG("dropping forkit message: " << msg); } #endif }; } /// This thread listens for and accepts prisoner kit processes. /// And also cleans up and balances the correct number of children. static std::shared_ptr PrisonerPoll; std::shared_ptr getNewChild_Blocks(const std::shared_ptr& destPoll, const std::string& configId, unsigned mobileAppDocId) { (void)mobileAppDocId; const auto startTime = std::chrono::steady_clock::now(); std::unique_lock lock(NewChildrenMutex); #if !MOBILEAPP assert(mobileAppDocId == 0 && "Unexpected to have mobileAppDocId in the non-mobile build"); std::chrono::milliseconds spawnTimeoutMs = ChildSpawnTimeoutMs.load() / 2; if (configId.empty() || SubForKitProcs.contains(configId)) { int numPreSpawn = COOLWSD::NumPreSpawnedChildren; ++numPreSpawn; // Replace the one we'll dispatch just now. LOG_DBG("getNewChild: Rebalancing children of config[" << configId << "] to " << numPreSpawn); rebalanceChildren(configId, numPreSpawn); } else { // configId exists, and no SubForKitProcs for it seen yet, be more generous for startup time. spawnTimeoutMs = std::chrono::milliseconds(CHILD_SPAWN_TIMEOUT_MS); LOG_DBG("getNewChild: awaiting subforkit[" << configId << "], timeout of " << spawnTimeoutMs << "ms"); } const std::chrono::milliseconds timeout = spawnTimeoutMs; LOG_TRC("Waiting for a new child for a max of " << timeout); #else // MOBILEAPP const auto timeout = std::chrono::hours(100); #if DOCS_SHARE_PROCESS assert(mobileAppDocId > 0 && "Unexpected to have no mobileAppDocId in the mobile build"); #endif std::thread([mobileAppDocId] { ProcUtil::setThreadName("lokit_main_" + HexUtil::encodeId(mobileAppDocId, 3)); // Ugly to have that static global PrisonerServerSocketFD, Otoh we know // there is just one COOLWSD object. (Even in real Online.) lokit_main(PrisonerServerSocketFD, COOLWSD::UserInterface, mobileAppDocId); }).detach(); #endif // MOBILEAPP // FIXME: blocks ... // Unfortunately we need to wait after spawning children to avoid bombing the system. // If we fail fast and return, the next document will spawn more children without knowing // there are some on the way already. And if the system is slow already, that wouldn't help. LOG_TRC("Waiting for NewChildrenCV"); if (NewChildrenCV.wait_for(lock, timeout, [configId]() { LOG_TRC("Predicate for NewChildrenCV wait: NewChildren.size()=" << NewChildren.size()); // find a candidate with matching configId auto found = std::find_if( NewChildren.begin(), NewChildren.end(), [configId](const auto& candidate) -> bool { return candidate->getConfigId() == configId; }); const bool candidateMatch = found != NewChildren.end(); // move this candidate into the last position if (candidateMatch) std::swap(*found, NewChildren.back()); return candidateMatch; })) { LOG_TRC("NewChildrenCV wait successful"); std::shared_ptr child = NewChildren.back(); NewChildren.pop_back(); const size_t available = NewChildren.size(); // Release early before moving sockets. lock.unlock(); // Validate before returning. if (child && child->isAlive()) { LOG_DBG("getNewChild: Have " << available << " spare " << (available == 1 ? "child" : "children") << " after popping [" << child->getPid() << "] to return in " << std::chrono::duration_cast( std::chrono::steady_clock::now() - startTime)); // Change ownership now. child->moveSocketFromTo(PrisonerPoll, destPoll); return child; } LOG_WRN("getNewChild: popped dead child, need to find another."); } else { LOG_TRC("NewChildrenCV wait failed"); LOG_WRN("getNewChild: No child available. Sending spawn request to forkit and failing."); } LOG_DBG("getNewChild: Timed out while waiting for new child."); if (!configId.empty()) { auto it = SubForKitProcs.find(configId); if (it != SubForKitProcs.end()) { LOG_WRN("subForKit " << configId << " failed to respond, resetting it"); dropSubForKit(it, /*askToExit=*/false); } } return nullptr; } #ifdef __linux__ #if !MOBILEAPP namespace { class InotifySocket : public Socket { public: InotifySocket(std::chrono::steady_clock::time_point creationTime): Socket(inotify_init1(IN_NONBLOCK), Socket::Type::Unix, creationTime) , _stopOnConfigChange(true) { if (getFD() == -1) { LOG_WRN("Inotify - Failed to start a watcher for the configuration, disabling " "stop_on_config_change"); _stopOnConfigChange = false; return; } watch(COOLWSD_CONFIGDIR); } /// Check for file changes, stop the server if we find any void handlePoll(SocketDisposition &disposition, std::chrono::steady_clock::time_point now, int events) override; int getPollEvents(std::chrono::steady_clock::time_point /* now */, int64_t & /* timeoutMaxMicroS */) override { return POLLIN; } bool watch(std::string_view configFile); private: int _watchedCount = 0; bool _stopOnConfigChange; }; } bool InotifySocket::watch(const std::string_view configFile) { LOG_TRC("Inotify - Attempting to watch " << configFile << ", in addition to current " << _watchedCount << " watched files"); if (getFD() == -1) { LOG_WRN("Inotify - Trying to watch config file " << configFile << " without an inotify file descriptor"); return false; } int watchedStatus; watchedStatus = inotify_add_watch(getFD(), configFile.data(), IN_MODIFY); if (watchedStatus == -1) LOG_WRN("Inotify - Failed to watch config file " << configFile); else _watchedCount++; return watchedStatus != -1; } void InotifySocket::handlePoll(SocketDisposition & /* disposition */, std::chrono::steady_clock::time_point /* now */, int /* events */) { LOG_TRC("InotifyPoll - woken up. Reload on config change: " << _stopOnConfigChange << ", Watching " << _watchedCount << " files"); if (!_stopOnConfigChange) return; char buf[4096]; static_assert(sizeof(buf) >= sizeof(struct inotify_event) + NAME_MAX + 1, "see man 7 inotify"); const struct inotify_event* event; LOG_TRC("InotifyPoll - Checking for config changes..."); while (true) { ssize_t len = read(getFD(), buf, sizeof(buf)); if (len == -1 && errno != EAGAIN) { // Some read error, EAGAIN is when there is no data so let's not warn for it LOG_WRN_SYS("InotifyPoll - Read error when trying to get events"); } else if (len == -1) { LOG_TRC("InotifyPoll - Got to end of data when reading inotify"); } if (len <= 0) break; assert(buf[len - 1] == 0 && "see man 7 inotify"); for (char* ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) { event = reinterpret_cast(ptr); LOG_WRN("InotifyPoll - Config file " << event->name << " was modified, stopping COOLWSD"); SigUtil::requestShutdown(); } } } #endif // if !MOBILEAPP #endif // #ifdef __linux__ #if !MOBILEAPP void ForKitProcWSHandler::handleMessage(const std::vector &data) { LOG_TRC("ForKitProcWSHandler: handling incoming [" << COOLProtocol::getAbbreviatedMessage(data.data(), data.size()) << "]."); const std::string firstLine = COOLProtocol::getFirstLine(data.data(), data.size()); const StringVector tokens = StringVector::tokenize(firstLine.data(), firstLine.size()); if (tokens.startsWith(0, "segfaultcount")) { int segFaultcount = 0; int killedCount = 0; int oomKilledCount = 0; if (COOLProtocol::getNonNegTokenInteger(tokens[0], "segfaultcount", segFaultcount) && COOLProtocol::getNonNegTokenInteger(tokens[1], "killedcount", killedCount) && COOLProtocol::getNonNegTokenInteger(tokens[2], "oomkilledcount", oomKilledCount)) { Admin::instance().addErrorExitCounters(segFaultcount, killedCount, oomKilledCount); if (segFaultcount) { LOG_INF(segFaultcount << " coolkit processes crashed with segmentation fault."); SigUtil::addActivity("coolkit(s) crashed"); UNITWSD_CALL(kitSegfault(segFaultcount)); } if (killedCount) { LOG_INF(killedCount << " coolkit processes killed."); SigUtil::addActivity("coolkit(s) killed"); UNITWSD_CALL(kitKilled(killedCount)); } if (oomKilledCount) { LOG_INF(oomKilledCount << " coolkit processes killed by oom."); SigUtil::addActivity("coolkit(s) killed by oom"); UNITWSD_CALL(kitOomKilled(oomKilledCount)); } } else { LOG_WRN( "ForKitProcWSHandler: Invalid 'segfaultcount' message received. Got:" << firstLine); } } else { LOG_ERR("ForKitProcWSHandler: unknown command: " << tokens[0]); } } #endif COOLWSD::COOLWSD() { } COOLWSD::~COOLWSD() { if (UnitBase::isUnitTesting()) { // We won't have a valid UnitWSD::get() when not testing. UNITWSD_CALL(setWSD(nullptr)); } } #if !MOBILEAPP bool queueMessageToForKit(const std::string& message) { if (!PrisonerPoll) return false; PrisonerPoll->queueMessageToForKit(message); return true; } bool COOLWSD::queueMessageToPrimordialForKit(const std::string& message) { return queueMessageToForKit(message); } void COOLWSD::requestTerminateSpareKits() { // Request existing spare kits to quit, to get replaced with ones that // include the new fonts. if (PrisonerPoll) { PrisonerPoll->addCallback( [] { std::unique_lock lock(NewChildrenMutex); const int count = NewChildren.size(); for (int i = count - 1; i >= 0; --i) NewChildren[i]->requestTermination(); }); } } /// Ask the spare kits of a configuration to exit, then its subforkit. The subforkit is still their /// parent, so it reaps them and removes their jails before it exits itself. static void closeSubForKit(const std::string& configId, const std::shared_ptr& subForKit) { if (!PrisonerPoll) return; // The spare kits are taken now, so no document gets one of them, and the kits of a later // subforkit with the same configuration stay. std::vector> spareKits; { std::unique_lock lock(NewChildrenMutex); for (auto it = NewChildren.begin(); it != NewChildren.end();) { if ((*it)->getConfigId() == configId) { spareKits.push_back(*it); it = NewChildren.erase(it); } else ++it; } } // The sockets belong to the prisoner poll, so the messages are sent from its thread. PrisonerPoll->addCallback( [spareKits = std::move(spareKits), subForKit] { for (const std::shared_ptr& kit : spareKits) kit->close(); subForKit->close(); }); } namespace { /// Runs body in a child process that exits as soon as body returns. /// Answers with the status that child exited with, or -1 if it did not exit normally. int runInThrowawayFork(const char* name, const std::function& body) { Log::preFork(); pid_t pid = fork(); if (!pid) { // Child Log::postFork(); _exit(body()); } // Parent if (pid == -1) { LOG_SYS(name << " fork failed"); return -1; } int wstatus; const int rc = waitpid(pid, &wstatus, 0); if (rc == -1) { LOG_SYS(name << " waitpid failed"); return -1; } if (!WIFEXITED(wstatus)) { LOG_SYS(name << " abnormal termination"); return -1; } int status = WEXITSTATUS(wstatus); LOG_DBG(name << " status: " << std::hex << status << std::dec); return status; } /// True when the kernel here lets a process apply landlock rules. /// Landlock rules cannot be lifted once applied, so this runs in a child. bool testLandlock() { return Landlock::isSupported() && runInThrowawayFork("testLandlock", [] { return Landlock::lock(std::vector()) ? 1 : 0; }) == 1; } bool startWithLandlockJail() { static const bool start = std::getenv("COOL_FORCE_LANDLOCK") != nullptr; return start; } } // namespace // Due to the possibility of enterMountingNS failing at an intermediate stage // after entering a usernamespace, but unable to enter a useful mounting namespace // do a test mount in another separate child whose failure don't affect the parent bool COOLWSD::testMountingNSInFork() { return runInThrowawayFork("testMountingNSInFork", [] { // setupChildRoot does a test bind mount + umount to see if // that fully works so we have a mount namespace here just for // the purposes of that test LOG_DBG("Test moving into user namespace as uid 0 in level 2 " "child"); return JailUtil::enterMountingNS(geteuid(), getegid()) ? 1 : 0; }) == 1; } void COOLWSD::setupChildRoot(const bool UseMountNamespaces) { JailUtil::disableBindMounting(); // Default to assume failure JailUtil::disableMountNamespaces(); #if ENABLE_CHILDROOTS // Do the setup in a fork so we have no other threads running which // disrupt creation of linux namespaces const int status = runInThrowawayFork( "setupChildRoot", [UseMountNamespaces] { int ret = 0; if (UseMountNamespaces) { // setupChildRoot does a test bind mount + umount to see if that fully works // so we have a mount namespace here just for the purposes of that test // First see if it works in (another) throw away child so a successful // NEWUSER, but a failed NEWNS, or an unusable one, doesn't affect this // process. So on failure we can skip the enterMountingNS at this level. const bool childMountWorked = COOLWSD::testMountingNSInFork(); LOG_DBG("Move into user namespace as uid 0"); if (childMountWorked && JailUtil::enterMountingNS(geteuid(), getegid())) JailUtil::enableMountNamespaces(); else { LOG_ERR("creating usernamespace for mount user failed. Jails will be built by " "the coolforkit-caps helper instead, which starts only when it holds " "CAP_CHOWN, CAP_FOWNER and CAP_SYS_CHROOT and is allowed to raise " "privileges as it starts."); } } // Setup the jails. JailUtil::cleanupJails(CleanupChildRoot); JailUtil::setupChildRoot(IsBindMountingEnabled, ChildRoot, SysTemplate); if (JailUtil::isMountNamespacesEnabled()) ret |= (1 << 0); if (JailUtil::isBindMountingEnabled()) ret |= (1 << 1); return ret; }); if (status < 0) return; IsBindMountingEnabled = (status & (1 << 1)); LOG_INF("Using Bind Mounting: " << IsBindMountingEnabled); EnableMountNamespaces = (status & (1 << 0)); LOG_INF("Using Mount Namespaces: " << EnableMountNamespaces); if (IsBindMountingEnabled) JailUtil::enableBindMounting(); if (EnableMountNamespaces) JailUtil::enableMountNamespaces(); if (ConfigUtil::getConfigValue("mount_jail_tree", true)) JailUtil::enableBindMountingConfigured(); else JailUtil::disableBindMountingConfigured(); #else (void) UseMountNamespaces; #endif if (!Util::isMobileApp()) { // The TMPDIR that coolwsd, forkit and anything they spawn inherit. Library code that // honours TMPDIR lands its files here, in the phase before any jail exists. It sits above // the jails, so nothing inside one can see it. Create it after cleanupJails, which // deletes any pre-existing content under ChildRoot. const std::string tmpDir = ChildRoot + "systmp"; FileUtil::createDirectories(tmpDir); LOG_INF("Setting system temporary directory path: " << tmpDir); FileUtil::setSysTempDirectoryPath(tmpDir); // Only implemented and meaningful on *ix. } } #endif #if !MOBILEAPP namespace { /// When the container is started with an arbitrary UID (for example OpenShift /// assigns a random one) there is no matching entry in /etc/passwd, so /// getpwuid() fails and the privileged-user check refuses to start. Add an /// entry mapping the current UID to the 'cool' user. The old container start /// script did this via libnss_wrapper and LD_PRELOAD; appending to /etc/passwd /// directly avoids the preload (which must be set before exec) and drops the /// extra runtime dependency. The image makes /etc/passwd writable for the root /// group, which is the group an arbitrary OpenShift UID runs as. void ensureUserEntry() { const uid_t uid = getuid(); if (getpwuid(uid) != nullptr) return; // Already resolvable, nothing to do. std::ofstream passwd("/etc/passwd", std::ios::app); if (!passwd) { LOG_WRN("No passwd entry for uid " << uid << " and /etc/passwd is not writable; the " "privileged-user check may fail."); return; } passwd << "cool:x:" << uid << ':' << getgid() << "::/opt/cool:/usr/sbin/nologin\n"; LOG_INF("Added a passwd entry mapping uid " << uid << " to the 'cool' user."); } } // namespace #endif #if !MOBILEAPP /// Health-check the locally running coolwsd for the --probe option: connect to /// the loopback interface on the configured port, GET the /livez liveness /// endpoint (see healthchecks.txt), and return EX_OK when the server answers /// HTTP 200. This gives the shell-less, distroless container image a /// self-contained HEALTHCHECK that needs no curl. A container is restarted when /// its health check fails, so this probes liveness, not readiness: a server /// that is merely at capacity stays up, while one whose liveness checks fail /// answers HTTP 500 and gets restarted. /// /// It talks to coolwsd's own socket, so ssl.termination is irrelevant here - it /// describes the upstream proxy this probe deliberately bypasses. /// /// The scheme (http/https) follows ssl.enable, but this separate probe process /// only sees the config file and, under --use-env-vars, extra_params - not the /// ssl.enable override the running server may have received directly on its own /// command line (docker run ... --o:ssl.enable=false), which no HEALTHCHECK can /// observe. So the configured scheme is only a preference: if it does not answer /// we try the other one. A wrong scheme fails the connection or TLS handshake /// instead of returning HTTP 200, so trying both cannot yield a false positive. static int probeRunningServer() { const int port = ClientPortNumber > 0 ? ClientPortNumber : DEFAULT_CLIENT_PORT_NUMBER; const std::string path = COOLWSD::ServiceRoot + "/livez"; // Reach the server over the loopback. net.listen (any/loopback) always // includes the loopback, so only the address family matters: the IPv4 // literal, or the bracketed IPv6 literal for an IPv6-only server. Both are // literals, so the probe needs no name resolution. const std::string host = (ClientPortProto == Socket::Type::IPv6) ? "[::1]" : "127.0.0.1"; // The HTTP client resolves via the async DNS subsystem, which the normal // server only starts later in innerMain(); bring it up just for the probe. net::AsyncDNS::startAsyncDNS(); int exitCode = EX_UNAVAILABLE; std::string error; bool completeResponse = false; // Two attempts share the HEALTHCHECK --timeout budget (10s in the image), so // keep each below half of it. const std::function tryScheme = [&](bool ssl) -> bool { const std::string target = (ssl ? "https://" : "http://") + host + ':' + std::to_string(port) + path; #if ENABLE_SSL if (ssl && !ssl::Manager::isClientContextInitialized()) ssl::Manager::initializeClientContext( std::string(), std::string(), std::string(), DEFAULT_CIPHER_SET, ssl::CertificateVerification::Disabled); #endif completeResponse = false; try { const std::shared_ptr session = ssl ? http::Session::createHttpSsl(host, port) : http::Session::createHttp(host, port); if (!session) error = "could not create a session for " + target; else { const std::shared_ptr response = session->syncRequest(http::Request(path), std::chrono::seconds(4)); if (response && response->state() == http::Response::State::Complete) { // The server answered with a complete HTTP response, so this // scheme is the right one: a non-200 is a real unhealthy signal, // not a reason to try the other scheme. completeResponse = true; if (response->statusCode() == http::StatusCode::OK) { exitCode = EX_OK; return true; } error = target + " returned HTTP " + std::to_string(static_cast(response->statusCode())); } else if (response && response->state() == http::Response::State::Timeout) error = "timed out connecting to " + target; else error = "could not connect to " + target; } } catch (const std::exception& ex) { error = "error probing " + target + ": " + ex.what(); } return false; }; const bool preferSsl = ConfigUtil::isSslEnabled(); #if ENABLE_SSL const bool canSsl = true; #else const bool canSsl = false; #endif // Try the configured scheme first (keeps the normal https deployment quiet). // Only fall back on a transport/handshake failure - i.e. no complete HTTP // response, which is what a scheme mismatch looks like; a connecting-to-http // https probe may even hang to the timeout, so we must not gate this on it. // http is always available; https only in an SSL-enabled build. if (!tryScheme(preferSsl) && !completeResponse) { const bool other = !preferSsl; if (!other || canSsl) tryScheme(other); } net::AsyncDNS::stopAsyncDNS(); if (exitCode != EX_OK) std::cerr << "coolwsd --probe: " << error << std::endl; return exitCode; } #endif // !MOBILEAPP void COOLWSD::innerInitialize(Poco::Util::Application& self) { #if !MOBILEAPP ensureUserEntry(); if (geteuid() == 0 && CheckCoolUser) { throw std::runtime_error("Do not run as root. Please run as cool user."); } #endif Util::setApplicationPath( Poco::Path(Poco::Util::Application::instance().commandPath()).parent().toString()); StartTime = std::chrono::steady_clock::now(); // Initialize the config subsystem. LayeredConfiguration& conf = config(); const auto& defAppConfig = ConfigUtil::getDefaultAppConfig(); // Set default values, in case they are missing from the config file. Poco::AutoPtr defConfig(new ConfigUtil::AppConfigMap(defAppConfig)); conf.addWriteable(defConfig, PRIO_SYSTEM); // Lowest priority #if !MOBILEAPP // Load default configuration files, with name independent // of Poco's view of app-name, from local file if present. // Fallback to the COOLWSD_CONFIGDIR or --config-file path. Poco::Path configPath("coolwsd.xml"); const std::string configFilePath = Poco::Util::Application::findFile(configPath) ? configPath.toString() : ConfigFile; loadConfiguration(configFilePath, PRIO_DEFAULT); // Override any settings passed on the command-line or via environment variables if (UseEnvVarOptions) initializeEnvOptions(); Poco::AutoPtr overrideConfig( new ConfigUtil::AppConfigMap(_overrideSettings)); conf.addWriteable(overrideConfig, PRIO_APPLICATION); // Highest priority #endif // !MOBILEAPP // This caches some oft-used settings and must come after overriding. ConfigUtil::initialize(&config()); #if !MOBILEAPP if (!UnitTestLibrary.empty()) { UnitWSD::defaultConfigure(conf); } // Experimental features. EnableExperimental = ConfigUtil::getConfigValue(conf, "experimental_features", false); EnableAccessibility = ConfigUtil::getConfigValue(conf, "accessibility.enable", Util::isMobileApp()); // Setup user interface mode UserInterface = ConfigUtil::getConfigValue(conf, "user_interface.mode", "default"); if (UserInterface == "compact") UserInterface = "classic"; if (UserInterface == "tabbed") UserInterface = "notebookbar"; if (EnableAccessibility) UserInterface = "notebookbar"; // Load view mode file extensions configuration COOLWSD::ViewModeFileExtensions = ConfigUtil::getConfigValue( conf, "view_mode.file_extensions", ""); // Set the log-level after complete initialization to force maximum details at startup. LogLevel = ConfigUtil::getConfigValue(conf, "logging.level", "trace"); LogDisabledAreas = ConfigUtil::getConfigValue(conf, "logging.disabled_areas", "Socket,WebSocket,Admin"); MostVerboseLogLevelSettableFromClient = ConfigUtil::getConfigValue( conf, "logging.most_verbose_level_settable_from_client", "notice"); LeastVerboseLogLevelSettableFromClient = ConfigUtil::getConfigValue( conf, "logging.least_verbose_level_settable_from_client", "fatal"); setenv("COOL_LOGLEVEL", LogLevel.c_str(), true); setenv("COOL_LOGDISABLED_AREAS", LogDisabledAreas.c_str(), true); if constexpr (!Util::isDebugEnabled()) { const std::string salLog = ConfigUtil::getConfigValue(conf, "logging.lokit_sal_log", "-INFO-WARN"); setenv("SAL_LOG", salLog.c_str(), 0); } #if WASMAPP // In WASM, we want to log to the Log Console. // Disable logging to file to log to stdout and // disable color since this isn't going to the terminal. constexpr bool withColor = false; constexpr bool logToFile = false; constexpr bool logToFileUICmd = false; #else const bool withColor = ConfigUtil::getConfigValue(conf, "logging.color", true) && isatty(fileno(stderr)); if (withColor) { setenv("COOL_LOGCOLOR", "1", true); } const auto logToFile = ConfigUtil::getConfigValue(conf, "logging.file[@enable]", false); std::map logProperties; if (logToFile) { for (std::size_t i = 0;; ++i) { const std::string confPath = "logging.file.property[" + std::to_string(i) + ']'; const std::string confName = config().getString(confPath + "[@name]", ""); if (!confName.empty()) { const std::string value = config().getString(confPath, ""); logProperties.emplace(confName, value); } else if (!config().has(confPath)) { break; } } // Setup the logfile envar for the kit processes. const auto it = logProperties.find("path"); if (it != logProperties.end()) { setenv("COOL_LOGFILE", "1", true); setenv("COOL_LOGFILENAME", it->second.c_str(), true); if (!CleanupOnly) { std::cerr << "\nLogging at " << LogLevel << " level to file: " << it->second.c_str() << std::endl; } } } // Do the same for ui command logging const bool logToFileUICmd = ConfigUtil::getConfigValue(conf, "logging_ui_cmd.file[@enable]", false); std::map logPropertiesUICmd; if (logToFileUICmd) { for (std::size_t i = 0;; ++i) { const std::string confPath = "logging_ui_cmd.file.property[" + std::to_string(i) + ']'; const std::string confName = config().getString(confPath + "[@name]", ""); if (!confName.empty()) { const std::string value = config().getString(confPath, ""); logPropertiesUICmd.emplace(confName, value); } else if (!config().has(confPath)) { break; } } // Setup the logfile envar for the kit processes. const auto it = logPropertiesUICmd.find("path"); if (it != logPropertiesUICmd.end()) { setenv("COOL_LOGFILE_UICMD", "1", true); setenv("COOL_LOGFILENAME_UICMD", it->second.c_str(), true); if (!CleanupOnly) { std::cerr << "\nLogging UI Commands to file: " << it->second.c_str() << std::endl; } } const bool merge = ConfigUtil::getConfigValue(conf, "logging_ui_cmd.merge", true); const bool logEndtime = ConfigUtil::getConfigValue(conf, "logging_ui_cmd.merge_display_end_time", false); if (merge) { setenv("COOL_LOG_UICMD_MERGE", "1", true); } if (logEndtime) { setenv("COOL_LOG_UICMD_END_TIME", "1", true); } } #endif // Log at trace level until we complete the initialization. LogLevelStartup = ConfigUtil::getConfigValue(conf, "logging.level_startup", "trace"); setenv("COOL_LOGLEVEL_STARTUP", LogLevelStartup.c_str(), true); Log::initialize("wsd", LogLevelStartup, withColor, logToFile, logProperties, logToFileUICmd, logPropertiesUICmd); if (LogLevel != LogLevelStartup) { LOG_INF("Setting log-level to [" << LogLevelStartup << "] and delaying setting to [" << LogLevel << "] until after WSD initialization."); } Util::sleepFromEnvIfSet("Coolwsd", "SLEEPFORDEBUGGER"); if (ConfigUtil::getConfigValue(conf, "browser_logging", false)) { LogToken = Util::rng::getHexString(16); } // First log entry. ServerName = config().getString("server_name"); LOG_INF("Initializing coolwsd " << Util::getCoolVersion() << " server [" << ServerName << "]. Experimental features are " << (EnableExperimental ? "enabled." : "disabled.")); std::ostringstream ossConfig; ossConfig << "Loaded config file [" << configFilePath << "] (non-default values):\n"; ossConfig << ConfigUtil::getLoggableConfig(conf); LOG_DBG_S("View mode extensions: [" << COOLWSD::ViewModeFileExtensions << ']'); LoggableConfigEntries = ossConfig.str(); LOG_INF(LoggableConfigEntries); // Set before the unit-test configure hook, which can then replace it. net::Defaults.pongTimeout = std::chrono::seconds(ConfigUtil::getConfigValue( conf, "net.client_pong_timeout_secs", net::DefaultPongTimeout.count())); // Initialize the UnitTest subsystem. if (!UnitWSD::init(UnitWSD::UnitType::Wsd, UnitTestLibrary)) { throw std::runtime_error("Failed to load wsd unit test library."); } UNITWSD_CALL(setWSD(this)); // Allow UT to manipulate before using configuration values. UNITWSD_CALL(configure(conf)); // Trace Event Logging. EnableTraceEventLogging = ConfigUtil::getConfigValue(conf, "trace_event[@enable]", false); if (EnableTraceEventLogging) { const auto traceEventFile = ConfigUtil::getConfigValue( conf, "trace_event.path", COOLWSD_TRACEEVENTFILE); LOG_INF("Trace Event file is " << traceEventFile << "."); TraceEventFile = fopen(traceEventFile.c_str(), "w"); if (TraceEventFile != nullptr) { if (fcntl(fileno(TraceEventFile), F_SETFD, FD_CLOEXEC) == -1) { fclose(TraceEventFile); TraceEventFile = nullptr; } else { fprintf(TraceEventFile, "[\n"); // Output a metadata event that tells that this is the WSD process fprintf(TraceEventFile, "{\"name\":\"process_name\",\"ph\":\"M\",\"args\":{\"name\":\"WSD\"}," "\"pid\":%ld,\"tid\":%ld},\n", ProcUtil::getProcessId(), ProcUtil::getThreadId()); fprintf(TraceEventFile, "{\"name\":\"thread_name\",\"ph\":\"M\",\"args\":{\"name\":\"Main\"}," "\"pid\":%ld,\"tid\":%ld},\n", ProcUtil::getProcessId(), ProcUtil::getThreadId()); } } } // Check deprecated settings. if (ConfigUtil::hasProperty("storage.wopi.reuse_cookies")) LOG_WRN("NOTE: Deprecated config option storage.wopi.reuse_cookies is no longer supported"); COOLWSD::WASMEnabled = ConfigUtil::getConfigValue(conf, "wasm.enable", false); // Get anonymization settings. #if COOLWSD_ANONYMIZE_USER_DATA AnonymizeUserData = true; LOG_INF("Anonymization of user-data is permanently enabled."); #else LOG_INF("Anonymization of user-data is configurable."); const bool haveAnonymizeUserDataConfig = ConfigUtil::getRawConfig(conf, "logging.anonymize.anonymize_user_data", AnonymizeUserData); bool anonymizeFilenames = false; bool anonymizeUsernames = false; if (ConfigUtil::getRawConfig(conf, "logging.anonymize.usernames", anonymizeFilenames) || ConfigUtil::getRawConfig(conf, "logging.anonymize.filenames", anonymizeUsernames)) { LOG_WRN("NOTE: both logging.anonymize.usernames and logging.anonymize.filenames are deprecated and superseded by " "logging.anonymize.anonymize_user_data. Please remove username and filename entries from the config and use only anonymize_user_data."); if (haveAnonymizeUserDataConfig) LOG_WRN("Since logging.anonymize.anonymize_user_data is provided (" << AnonymizeUserData << ") in the config, it will be used."); else { AnonymizeUserData = (anonymizeFilenames || anonymizeUsernames); } } #endif if (AnonymizeUserData && LogLevel == "trace" && !CleanupOnly) { if (ConfigUtil::getConfigValue(conf, "logging.anonymize.allow_logging_user_data", false)) { LOG_WRN("Enabling trace logging while anonymization is enabled due to logging.anonymize.allow_logging_user_data setting. " "This will leak user-data!"); // Disable anonymization as it's useless now. AnonymizeUserData = false; } else { static const char failure[] = "Anonymization and trace-level logging are incompatible. " "Please reduce logging level to debug or lower in coolwsd.xml to prevent leaking sensitive user data."; LOG_FTL(failure); std::cerr << '\n' << failure << std::endl; if constexpr (Util::isDebugEnabled()) { std::cerr << "\nIf you have used 'make run', edit coolwsd.xml and make sure you " "have removed " "'--o:logging.level=trace' from the command line in Makefile.am.\n" << std::endl; } Util::forcedExit(EX_SOFTWARE); } } std::uint64_t anonymizationSalt = 82589933; bool highStrengthAnonymize = false; LOG_INF("Anonymization of user-data is " << (AnonymizeUserData ? "enabled." : "disabled.")); if (AnonymizeUserData) { // Get the salt, if set, otherwise default, and set as envar, so the kits inherit it. anonymizationSalt = ConfigUtil::getConfigValue( conf, "logging.anonymize.anonymization_salt", 82589933); const std::string anonymizationSaltStr = std::to_string(anonymizationSalt); setenv("COOL_ANONYMIZATION_SALT", anonymizationSaltStr.c_str(), true); highStrengthAnonymize = ConfigUtil::getConfigValue( conf, "logging.anonymize.high_strength", false); if (highStrengthAnonymize) { LOG_INF("Using high-strength cryptographic anonymization (PBKDF2-HMAC-SHA512)."); setenv("COOL_ANONYMIZATION_HIGH_STRENGTH", "1", true); } } Anonymizer::initialize(AnonymizeUserData, anonymizationSalt, highStrengthAnonymize); { bool enableWebsocketURP = ConfigUtil::getConfigValue("security.enable_websocket_urp", false); if (enableWebsocketURP) LOG_WRN("NOTE: Deprecated config option security.enable_websocket_urp is enabled. " "This feature is deprecated and will be removed in a future release."); setenv("ENABLE_WEBSOCKET_URP", enableWebsocketURP ? "true" : "false", 1); } { std::string proto = ConfigUtil::getConfigValue(conf, "net.proto", ""); if (Util::iequal(proto, "ipv4")) ClientPortProto = Socket::Type::IPv4; else if (Util::iequal(proto, "ipv6")) ClientPortProto = Socket::Type::IPv6; else if (Util::iequal(proto, "all")) ClientPortProto = Socket::Type::All; else LOG_WRN("Invalid protocol: " << proto); } { std::string listen = ConfigUtil::getConfigValue(conf, "net.listen", ""); if (Util::iequal(listen, "any")) ClientListenAddr = ServerSocket::Type::Public; else if (Util::iequal(listen, "loopback")) ClientListenAddr = ServerSocket::Type::Local; else LOG_WRN("Invalid listen address: " << listen << ". Falling back to default: 'any'" ); } // Prefix for the coolwsd pages; should not end with a '/' ServiceRoot = ConfigUtil::getPathFromConfig("net.service_root"); while (ServiceRoot.length() > 0 && ServiceRoot[ServiceRoot.length() - 1] == '/') ServiceRoot.pop_back(); IsProxyPrefixEnabled = ConfigUtil::getConfigValue(conf, "net.proxy_prefix", false); LOG_INF("SSL support: SSL is " << (ConfigUtil::isSslEnabled() ? "enabled." : "disabled.")); LOG_INF("SSL support: termination is " << (ConfigUtil::isSSLTermination() ? "enabled." : "disabled.")); if (ProbeOnly) { const int probeExitCode = probeRunningServer(); // Not Util::forcedExit(): its non-zero path logs via LOG_FTL, which // always writes to stderr regardless of log level and so cannot be // silenced. This is forcedExit's own tail without that line. Log::shutdown(); std::_Exit(probeExitCode); } std::string allowedLanguages(config().getString("allowed_languages")); setenv("KIT_ALLOWLIST_LANGUAGES", allowedLanguages.c_str(), 1); #endif // !MOBILEAPP #if defined(DEBUG) // Enable if you need more logging from core //setenv("SAL_LOG", "+INFO+WARN", 0); // Enable if you need to see the top left corner of tile that was rendered //setenv("LOK_DEBUG_TILES", "1", 0); #endif if (ConfigUtil::hasProperty("per_document.pdf_resolution_dpi")) LOG_WRN("NOTE: Deprecated config option per_document.pdf_resolution_dpi is no longer supported"); SysTemplate = ConfigUtil::getPathFromConfig("sys_template_path"); if (SysTemplate.empty()) { LOG_FTL("Missing sys_template_path config entry."); throw Poco::Util::MissingOptionException("systemplate"); } ChildRoot = ConfigUtil::getPathFromConfig("child_root_path"); if (ChildRoot.empty()) { LOG_FTL("Missing child_root_path config entry."); throw Poco::Util::MissingOptionException("childroot"); } else { #if !MOBILEAPP if (CleanupOnly) { // Cleanup and exit. JailUtil::cleanupJails(ChildRoot); Util::forcedExit(EX_OK); } #endif if (ChildRoot[ChildRoot.size() - 1] != '/') ChildRoot += '/'; #if CODE_COVERAGE ::setenv("BASE_CHILD_ROOT", Poco::Path(ChildRoot).absolute().toString().c_str(), 1); #endif // We need to cleanup other people's expired jails CleanupChildRoot = ChildRoot; // Encode the process id into the path for parallel re-use of jails/ ChildRoot += std::to_string(ProcUtil::getProcessId()) + '-' + Util::rng::getHexString(8) + '/'; LOG_DBG("Normalizing childroot: " << ChildRoot); ChildRoot = Poco::Path(ChildRoot).makeDirectory().makeAbsolute().toString(); LOG_DBG("Childroot: " << ChildRoot); } #if !MOBILEAPP // Copy and serialize the config into XML to pass to forkit. KitXmlConfig.reset(new Poco::Util::XMLConfiguration); for (const auto& pair : defAppConfig) { try { KitXmlConfig->setString(pair.first, config().getRawString(pair.first)); } catch (const std::exception&) { // Nothing to do. } } // Fixup some config entries to match out decisions/overrides. KitXmlConfig->setBool("ssl.enable", ConfigUtil::isSslEnabled()); KitXmlConfig->setBool("ssl.termination", ConfigUtil::isSSLTermination()); // We don't pass the config via command-line // to avoid dealing with escaping and other traps. std::ostringstream oss; KitXmlConfig->save(oss); setenv("COOL_CONFIG", oss.str().c_str(), true); // For some reason I can't get at this setting in ChildSession::loKitCallback(). std::string fontsMissingHandling = ConfigUtil::getString("fonts_missing.handling", "log"); setenv("FONTS_MISSING_HANDLING", fontsMissingHandling.c_str(), 1); IsBindMountingEnabled = ConfigUtil::getConfigValue(conf, "mount_jail_tree", true); #if CODE_COVERAGE // Code coverage is not supported with bind-mounting. if (IsBindMountingEnabled) { LOG_WRN("Mounting is not compatible with code-coverage. Disabling."); IsBindMountingEnabled = false; } #endif // CODE_COVERAGE // Setup the jails. bool UseMountNamespaces = true; RequireLandlock = startWithLandlockJail(); NoCapsForKit = startWithLandlockJail() || Util::isKitInProcess() || !ConfigUtil::getConfigValue(conf, "security.capabilities", true); if (NoCapsForKit && UseMountNamespaces) { // With NoCapsForKit we don't chroot. If Linux namespaces are available, we could // chroot without capabilities, but the richdocumentscode AppImage layout isn't // compatible with the systemplate expectations for setting up the chroot so // disable MountNamespaces in NoCapsForKit mode for now. if (!Landlock::isSupported()) LOG_WRN("MountNamespaces is not compatible with NoCapsForKit. Disabling."); UseMountNamespaces = false; } if (NoCapsForKit && IsBindMountingEnabled) { // With NoCapsForKit there is no jail tree to bind-mount into. LOG_INF("Bind-mounting is not used with NoCapsForKit. Disabling."); IsBindMountingEnabled = false; } LOG_INF("Creating childroot: [" << ChildRoot << "] with" << (UseMountNamespaces ? "" : "out") << " mount-namespaces"); setupChildRoot(UseMountNamespaces); LOG_DBG("FileServerRoot before config: " << FileServerRoot); FileServerRoot = ConfigUtil::getPathFromConfig("file_server_root_path"); LOG_DBG("FileServerRoot after config: " << FileServerRoot); //creating quarantine directory if (ConfigUtil::getConfigValue(conf, "quarantine_files[@enable]", false)) { std::string path = Util::trimmed(ConfigUtil::getPathFromConfig("quarantine_files.path")); LOG_INF("Quarantine path is set to [" << path << "] in config"); if (path.empty()) { LOG_WRN("Quarantining is enabled via quarantine_files config, but no path is set in " "quarantine_files.path. Disabling quarantine"); } else { if (path[path.size() - 1] != '/') path += '/'; if (path[0] != '/') LOG_WRN("Quarantine path is relative. Please use an absolute path for better " "reliability"); LOG_DBG("Initializing quarantine at [" << path << ']'); Quarantine::initialize(path); } } else { LOG_INF("Quarantine is disabled in config"); } { // creating cache directory std::string path = Util::trimmed(ConfigUtil::getPathFromConfig("cache_files.path")); LOG_INF("Cache path is set to [" << path << "] in config"); if (path.empty()) { LOG_WRN("No cache path is set in cache_files.path. Disabling cache"); } else { Poco::File p(path); try { LOG_TRC("Creating cache directory [" << path << ']'); p.createDirectories(); LOG_DBG("Created cache directory [" << path << ']'); } catch (const std::exception&) { LOG_WRN("Failed to create cache directory [" << path << "]"); } if (FileUtil::Stat(path).exists()) Cache::initialize(path); } } NumPreSpawnedChildren = ConfigUtil::getConfigValue(conf, "num_prespawn_children", 1); if (NumPreSpawnedChildren < 1) { LOG_WRN("Invalid num_prespawn_children in config (" << NumPreSpawnedChildren << "). Resetting to 1."); NumPreSpawnedChildren = 1; } LOG_INF("NumPreSpawnedChildren set to " << NumPreSpawnedChildren << '.'); FileUtil::registerFileSystemForDiskSpaceChecks(ChildRoot); int threads = std::max(std::thread::hardware_concurrency(), 1); int maxConcurrency = ConfigUtil::getConfigValue(conf, "per_document.max_concurrency", 4); if (maxConcurrency > 16) { LOG_WRN("Using a large number of threads for every document puts pressure on " "the scheduler, and consumes memory, while providing marginal gains " "consider lowering max_concurrency from " << maxConcurrency); } if (maxConcurrency > threads) { LOG_ERR("Setting concurrency above the number of physical " "threads yields extra latency and memory usage for no benefit. " "Clamping " << maxConcurrency << " to " << threads << " threads."); maxConcurrency = threads; } if (maxConcurrency > 0) { setenv("MAX_CONCURRENCY", std::to_string(maxConcurrency).c_str(), 1); } LOG_INF("MAX_CONCURRENCY set to " << maxConcurrency << '.'); // It is worth avoiding configuring with a large number of under-weight // containers / VMs - better to have fewer, stronger ones. if (threads < 4) { LOG_WRN("Fewer threads than recommended. Having at least four threads for " "provides significant parallelism that can be used for burst " "compression of newly visible document pages, giving lower latency."); HardwareResourceWarning = "lowresources"; } #endif const auto redlining = ConfigUtil::getConfigValue(conf, "per_document.redlining_as_comments", false); if (!redlining) { setenv("DISABLE_REDLINE", "1", 1); LOG_INF("DISABLE_REDLINE set"); } const std::string ctlNumerals = ConfigUtil::getConfigValue(conf, "per_document.ctl_numerals", "arabic"); if (ctlNumerals != "arabic") { if (ctlNumerals == "hindi" || ctlNumerals == "system" || ctlNumerals == "context") { setenv("COOL_CTL_NUMERALS", ctlNumerals.c_str(), 1); LOG_INF("COOL_CTL_NUMERALS set to [" << ctlNumerals << ']'); } else { LOG_ERR("Invalid per_document.ctl_numerals value [" << ctlNumerals << "], expected one of: arabic, hindi, system, context. Using arabic."); } } // Otherwise we profile the soft-device at jail creation time. setenv("SAL_DISABLE_OPENCL", "true", 1); // Disable getting the OS print queue and default printer setenv("SAL_DISABLE_PRINTERLIST", "true", 1); setenv("SAL_DISABLE_DEFAULTPRINTER", "true", 1); // Disable fsync - we're a state-less container setenv("SAL_DISABLE_FSYNC", "true", 1); // Staticize our configuration to increase sharing setenv("SAL_CONFIG_STATICIZE", "true", 1); // Log the connection and document limits. #if ENABLE_WELCOME_MESSAGE if (ConfigUtil::getConfigValue(conf, "home_mode.enable", false)) { COOLWSD::MaxConnections = 20; COOLWSD::MaxDocuments = 10; } else { conf.setString("welcome.enable", "true"); COOLWSD::MaxConnections = MAX_CONNECTIONS; COOLWSD::MaxDocuments = MAX_DOCUMENTS; } #else { COOLWSD::MaxConnections = MAX_CONNECTIONS; COOLWSD::MaxDocuments = MAX_DOCUMENTS; } #endif LOG_DBG("net::Defaults: Socket[inactivityTimeout " << net::Defaults.inactivityTimeout << ", maxExtConnections " << net::Defaults.maxExtConnections << ", pongTimeout " << net::Defaults.pongTimeout << ']'); #if !MOBILEAPP NoSeccomp = Util::isKitInProcess() || !ConfigUtil::getConfigValue(conf, "security.seccomp", true); NoCapsForKit = startWithLandlockJail() || Util::isKitInProcess() || !ConfigUtil::getConfigValue(conf, "security.capabilities", true); AdminEnabled = ConfigUtil::getConfigValue(conf, "admin_console.enable", true); IndirectionServerEnabled = !ConfigUtil::getConfigValue(conf, "indirection_endpoint.url", "").empty(); #if ENABLE_DEBUG if (Util::isKitInProcess()) SingleKit = true; #endif if (!NoCapsForKit && !EnableMountNamespaces) { LandlockAvailable = testLandlock(); LOG_INF("Landlock available as a fallback jail: " << LandlockAvailable); } #endif #if !MOBILEAPP // The remote sentence checkers and the translation providers. The kit // reads them from the environment, one cluster of variables per block. // The apps have no server configuration: their services are set up in // the settings dialog alone, and nothing here must look like an // administrator's choice to the kit. const auto exportKitEnvBlock = [&conf](const std::string& block, const char* prefix, const std::vector>& keys) { for (const std::pair& key : keys) { const std::string name = std::string(prefix) + '_' + key.second; const std::string path = block + '.' + key.first; // The booleans go as "true"/"false", whatever the file spells. if (path.ends_with(".enabled") || path.ends_with(".ssl_verification") || path.ends_with(".allow_user_settings")) { const bool value = ConfigUtil::getConfigValue(conf, path, false); setenv(name.c_str(), value ? "true" : "false", 1); } else { const std::string value = ConfigUtil::getConfigValue(conf, path, ""); setenv(name.c_str(), value.c_str(), 1); } } }; const std::vector> serverKeys = { { "enabled", "ENABLED" }, { "base_url", "BASEURL" }, { "user_name", "USERNAME" }, { "api_key", "APIKEY" }, { "ssl_verification", "SSL_VERIFICATION" }, { "allow_user_settings", "ALLOW_USER_SETTINGS" }, }; exportKitEnvBlock("languagetool", "LANGUAGETOOL", serverKeys); // The Duden Korrekturserver takes an API token, not an account name. exportKitEnvBlock("duden", "DUDEN", { { "enabled", "ENABLED" }, { "base_url", "BASEURL" }, { "api_key", "APIKEY" }, { "ssl_verification", "SSL_VERIFICATION" }, { "allow_user_settings", "ALLOW_USER_SETTINGS" }, { "spellchecking_level", "SPELLCHECKING_LEVEL" }, { "orthography_standard", "ORTHOGRAPHY_STANDARD" }, { "message_level", "MESSAGE_LEVEL" }, { "property_sets", "PROPERTY_SETS" }, { "dictionaries", "DICTIONARIES" } }); exportKitEnvBlock("grammalecte", "GRAMMALECTE", { { "enabled", "ENABLED" }, { "base_url", "BASEURL" }, { "ssl_verification", "SSL_VERIFICATION" }, { "allow_user_settings", "ALLOW_USER_SETTINGS" } }); // The Duden Korrekturserver used to be driven through the languagetool // block with rest_protocol set to "duden". Such a file still works: its // values, the switch among them, go to the Duden checker and // LanguageTool stays off. if (ConfigUtil::getConfigValue(conf, "languagetool.rest_protocol", "") == "duden") { LOG_WRN("languagetool.rest_protocol=duden is deprecated; move the settings to the " "duden block of coolwsd.xml"); if (!ConfigUtil::getConfigValue(conf, "duden.enabled", false)) { for (const char* key : { "ENABLED", "BASEURL", "APIKEY", "SSL_VERIFICATION", "ALLOW_USER_SETTINGS" }) { const std::string from = std::string("LANGUAGETOOL_") + key; const std::string to = std::string("DUDEN_") + key; if (const char* value = getenv(from.c_str())) setenv(to.c_str(), value, 1); } } setenv("LANGUAGETOOL_ENABLED", "false", 1); } // The translation providers, the same way: the apps have no server // configuration, and their users set up a provider in the settings // dialog alone. exportKitEnvBlock("translation", "TRANSLATION", { { "provider", "PROVIDER" } }); exportKitEnvBlock("deepl", "DEEPL", { { "enabled", "ENABLED" }, { "api_url", "API_URL" }, { "auth_key", "AUTH_KEY" }, { "allow_user_settings", "ALLOW_USER_SETTINGS" } }); exportKitEnvBlock("libretranslate", "LIBRETRANSLATE", { { "enabled", "ENABLED" }, { "base_url", "BASE_URL" }, { "api_key", "API_KEY" }, { "ssl_verification", "SSL_VERIFICATION" }, { "allow_user_settings", "ALLOW_USER_SETTINGS" } }); #endif #if !MOBILEAPP const std::string helpUrl = ConfigUtil::getConfigValue(conf, "help_url", HELP_URL); setenv("KIT_HELP_URL", helpUrl.c_str(), 1); #else // On mobile UI there should be no tunnelled dialogs. But if there are some, by mistake, // at least they should not have a non-working Help button. setenv("KIT_HELP_URL", "", 1); #endif if constexpr (ConfigUtil::isSupportKeyEnabled()) { const std::string supportKeyString = ConfigUtil::getConfigValue(conf, "support_key", ""); if (supportKeyString.empty()) { LOG_WRN("Support key not set, please use 'coolconfig set-support-key'."); std::cerr << "Support key not set, please use 'coolconfig set-support-key'." << std::endl; COOLWSD::OverrideWatermark = "Unsupported, the support key is missing."; } else { SupportKey key(supportKeyString); if (!key.verify()) { LOG_WRN("Invalid support key, please use 'coolconfig set-support-key'."); std::cerr << "Invalid support key, please use 'coolconfig set-support-key'." << std::endl; COOLWSD::OverrideWatermark = "Unsupported, the support key is invalid."; } else { int validDays = key.validDaysRemaining(); if (validDays <= 0) { LOG_WRN("Your support key has expired, please ask for a new one, and use 'coolconfig set-support-key'."); std::cerr << "Your support key has expired, please ask for a new one, and use 'coolconfig set-support-key'." << std::endl; COOLWSD::OverrideWatermark = "Unsupported, the support key has expired."; } else { LOG_INF("Your support key is valid for " << validDays << " days"); COOLWSD::MaxConnections = 1000; COOLWSD::MaxDocuments = 200; COOLWSD::OverrideWatermark.clear(); } } } } if (COOLWSD::MaxConnections < 3) { LOG_ERR("MAX_CONNECTIONS must be at least 3"); COOLWSD::MaxConnections = 3; } if (COOLWSD::MaxDocuments > COOLWSD::MaxConnections) { LOG_ERR("MAX_DOCUMENTS cannot be bigger than MAX_CONNECTIONS"); COOLWSD::MaxDocuments = COOLWSD::MaxConnections; } #if !WASMAPP && !defined(_WIN32) struct rlimit rlim; if (::getrlimit(RLIMIT_NOFILE, &rlim) == 0) { LOG_INF("Maximum file descriptor supported by the system: " << rlim.rlim_cur - 1); // 4 fds per document are used for client connection, Kit process communication, and // a wakeup pipe with 2 fds. 32 fds (i.e. 8 documents) are reserved. LOG_INF("Maximum number of open documents supported by the system: " << rlim.rlim_cur / 4 - 8); } else LOG_SYS("Failed to get RLIMIT_NOFILE"); #endif LOG_INF("Maximum concurrent open Documents limit: " << COOLWSD::MaxDocuments); LOG_INF("Maximum concurrent client Connections limit: " << COOLWSD::MaxConnections); COOLWSD::NumConnections = 0; // Command Tracing. if (ConfigUtil::getConfigValue(conf, "trace[@enable]", false)) { const auto path = ConfigUtil::getConfigValue(conf, "trace.path", ""); const auto recordOutgoing = ConfigUtil::getConfigValue(conf, "trace.outgoing.record", false); std::vector filters; for (size_t i = 0; ; ++i) { const std::string confPath = "trace.filter.message[" + std::to_string(i) + ']'; std::string regex = config().getString(confPath, ""); if (!regex.empty()) { filters.push_back(std::move(regex)); } else if (!config().has(confPath)) { break; } } const auto compress = ConfigUtil::getConfigValue(conf, "trace.path[@compress]", false); const auto takeSnapshot = ConfigUtil::getConfigValue(conf, "trace.path[@snapshot]", false); TraceDumper = std::make_unique(path, recordOutgoing, compress, takeSnapshot, filters); } setLokitEnvironmentVariables(conf); #if !MOBILEAPP SavedClipboards = std::make_unique(); LOG_TRC("Initialize FileServerRequestHandler"); COOLWSD::FileRequestHandler = std::make_unique(COOLWSD::FileServerRoot); #endif COOLWSDServer::WebServerPoll = std::make_unique("websrv_poll"); #if !MOBILEAPP net::AsyncDNS::startAsyncDNS(); #if ENABLE_SSL // Generate a self-signed certificate when SSL is enabled, none is already // configured, and generation hasn't been disabled. This replaces the // openssl command-line invocation in the old container start script. It must // run before StorageConnectionManager::initialize() below, which is the // first consumer of the SSL cert/key/CA paths. if (ConfigUtil::isSslEnabled() && !std::getenv("DONT_GEN_SSL_CERT")) { const std::string certPath = ConfigUtil::getPathFromConfig("ssl.cert_file_path"); if (certPath.empty() || !Poco::File(certPath).exists()) { const std::string sslDir = "/tmp/ssl"; const char* certDomain = std::getenv("cert_domain"); if (ssl::generateSelfSignedCert(sslDir, certDomain ? certDomain : "localhost")) { config().setString("ssl.cert_file_path", sslDir + "/cert.pem"); config().setString("ssl.key_file_path", sslDir + "/privkey.pem"); config().setString("ssl.ca_file_path", sslDir + "/cert.pem"); } } } #endif LOG_TRC("Initialize StorageConnectionManager"); StorageConnectionManager::initialize(); LOG_TRC("Initialize Admin"); Admin::initialize(); LOG_TRC("Initialize RemoteDocumentBroker"); RemoteDocumentBroker::initialize(); #endif PrisonerPoll = std::make_unique(); COOLWSDServer::Instance = std::make_unique(); LOG_TRC("Initialize StorageBase"); StorageBase::initialize(); #if !MOBILEAPP #ifdef __linux__ // Check for smaps_rollup bug where rewinding and rereading gives // bogus doubled results if (FILE* fp = fopen("/proc/self/smaps_rollup", "r")) { std::size_t memoryDirty1 = ProcUtil::getPssAndDirtyFromSMaps(fp).second; (void)ProcUtil::getPssAndDirtyFromSMaps(fp); // interleave another rewind+read to margin std::size_t memoryDirty2 = ProcUtil::getPssAndDirtyFromSMaps(fp).second; LOG_TRC("Comparing smaps_rollup read and rewind+read: " << memoryDirty1 << " vs " << memoryDirty2); if (memoryDirty2 >= memoryDirty1 * 2) { // Believed to be fixed in >= v4.19, bug seen in 4.15.0 and not in 6.5.10 // https://github.com/torvalds/linux/commit/258f669e7e88c18edbc23fe5ce00a476b924551f LOG_WRN("Reading smaps_rollup twice reports Private_Dirty doubled, smaps_rollup is unreliable on this kernel"); setenv("COOL_DISABLE_SMAPS_ROLLUP", "1", true); } fclose(fp); } #endif ServerApplication::initialize(self); DocProcSettings docProcSettings; docProcSettings.setLimitVirtMemMb( ConfigUtil::getConfigValue("per_document.limit_virt_mem_mb", 0)); docProcSettings.setLimitStackMemKb( ConfigUtil::getConfigValue("per_document.limit_stack_mem_kb", 0)); docProcSettings.setLimitFileSizeMb( ConfigUtil::getConfigValue("per_document.limit_file_size_mb", 0)); docProcSettings.setLimitNumberOpenFiles( ConfigUtil::getConfigValue("per_document.limit_num_open_files", 0)); if (const int nofile = docProcSettings.getLimitNumberOpenFiles(); nofile > 0 && nofile < Rlimit::MinRequiredOpenFiles) { LOG_WRN("per_document.limit_num_open_files is " << nofile << ", below the " << Rlimit::MinRequiredOpenFiles << " files the kit needs at steady state. Loading documents may fail altogether"); } DocCleanupSettings &docCleanupSettings = docProcSettings.getCleanupSettings(); docCleanupSettings.setEnable( ConfigUtil::getConfigValue("per_document.cleanup[@enable]", true)); docCleanupSettings.setCleanupInterval( ConfigUtil::getConfigValue("per_document.cleanup.cleanup_interval_ms", 10000)); docCleanupSettings.setBadBehaviorPeriod( ConfigUtil::getConfigValue("per_document.cleanup.bad_behavior_period_secs", 60)); docCleanupSettings.setIdleTime( ConfigUtil::getConfigValue("per_document.cleanup.idle_time_secs", 300)); docCleanupSettings.setLimitDirtyMem( ConfigUtil::getConfigValue("per_document.cleanup.limit_dirty_mem_mb", 3072)); docCleanupSettings.setLimitCpu( ConfigUtil::getConfigValue("per_document.cleanup.limit_cpu_per", 85)); docCleanupSettings.setLostKitGracePeriod( ConfigUtil::getConfigValue("per_document.cleanup.lost_kit_grace_period_secs", 120)); Admin::instance().setDefDocProcSettings(docProcSettings, false); #else (void) self; #endif } void COOLWSD::setLokitEnvironmentVariables(const Poco::Util::LayeredConfiguration& conf) { #if MOBILEAPP // The apps run the engine on the user's own machine, as the user, so every host the user // can reach is a legitimate external data source, as in soffice. The allow-list below is // for a shared server, whose kit must not fetch from the server's network on a document's // behalf, so the apps set none and the engine's host filter stays off. (void) conf; #else // Allowed hosts for being external data source in the documents std::vector lokAllowedHosts; appendAllowedHostsFrom(conf, "net.lok_allow", lokAllowedHosts); // For backward compatibility post_allow hosts are also allowed bool postAllowed = conf.getBool("net.post_allow[@allow]", false); if (postAllowed) appendAllowedHostsFrom(conf, "net.post_allow", lokAllowedHosts); // For backward compatibility wopi hosts are also allowed bool wopiAllowed = conf.getBool("storage.wopi[@allow]", false); if (wopiAllowed) { appendAllowedHostsFrom(conf, "storage.wopi", lokAllowedHosts, /*onlyAllowed=*/true); appendAllowedAliasGroups(conf, lokAllowedHosts); } if (lokAllowedHosts.size()) { // One entry per line; the kit tells CIDR networks and regular // expressions apart itself. std::string allowlist; for (const std::string& host : lokAllowedHosts) { if (!Util::IpNetwork::parse(host) && !RegexUtil::isRegexValid(host)) { LOG_ERR("Invalid regular expression for allowed host: \"" << host << "\""); continue; } if (!allowlist.empty()) allowlist += '\n'; allowlist += host; } setenv("KIT_HOST_ALLOWLIST", allowlist.c_str(), true); if (!ConfigUtil::getConfigValue(conf, "ssl.ssl_verification", true)) { // also disable host verification for allowed hosts ::setenv("KIT_HOST_ALLOWLIST_EXEMPT_VERIFY_HOST", "1", true); } } setenv("KIT_ALLOWED_EXTREF_PATHS", "", true); #endif } void COOLWSD::initializeSSL() { #if ENABLE_SSL if (!ConfigUtil::isSslEnabled()) return; const std::string ssl_cert_file_path = ConfigUtil::getPathFromConfig("ssl.cert_file_path"); LOG_INF("SSL Cert file: " << ssl_cert_file_path); const std::string ssl_key_file_path = ConfigUtil::getPathFromConfig("ssl.key_file_path"); LOG_INF("SSL Key file: " << ssl_key_file_path); const std::string ssl_ca_file_path = ConfigUtil::getPathFromConfig("ssl.ca_file_path"); LOG_INF("SSL CA file: " << ssl_ca_file_path); std::string ssl_cipher_list = config().getString("ssl.cipher_list", ""); if (ssl_cipher_list.empty()) ssl_cipher_list = DEFAULT_CIPHER_SET; LOG_INF("SSL Cipher list: " << ssl_cipher_list); const std::string ssl_min_protocol_version = config().getString("ssl.min_protocol_version", "TLSv1.2"); LOG_INF("SSL minimum protocol version: " << ssl_min_protocol_version); // Initialize the non-blocking server socket SSL context. ssl::Manager::initializeServerContext(ssl_cert_file_path, ssl_key_file_path, ssl_ca_file_path, ssl_cipher_list, ssl::CertificateVerification::Disabled, ssl_min_protocol_version); if (!ssl::Manager::isServerContextInitialized()) LOG_ERR("Failed to initialize Server SSL."); else { LOG_INF("Initialized Server SSL."); SigUtil::addActivity("initialized SSL"); } #else LOG_INF("SSL is unavailable in this build."); #endif } void COOLWSD::defineOptions(Poco::Util::OptionSet& optionSet) { if (Util::isMobileApp()) return; ServerApplication::defineOptions(optionSet); optionSet.addOption(Option("help", "", "Display help information on command line arguments.") .required(false) .repeatable(false)); optionSet.addOption(Option("version-hash", "", "Display product version-hash information and exit.") .required(false) .repeatable(false)); optionSet.addOption(Option("version", "", "Display version and hash information.") .required(false) .repeatable(false)); optionSet.addOption(Option("cleanup", "", "Cleanup jails and other temporary data and exit.") .required(false) .repeatable(false)); optionSet.addOption(Option("probe", "", "Health-check the locally running coolwsd over its " "configured port and exit 0 if healthy, non-zero otherwise.") .required(false) .repeatable(false)); optionSet.addOption(Option("port", "", "Port number to listen to (default: " + std::to_string(DEFAULT_CLIENT_PORT_NUMBER) + "),") .required(false) .repeatable(false) .argument("port_number")); if constexpr (Util::isDebugEnabled()) { optionSet.addOption(Option("find-free-port", "", "Find a free port to listen on, starting from the default.") .required(false) .repeatable(false)); } optionSet.addOption(Option("disable-ssl", "", "Disable SSL security layer.") .required(false) .repeatable(false)); optionSet.addOption(Option("disable-cool-user-checking", "", "Don't check whether coolwsd is running under the user 'cool'. NOTE: This is insecure, use only when you know what you are doing!") .required(false) .repeatable(false)); optionSet.addOption(Option("override", "o", "Override any setting by providing full xmlpath=value.") .required(false) .repeatable(true) .argument("xmlpath")); optionSet.addOption(Option("config-file", "", "Override configuration file path.") .required(false) .repeatable(false) .argument("path")); optionSet.addOption(Option("lo-template-path", "", "Override the COKit core installation directory path.") .required(false) .repeatable(false) .argument("path")); optionSet.addOption(Option("unattended", "", "Unattended run, won't wait for a debugger on faulting.") .required(false) .repeatable(false)); optionSet.addOption(Option("signal", "", "Send signal SIGUSR2 to parent process when server is ready to accept connections") .required(false) .repeatable(false)); optionSet.addOption(Option("use-env-vars", "", "Use the environment variables defined on " "https://sdk.collaboraonline.com/docs/installation/" "CODE_Docker_image.html#setting-the-application-configuration-" "dynamically-via-environment-variables to set options. " "'extra_params' is appended to the command line by this option; " "'DONT_GEN_SSL_CERT' and 'cert_domain' control certificate " "generation independently of this option.") .required(false) .repeatable(false)); if constexpr (Util::isDebugEnabled()) { optionSet.addOption(Option("unitlib", "", "Unit testing library path.") .required(false) .repeatable(false) .argument("unitlib")); optionSet.addOption(Option("careerspan", "", "How many seconds to run.") .required(false) .repeatable(false) .argument("seconds")); optionSet.addOption(Option("singlekit", "", "Spawn one CollaboraOffice kit.") .required(false) .repeatable(false)); optionSet.addOption( Option("forcecaching", "", "Force HTML & asset caching even in debug mode: accelerates cypress.") .required(false) .repeatable(false)); } } void COOLWSD::handleOption(const std::string& optionName, const std::string& value) { #if !MOBILEAPP ServerApplication::handleOption(optionName, value); if (optionName == "help") { displayHelp(); Util::forcedExit(EX_OK); } else if (optionName == "version-hash") { std::cout << Util::getCoolVersionHash() << std::endl; Util::forcedExit(EX_OK); } else if (optionName == "version") ; // ignore for compatibility else if (optionName == "cleanup") CleanupOnly = true; // Flag for later as we need the config. else if (optionName == "probe") { ProbeOnly = true; // Flag for later as we need the config (port + ssl). // A probe must stay silent: it must neither write to the running // server's log file nor print the usual startup log lines to stderr. // Override logging to none before the config is read. _overrideSettings["logging.file[@enable]"] = "false"; _overrideSettings["logging_ui_cmd.file[@enable]"] = "false"; _overrideSettings["logging.level"] = "none"; _overrideSettings["logging.level_startup"] = "none"; } else if (optionName == "port") ClientPortNumber = NumUtil::stoi(value); #if ENABLE_DEBUG else if (optionName == "find-free-port") FindFreePort = true; #endif else if (optionName == "disable-ssl") _overrideSettings["ssl.enable"] = "false"; else if (optionName == "disable-cool-user-checking") CheckCoolUser = false; else if (optionName == "override") { std::string optName; std::string optValue; if (!COOLProtocol::parseNameValuePair(value, optName, optValue)) throw Poco::Util::InvalidArgumentException("--o:" + value + " is not xmlpath=value"); _overrideSettings[optName] = std::move(optValue); } else if (optionName == "config-file") ConfigFile = value; else if (optionName == "lo-template-path") LoTemplate = value; else if (optionName == "signal") SignalParent = true; else if (optionName == "use-env-vars") UseEnvVarOptions = true; #if ENABLE_DEBUG else if (optionName == "unitlib") UnitTestLibrary = value; else if (optionName == "unattended") { UnattendedRun = true; SigUtil::setUnattended(); } else if (optionName == "careerspan") careerSpanMs = std::chrono::seconds(NumUtil::stoi(value)); // Convert second to ms else if (optionName == "singlekit") { SingleKit = true; NumPreSpawnedChildren = 1; } else if (optionName == "forcecaching") ForceCaching = true; #endif #else (void) optionName; (void) value; #endif } #if !MOBILEAPP void COOLWSD::initializeEnvOptions() { int n = 0; char* aliasGroup; while ((aliasGroup = std::getenv(("aliasgroup" + std::to_string(n + 1)).c_str())) != nullptr) { bool first = true; std::istringstream aliasGroupStream; aliasGroupStream.str(aliasGroup); int j = 0; for (std::string alias; std::getline(aliasGroupStream, alias, ',');) { if (first) { const std::string path = "storage.wopi.alias_groups.group[" + std::to_string(n) + "].host"; _overrideSettings[path] = alias; _overrideSettings[path + "[@allow]"] = "true"; first = false; } else { _overrideSettings["storage.wopi.alias_groups.group[" + std::to_string(n) + "].alias[" + std::to_string(j) + ']'] = alias; j++; } } n++; } if (n >= 1) { _overrideSettings["alias_groups[@mode]"] = "groups"; } char* optionValue; if ((optionValue = std::getenv("username")) != nullptr) _overrideSettings["admin_console.username"] = optionValue; if ((optionValue = std::getenv("password")) != nullptr) _overrideSettings["admin_console.password"] = optionValue; if ((optionValue = std::getenv("server_name")) != nullptr) _overrideSettings["server_name"] = optionValue; if ((optionValue = std::getenv("dictionaries")) != nullptr) _overrideSettings["allowed_languages"] = optionValue; if ((optionValue = std::getenv("remoteconfigurl")) != nullptr) _overrideSettings["remote_config.remote_url"] = optionValue; if ((optionValue = std::getenv("content_security_policy")) != nullptr) _overrideSettings["net.content_security_policy"] = optionValue; } void COOLWSD::displayHelp() { Poco::Util::HelpFormatter helpFormatter(options()); helpFormatter.setCommand(commandName()); helpFormatter.setUsage("OPTIONS"); helpFormatter.setHeader("Collabora Online WebSocket server."); helpFormatter.format(std::cout); } bool COOLWSD::checkAndRestoreForKit() { // clang issues warning for WIF*() macro usages below: // "equality comparison with extraneous parentheses [-Werror,-Wparentheses-equality]" // https://bugs.llvm.org/show_bug.cgi?id=22949 #if defined __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wparentheses-equality" #endif if (ForKitProcId == -1) { // Fire the ForKit process for the first time. if (!SigUtil::getShutdownRequestFlag() && !createForKit()) { // Should never fail. LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn coolforkit."); SigUtil::requestShutdown(); } } if (Util::isKitInProcess()) return true; int status; const pid_t pid = waitpid(ForKitProcId, &status, WUNTRACED | WNOHANG); if (pid > 0) { if (pid == ForKitProcId) { if (WIFEXITED(status) || WIFSIGNALED(status)) { if (WIFEXITED(status)) { LOG_INF("Forkit process [" << pid << "] exited with code: " << WEXITSTATUS(status) << '.'); // EX_CONFIG is coolforkit-caps saying it holds no capabilities. if (WEXITSTATUS(status) == EX_CONFIG) CapabilityJailUnusable = true; } else { LOG_ERR("Forkit process [" << pid << "] " << (WCOREDUMP(status) ? "core-dumped" : "died") << " with " << SigUtil::signalName(WTERMSIG(status))); } // subforkits exit when their parent forkit, so we can // immediately consider these obsolete for (auto it = SubForKitProcs.begin(); it != SubForKitProcs.end(); ) { LOG_DBG("dropping subforkit " << it->first); it = dropSubForKit(it, /*askToExit=*/false); } OutstandingSubForKitSpawns.clear(); LastSubForKitSpawnRequestTimes.clear(); // Spawn a new forkit and try to dust it off and resume. if (!SigUtil::getShutdownRequestFlag() && !createForKit()) { LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn forkit instance."); SigUtil::requestShutdown(); } } else if (WIFSTOPPED(status)) { LOG_INF("Forkit process [" << pid << "] stopped with " << SigUtil::signalName(WSTOPSIG(status))); } else if (WIFCONTINUED(status)) { LOG_INF("Forkit process [" << pid << "] resumed with SIGCONT."); } else { LOG_WRN("Unknown status returned by waitpid: " << std::hex << status << std::dec); } return true; } else { LOG_ERR("An unknown child process [" << pid << "] died."); } } else if (pid < 0) { LOG_SYS("Forkit waitpid failed"); if (errno == ECHILD) { // No child processes. // Spawn a new forkit and try to dust it off and resume. if (!SigUtil::getShutdownRequestFlag() && !createForKit()) { LOG_FTL("Setting ShutdownRequestFlag: Failed to spawn forkit instance."); SigUtil::requestShutdown(); } } return true; } return false; #if defined __clang__ #pragma clang diagnostic pop #endif } #endif void COOLWSD::doHousekeeping() { if (PrisonerPoll) { PrisonerPoll->wakeup(); } } void COOLWSD::closeDocument(const std::string& docKey, const std::string& message) { std::unique_lock docBrokersLock(DocBrokersMutex); auto docBrokerIt = DocBrokers.find(docKey); if (docBrokerIt != DocBrokers.end()) { std::shared_ptr docBroker = docBrokerIt->second; docBroker->addCallback([docBroker, message]() { docBroker->closeDocument(message); }); } } void COOLWSD::autoSave(const std::string& docKey) { std::unique_lock docBrokersLock(DocBrokersMutex); auto docBrokerIt = DocBrokers.find(docKey); if (docBrokerIt != DocBrokers.end()) { std::shared_ptr docBroker = docBrokerIt->second; docBroker->addCallback( [docBroker]() { docBroker->autoSave(/*force=*/true, /*dontSaveIfUnmodified=*/true); }); } } void COOLWSD::setMigrationMsgReceived(const std::string& docKey) { std::unique_lock docBrokersLock(DocBrokersMutex); auto docBrokerIt = DocBrokers.find(docKey); if (docBrokerIt != DocBrokers.end()) { std::shared_ptr docBroker = docBrokerIt->second; docBroker->addCallback([docBroker]() { docBroker->setMigrationMsgReceived(); }); } } void COOLWSD::setAllMigrationMsgReceived() { std::unique_lock docBrokersLock(DocBrokersMutex); for (const auto& brokerIt : DocBrokers) { std::shared_ptr docBroker = brokerIt.second; docBroker->addCallback([docBroker]() { docBroker->setMigrationMsgReceived(); }); } } void COOLWSD::setLogLevelsOfKits(const std::string& level) { std::lock_guard docBrokersLock(DocBrokersMutex); LOG_INF("Changing kits' log levels: [" << level << ']'); for (const auto& brokerIt : DocBrokers) { std::shared_ptr docBroker = brokerIt.second; docBroker->addCallback([docBroker, level]() { docBroker->setKitLogLevel(level); }); } } /// Really do the house-keeping void PrisonPoll::wakeupHook() { if constexpr (!Util::isMobileAppBuild()) { THREAD_UNSAFE_DUMP_BEGIN LOG_TRC("PrisonerPoll - wakes up with " << NewChildren.size() << " new children and " << DocBrokers.size() << " brokers and " << TotalOutstandingForks << " kits forking"); THREAD_UNSAFE_DUMP_END if (!COOLWSD::checkAndRestoreForKit()) { // No children have died. // Make sure we have sufficient reserves. prespawnChildren(); } } std::unique_lock docBrokersLock(DocBrokersMutex, std::defer_lock); if (docBrokersLock.try_lock()) { COOLWSD::cleanupDocBrokers(); SigUtil::checkForwardSigUsr2(forwardSigUsr2); } #if !MOBILEAPP cleanupCollabBrokers(); cleanupCollabFetchRequests(); cleanupCollabUploadRequests(); #endif } #if !MOBILEAPP bool COOLWSD::createForKit() { LOG_INF("Creating new forkit process."); SigUtil::addActivity("spawning new forkit"); // Creating a new forkit is always a slow process. ChildSpawnTimeoutMs = std::chrono::milliseconds(CHILD_SPAWN_TIMEOUT_MS); std::unique_lock newChildrenLock(NewChildrenMutex); StringVector args; std::string parentPath = Util::getApplicationPath(); #if STRACE_COOLFORKIT // if you want to use this, you need to sudo setcap cap_fowner,cap_chown,cap_sys_chroot=ep /usr/bin/strace args.push_back("-o"); args.push_back("strace.log"); args.push_back("-f"); args.push_back("-tt"); args.push_back("-s"); args.push_back("256"); args.push_back(parentPath + "coolforkit-caps"); #elif VALGRIND_COOLFORKIT NoCapsForKit = true; NoSeccomp = true; // args.push_back("--log-file=valgrind.log"); // args.push_back("--track-fds=all"); // for massif: can connect with (gdb) target remote | vgdb // and then monitor snapshot before kit exit // args.push_back("--tool=massif"); // args.push_back("--vgdb=yes"); // args.push_back("--vgdb-error=0"); args.push_back("--trace-children=yes"); args.push_back("--error-limit=no"); args.push_back("--num-callers=128"); std::string nocapsCopy = parentPath + "coolforkit-nocaps"; FileUtil::copy(parentPath + "coolforkit-caps", nocapsCopy, true, true); args.push_back(nocapsCopy); #endif args.push_back("--systemplate=" + SysTemplate); args.push_back("--lotemplate=" + LoTemplate); args.push_back("--childroot=" + ChildRoot); args.push_back("--clientport=" + std::to_string(ClientPortNumber)); args.push_back("--masterport=" + MasterLocation.getName()); const DocProcSettings& docProcSettings = Admin::instance().getDefDocProcSettings(); std::ostringstream ossRLimits; ossRLimits << "limit_virt_mem_mb:" << docProcSettings.getLimitVirtMemMb(); ossRLimits << ";limit_stack_mem_kb:" << docProcSettings.getLimitStackMemKb(); ossRLimits << ";limit_file_size_mb:" << docProcSettings.getLimitFileSizeMb(); ossRLimits << ";limit_num_open_files:" << docProcSettings.getLimitNumberOpenFiles(); args.push_back("--rlimits=" + ossRLimits.str()); if (UnitWSD::get().hasKitHooks()) args.push_back("--unitlib=" + UnitTestLibrary); args.push_back("--version"); const bool capabilityJailWasTheChoice = !EnableMountNamespaces && !NoCapsForKit; if (capabilityJailWasTheChoice && CapabilityJailUnusable && LandlockAvailable) { RequireLandlock = true; // The kits run without a chroot from here on, so the paths they are given are the // real ones. NoCapsForKit = true; } const bool usingCapabilityJail = capabilityJailWasTheChoice && !RequireLandlock; if (usingCapabilityJail && CapabilityJailUnusable) { LOG_FTL("The chroot built from file capabilities does not work here and landlock is " "unavailable, so a document can be isolated in no way."); return false; } if (NoCapsForKit || RequireLandlock) args.push_back("--nocaps"); if (RequireLandlock) args.push_back("--require-landlock"); if (NoSeccomp) args.push_back("--noseccomp"); args.push_back("--ui=" + UserInterface); if (!CheckCoolUser) args.push_back("--disable-cool-user-checking"); if (UnattendedRun) args.push_back("--unattended"); #if ENABLE_DEBUG if (SingleKit) args.push_back("--singlekit"); #endif #if STRACE_COOLFORKIT std::string forKitPath = "/usr/bin/strace"; #elif VALGRIND_COOLFORKIT std::string forKitPath = "/usr/bin/valgrind"; #else std::string forKitPath = std::move(parentPath); if (!usingCapabilityJail) { forKitPath += "coolforkit-ns"; if (EnableMountNamespaces) args.push_back("--namespace"); } else { forKitPath += "coolforkit-caps"; if (!FileUtil::Stat(forKitPath).exists()) LOG_FTL("coolforkit-caps does not exist, install coolwsd-deprecated package"); } #endif // Always reap first, in case we haven't done so yet. if (ForKitProcId != -1) { if (Util::isKitInProcess()) return true; int status; waitpid(ForKitProcId, &status, WUNTRACED | WNOHANG); ForKitProcId = -1; Admin::instance().setForKitPid(ForKitProcId); } // Below line will be executed by PrisonerPoll thread. ForKitProc = nullptr; PrisonerPoll->setForKitProcess(ForKitProc); const std::string defaultConfigId; // ForKit always spawns one. ++TotalOutstandingForks; ++OutstandingForks[defaultConfigId]; LOG_INF("Launching forkit process: " << forKitPath << ' ' << args.cat(' ', 0)); LastForkRequestTimes[defaultConfigId] = std::chrono::steady_clock::now(); int child = createForkit(forKitPath, args); if (child == -1 && usingCapabilityJail) CapabilityJailUnusable = true; ForKitProcId = child; LOG_INF("Forkit process launched: " << ForKitProcId); // Init the Admin manager Admin::instance().setForKitPid(ForKitProcId); const int balance = COOLWSD::NumPreSpawnedChildren - OutstandingForks[defaultConfigId]; if (balance > 0) rebalanceChildren(defaultConfigId, balance); // Answering true with no forkit lets the next attempt start coolforkit-ns instead. if (ForKitProcId == -1 && usingCapabilityJail && LandlockAvailable) { LOG_WRN("The coolforkit-caps helper did not start. The next attempt uses a landlock " "jail."); return true; } return ForKitProcId != -1; } bool COOLWSD::sendMessageToForKit(const std::string& message, const std::string& configId) { if (!PrisonerPoll) return false; if (configId.empty()) { PrisonerPoll->sendMessageToForKit(message); return true; } auto it = SubForKitProcs.find(configId); if (it == SubForKitProcs.end()) { LOG_WRN("subforkit: " << configId << " doesn't exist yet, dropping message: " << message); return false; } PrisonerPoll->sendMessageToForKit(message, it->second); return true; } #endif // !MOBILEAPP /// Handles the socket that the prisoner kit connected to WSD on. class PrisonerRequestDispatcher final : public WebSocketHandler { std::weak_ptr _childProcess; int _pid; ///< The Kit's PID (for logging). int _socketFD; ///< The socket FD to the Kit (for logging). bool _associatedWithDoc; ///< True when/if we get a DocBroker. public: PrisonerRequestDispatcher() : WebSocketHandler(/* isClient = */ false, /* isMasking = */ true) , _pid(0) , _socketFD(0) , _associatedWithDoc(false) { LOG_TRC_S("PrisonerRequestDispatcher"); } ~PrisonerRequestDispatcher() { LOG_TRC("~PrisonerRequestDispatcher"); // Notify the broker that we're done. // Note: since this class is the default WebScoketHandler // for all incoming connections, for ForKit we have to // replace it (once we receive 'GET /coolws/forkit') with // ForKitProcWSHandler (see ForKitProcess) and nothing to disconnect. std::shared_ptr child = _childProcess.lock(); if (child && child->getPid() > 0) onDisconnect(); } private: /// Keep our socket around ... void onConnect(const std::shared_ptr& socket) override { WebSocketHandler::onConnect(socket); LOG_TRC("Prisoner connected"); } void onDisconnect() override { LOG_DBG("Prisoner connection disconnected"); // Notify the broker that we're done. std::shared_ptr child = _childProcess.lock(); std::shared_ptr docBroker = child && child->getPid() > 0 ? child->getDocumentBroker() : nullptr; if (docBroker) { assert(child->getPid() == _pid && "Child PID changed unexpectedly"); const bool unexpected = !docBroker->isUnloading() && !SigUtil::getShutdownRequestFlag(); if (unexpected) { LOG_WRN("DocBroker [" << docBroker->getDocKey() << "] got disconnected from its Kit (" << child->getPid() << ") unexpectedly. Closing"); } else { LOG_DBG("DocBroker [" << docBroker->getDocKey() << "] disconnected from its Kit (" << child->getPid() << ") as expected"); } docBroker->disconnectedFromKit(unexpected); } else if (!_associatedWithDoc && !SigUtil::getShutdownRequestFlag()) { LOG_WRN("Unassociated Kit (" << _pid << ") disconnected unexpectedly"); std::string configId; std::unique_lock lock(NewChildrenMutex); auto it = std::find(NewChildren.begin(), NewChildren.end(), child); if (it != NewChildren.end()) { configId = (*it)->getConfigId(); NewChildren.erase(it); } else LOG_WRN("Unknown Kit process closed with pid " << (child ? child->getPid() : -1)); #if !MOBILEAPP rebalanceChildren(configId, COOLWSD::NumPreSpawnedChildren); #endif } } /// Called after successful socket reads. void handleIncomingMessage(SocketDisposition &disposition) override { if (_childProcess.lock()) { // FIXME: inelegant etc. - derogate to websocket code WebSocketHandler::handleIncomingMessage(disposition); return; } std::shared_ptr socket = getSocket().lock(); if (!socket) { LOG_ERR("Invalid socket while reading incoming message"); return; } Buffer& data = socket->getInBuffer(); if (data.empty()) { LOG_DBG("No data to process from the socket"); return; } #ifdef LOG_SOCKET_DATA LOG_TRC("HandleIncomingMessage: buffer has:\n" << HexUtil::dumpHex(std::string(data.data(), std::min(data.size(), 256UL)))); #endif // Consume the incoming data by parsing and processing the body. http::RequestParser request; #if !MOBILEAPP const int64_t read = request.readData(data.data(), data.size()); if (read < 0) { LOG_ERR("Error parsing prisoner socket data"); return; } if (read == 0) { // Not enough data. return; } assert(read > 0 && "Must have read some data!"); // Remove consumed data. data.eraseFirst(read); #endif try { std::string jailId; std::string configId; std::map admsProps; #if !MOBILEAPP LOG_TRC("Child connection with URI [" << Anonymizer::anonymizeUrl(request.getUrl()) << ']'); Poco::URI requestURI(request.getUrl()); if (requestURI.getPath() == FORKIT_URI) { // New ForKit is spawned. const Poco::URI::QueryParameters params = requestURI.getQueryParameters(); const int pid = socket->getPid(); for (const auto& param : params) { if (param.first == "configid") configId = param.second; } if (configId.empty()) // primordial forkit { if (pid != COOLWSD::ForKitProcId) { LOG_WRN("Connection request received on " << FORKIT_URI << " endpoint from unexpected ForKit process. Skipped"); return; } COOLWSD::ForKitProc = std::make_shared(COOLWSD::ForKitProcId, socket, request); LOG_ASSERT_MSG(socket->getInBuffer().empty(), "Unexpected data in prisoner socket"); socket->getInBuffer().clear(); PrisonerPoll->setForKitProcess(COOLWSD::ForKitProc); } else { LOG_INF("subforkit [" << configId << "], seen as created."); auto [it, inserted] = SubForKitProcs.try_emplace(configId, std::make_shared(pid, socket, request)); if (!inserted) { LOG_ERR("subforkit [" << configId << "] arrived (pid " << pid << ") but slot already filled by pid " << it->second->getPid() << "; dropping duplicate"); return; } OutstandingSubForKitSpawns.erase(configId); LastSubForKitSpawnRequestTimes.erase(configId); LOG_ASSERT_MSG(socket->getInBuffer().empty(), "Unexpected data in prisoner socket"); socket->getInBuffer().clear(); // created subforkit for a reason, create spare early std::unique_lock lock(NewChildrenMutex); rebalanceChildren(configId, COOLWSD::NumPreSpawnedChildren); UNITWSD_CALL(newSubForKit(it->second, configId)); } return; } if (requestURI.getPath() != NEW_CHILD_URI) { LOG_ERR("Invalid incoming child URI [" << requestURI.getPath() << ']'); return; } const auto duration = std::chrono::steady_clock::now() - LastForkRequestTimes[configId]; const auto durationMs = std::chrono::duration_cast(duration); LOG_TRC("New child spawned after " << durationMs << " of requesting"); // New Child is spawned. const Poco::URI::QueryParameters params = requestURI.getQueryParameters(); const int pid = socket->getPid(); for (const auto& param : params) { if (param.first == "jailid") jailId = param.second; else if (param.first == "configid") configId = param.second; else if (param.first == "version") { COOLWSD::LOKitVersion = param.second; Poco::JSON::Object::Ptr object; if (JsonUtil::parseJSON(COOLWSD::LOKitVersion, object)) { COOLWSD::LOKitVersionNumber = JsonUtil::getJSONValue(object, "ProductVersion") + JsonUtil::getJSONValue(object, "ProductExtension"); COOLWSD::LOKitVersionHash = JsonUtil::getJSONValue(object, "BuildId").substr(0, 8); } } else if (param.first.size() > 6 && param.first.compare(0, 5, "adms_") == 0) admsProps[param.first.substr(5)] = param.second; } if (pid <= 0) { LOG_ERR("Invalid PID in child URI [" << Anonymizer::anonymizeUrl(request.getUrl()) << ']'); return; } if (jailId.empty()) { LOG_ERR("Invalid JailId in child URI [" << Anonymizer::anonymizeUrl(request.getUrl()) << ']'); return; } LOG_ASSERT_MSG(socket->getInBuffer().empty(), "Unexpected data in prisoner socket"); socket->getInBuffer().clear(); LOG_INF("New child [" << pid << "], jailId: " << jailId << ", configId: " << configId); #else pid_t pid = 100; jailId = "jail"; socket->getInBuffer().clear(); #endif LOG_TRC("Calling make_shared, for NewChildren?"); auto child = std::make_shared(pid, jailId, configId, socket, request, admsProps); if (!Util::isMobileApp()) UNITWSD_CALL(newChild(child)); _pid = pid; _socketFD = socket->getFD(); #if !MOBILEAPP child->setSMapsFD(socket->getIncomingFD(SharedFDType::SMAPS)); #endif _childProcess = child; // weak addNewChild(std::move(child)); } catch (const std::bad_weak_ptr&) { // Using shared_from_this() from a constructor is not good. assert(!"Got std::bad_weak_ptr. Are we using shared_from_this() from a constructor?"); } catch (const std::exception&) { // Probably don't have enough data just yet. // TODO: timeout if we never get enough. } } /// Prisoner websocket fun ... (for now) virtual void handleMessage(const std::vector &data) override { if (UnitWSD::isUnitTesting() && UnitWSD::get().filterChildMessage(data)) return; auto message = std::make_shared(data.data(), data.size(), Message::Dir::Out); std::shared_ptr socket = getSocket().lock(); if (socket) { assert(socket->getFD() == _socketFD && "Socket FD changed unexpectedly"); LOG_TRC("Prisoner message [" << message->abbr() << ']'); } else LOG_WRN("Message handler called but without valid socket. Expected #" << _socketFD); std::shared_ptr child = _childProcess.lock(); std::shared_ptr docBroker = child && child->getPid() > 0 ? child->getDocumentBroker() : nullptr; if (docBroker) { assert(child->getPid() == _pid && "Child PID changed unexpectedly"); _associatedWithDoc = true; docBroker->handleInput(message); } else if (child && child->getPid() > 0) { const std::string abbreviatedMessage = Anonymizer::enabled() ? "..." : message->abbr(); LOG_WRN("Child " << child->getPid() << " has no DocBroker to handle message: [" << abbreviatedMessage << ']'); } else { const std::string abbreviatedMessage = Anonymizer::enabled() ? "..." : message->abbr(); LOG_ERR("Cannot handle message with unassociated Kit (PID " << _pid << "): [" << abbreviatedMessage); } } int getPollEvents(std::chrono::steady_clock::time_point /* now */, int64_t & /* timeoutMaxMs */) override { return POLLIN; } void performWrites(std::size_t /*capacity*/) override {} }; namespace { class PlainSocketFactory final : public SocketFactory { std::shared_ptr create(const int physicalFd, Socket::Type type) override { return StreamSocket::create( std::string(), physicalFd, type, false, HostType::Other, std::make_shared()); } }; #if ENABLE_SSL class SslSocketFactory final : public SocketFactory { std::shared_ptr create(const int physicalFd, Socket::Type type) override { return StreamSocket::create(std::string(), physicalFd, type, false, HostType::Other, std::make_shared()); } }; #endif class PrisonerSocketFactory final : public SocketFactory { std::shared_ptr create(const int fd, Socket::Type type) override { // No local delay. return StreamSocket::create(std::string(), fd, type, false, HostType::Other, std::make_shared(), StreamSocket::ReadType::UseRecvmsgExpectFD); } }; } COOLWSDServer::COOLWSDServer() : _acceptPoll("accept_poll") #if !MOBILEAPP , _admin(Admin::instance()) #endif { } COOLWSDServer::~COOLWSDServer() { stop(); } std::shared_ptr COOLWSDServer::findClientPort() { return findServerPort(); } void COOLWSDServer::startPrisoners() { PrisonerPoll->startThread(); PrisonerPoll->insertNewSocket(findPrisonerServerPort()); } // static void COOLWSDServer::stopPrisoners() { PrisonerPoll->joinThread(); } void COOLWSDServer::start(std::shared_ptr&& serverSocket) { #if MOBILEAPP coolwsd_server_socket_fd = serverSocket->getFD(); #endif _acceptPoll.startThread(); _acceptPoll.insertNewSocket(std::move(serverSocket)); WebServerPoll->startThread(); #if !MOBILEAPP _admin.start(); #endif } void COOLWSDServer::stop() { _acceptPoll.joinThread(); if (WebServerPoll) WebServerPoll->joinThread(); #if !MOBILEAPP _admin.stop(); #endif } void COOLWSDServer::dumpState(std::ostream& os) const { // FIXME: add some stop-world magic before doing the dump(?) ThreadChecks::Inhibit = true; std::string version, hash; Util::getVersionInfo(version, hash); THREAD_UNSAFE_DUMP_BEGIN os << "COOLWSDServer: " << version << " - " << hash << " state dumping" #if !MOBILEAPP "\n Kit version: " << COOLWSD::LOKitVersion << "\n Ports: server " << ClientPortNumber << " prisoner " << MasterLocation << "\n SSL: " << (ConfigUtil::isSslEnabled() ? "https" : "http") << "\n SSL-Termination: " << (ConfigUtil::isSSLTermination() ? "yes" : "no") << "\n Security: " << (COOLWSD::NoCapsForKit ? (Landlock::isSupported() ? "landlock" : "no") : (COOLWSD::EnableMountNamespaces ? "namespace" : "chroot")) << " containment, " << (COOLWSD::NoSeccomp ? "no " : "") << "api lockdown" "\n Admin: " << (COOLWSD::AdminEnabled ? "enabled" : "disabled") << "\n RouteToken: " << COOLWSD::RouteToken #endif << "\n Uptime (seconds): " << std::chrono::duration_cast(std::chrono::steady_clock::now() - COOLWSD::StartTime) .count() << "\n TerminationFlag: " << SigUtil::getTerminationFlag() << "\n isShuttingDown: " << SigUtil::getShutdownRequestFlag() << "\n NewChildren: " << NewChildren.size() << " (" << NewChildren.capacity() << ')' << "\n OutstandingForks: " << TotalOutstandingForks << "\n NumPreSpawnedChildren: " << COOLWSD::NumPreSpawnedChildren << "\n ChildSpawnTimeoutMs: " << ChildSpawnTimeoutMs.load() #if !MOBILEAPP << "\n of which ConvertTo: " << ConvertToBroker::getInstanceCount() #endif << "\n vs. MaxDocuments: " << COOLWSD::MaxDocuments << "\n NumConnections: " << COOLWSD::NumConnections << "\n vs. MaxConnections: " << COOLWSD::MaxConnections << "\n SysTemplate: " << COOLWSD::SysTemplate << "\n LoTemplate: " << COOLWSD::LoTemplate << "\n ChildRoot: " << COOLWSD::ChildRoot << "\n FileServerRoot: " << COOLWSD::FileServerRoot << "\n ServiceRoot: " << COOLWSD::ServiceRoot << "\n LOKitVersion: " << COOLWSD::LOKitVersion << "\n HostIdentifier: " << Util::getProcessIdentifier() << "\n ConfigFile: " << COOLWSD::ConfigFile << "\n LogLevel: " << COOLWSD::LogLevel << "\n LogDisabledAreas: " << COOLWSD::LogDisabledAreas << "\n AnonymizeUserData: " << (Anonymizer::enabled() ? "yes" : "no") << "\n CheckCoolUser: " << (COOLWSD::CheckCoolUser ? "yes" : "no") << "\n IsProxyPrefixEnabled: " << (COOLWSD::IsProxyPrefixEnabled ? "yes" : "no") << "\n OverrideWatermark: " << COOLWSD::OverrideWatermark << "\n UserInterface: " << COOLWSD::UserInterface << "\n Total PSS: " << ProcUtil::getProcessTreePss(ProcUtil::getProcessId()) << " KB" "\n Config: " << LoggableConfigEntries; THREAD_UNSAFE_DUMP_END std::string smap; if (const ssize_t size = FileUtil::readFile("/proc/self/smaps_rollup", smap); size <= 0) os << "\n smaps_rollup: "; else os << "\n smaps_rollup: " << Util::replace(std::move(smap), "\n", "\n\t"); #if !MOBILEAPP if (FetchHttpSession) { os << "\nFetchHttpSession:\n"; FetchHttpSession->dumpState(os, "\n "); } else #endif // !MOBILEAPP { os << "\nFetchHttpSession: null\n"; } os << "\nServer poll:\n"; _acceptPoll.dumpState(os); os << "\nWeb Server poll:\n"; WebServerPoll->dumpState(os); os << "\nPrisoner poll:\n"; PrisonerPoll->dumpState(os); #if !MOBILEAPP Admin::dumpMetrics(); // Dump the state from the Admin poll thread. // If we have any DNS work going on. os << '\n'; net::AsyncDNS::dumpState(os); os << '\n'; COOLWSD::SavedClipboards->dumpState(os); os << '\n'; COOLWSD::FileRequestHandler->dumpState(os); if (RemoteDocumentBroker::isInitialized()) { os << '\n'; RemoteDocumentBroker::instance().dumpState(os); } #endif { std::lock_guard docBrokerLock(DocBrokersMutex); os << "\nDocument Broker polls " "[ " << DocBrokers.size() << " ]:\n"; for (const auto& i : DocBrokers) i.second->dumpState(os); } #if !MOBILEAPP os << "\nConverter count: " << ConvertToBroker::getInstanceCount() << '\n'; #endif os << "\nDone COOLWSDServer state dumping.\n"; ThreadChecks::Inhibit = false; } /// Create the internal only, local socket for forkit / kits prisoners to talk to. std::shared_ptr COOLWSDServer::findPrisonerServerPort() { std::shared_ptr factory = std::make_shared(); #if !MOBILEAPP auto socket = std::make_shared( std::chrono::steady_clock::now(), *PrisonerPoll, factory); const UnxSocketPath location = socket->bind(); if (!location.isValid()) { LOG_FTL("Failed to create local unix domain socket. Exiting."); Util::forcedExit(EX_SOFTWARE); return nullptr; } if (!socket->listen()) { LOG_FTL("Failed to listen on local unix domain socket at " << location << ". Exiting."); Util::forcedExit(EX_SOFTWARE); } LOG_INF("Listening to prisoner connections on " << location); MasterLocation = location; #if ENABLE_CHILDROOTS if(!socket->linkTo(COOLWSD::SysTemplate)) { LOG_FTL("Failed to hardlink local unix domain socket into a jail. Exiting."); Util::forcedExit(EX_SOFTWARE); } #endif #else constexpr int UNUSED_PORT_NUMBER = 0; std::shared_ptr socket = ServerSocket::create(ServerSocket::Type::Public, UNUSED_PORT_NUMBER, ClientPortProto, std::chrono::steady_clock::now(), *PrisonerPoll, factory); PrisonerServerSocketFD = socket->getFD(); LOG_INF("Listening to prisoner connections on #" << PrisonerServerSocketFD); #endif return socket; } /// Create the externally listening public socket std::shared_ptr COOLWSDServer::findServerPort() { std::shared_ptr factory; std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); if (ClientPortNumber <= 0) { // A unit test listens on a free port that the kernel picks, so no other process on the // host knows its port. ClientPortNumber = UnitWSD::isUnitTesting() ? 0 : DEFAULT_CLIENT_PORT_NUMBER; } #if ENABLE_SSL if (ConfigUtil::isSslEnabled()) factory = std::make_shared(); else #endif factory = std::make_shared(); std::shared_ptr socket = ServerSocket::create( ClientListenAddr, ClientPortNumber, ClientPortProto, now, *WebServerPoll, factory); #if !MOBILEAPP const int firstPortNumber = ClientPortNumber; #endif while (!socket && ClientPortNumber > 0 && (UnitWSD::isUnitTesting() #if ENABLE_DEBUG || COOLWSD::FindFreePort #endif )) { ++ClientPortNumber; LOG_INF("Client port " << (ClientPortNumber - 1) << " is busy, trying " << ClientPortNumber); socket = ServerSocket::create(ClientListenAddr, ClientPortNumber, ClientPortProto, now, *WebServerPoll, factory); } #if !MOBILEAPP if (!socket) { LOG_FTL("Failed to listen on Server port(s) (" << firstPortNumber << '-' << ClientPortNumber << "). Exiting"); Util::forcedExit(EX_SOFTWARE); } if (ClientPortNumber == 0) ClientPortNumber = net::boundPort(socket->getFD()); LOG_INF('#' << socket->getFD() << " Listening to client connections on port " << ClientPortNumber); #else LOG_INF("Listening to client connections on #" << socket->getFD()); #endif return socket; } void COOLWSDServer::AcceptPoll::wakeupHook() { SigUtil::checkDumpGlobalState(dump_state); } #if !MOBILEAPP void COOLWSD::processFetchUpdate(const std::shared_ptr& poll) { try { const std::string url(INFOBAR_URL); if (url.empty()) return; // No url, nothing to do. if (FetchHttpSession) return; Poco::URI uriFetch(url); uriFetch.addQueryParameter("product", ConfigUtil::getString("product_name", APP_NAME)); uriFetch.addQueryParameter("version", Util::getCoolVersion()); LOG_TRC("Infobar update request from " << uriFetch.toString()); FetchHttpSession = StorageConnectionManager::getHttpSession(uriFetch); if (!FetchHttpSession) return; http::Request request(uriFetch.getPathAndQuery()); request.add("Accept", "application/json"); FetchHttpSession->setFinishedHandler([](const std::shared_ptr& httpSession) { std::shared_ptr httpResponse = httpSession->response(); FetchHttpSession.reset(); if (httpResponse->statusLine().statusCode() == http::StatusCode::OK) { LOG_DBG("Infobar update returned: " << httpResponse->getBody()); std::lock_guard lock(COOLWSD::FetchUpdateMutex); COOLWSD::LatestVersion = httpResponse->getBody(); } else LOG_WRN("Failed to update the infobar. Got: " << httpResponse->statusLine().statusCode() << ' ' << httpResponse->statusLine().reasonPhrase()); }); FetchHttpSession->asyncRequest(request, poll); } catch(const Poco::Exception& exc) { LOG_DBG("FetchUpdate: " << exc.displayText()); } catch(std::exception& exc) { LOG_DBG("FetchUpdate: " << exc.what()); } catch(...) { LOG_DBG("FetchUpdate: Unknown exception"); } } #if ENABLE_DEBUG std::string COOLWSD::getServerURL() { return getServiceURI(""); } #endif #endif void COOLWSD::innerMain() { #if !MOBILEAPP # ifdef __linux__ // down-pay all the forkit linking cost once & early. setenv("LD_BIND_NOW", "1", 1); # endif std::string version, hash; Util::getVersionInfo(version, hash); LOG_INF("Coolwsd version details: " << version << " - " << hash << " - id " << Util::getProcessIdentifier() << " - on " << Util::getLinuxVersion()); #endif SigUtil::addActivity("coolwsd init"); initializeSSL(); #if !MOBILEAPP // Fetch remote settings from server if configured std::shared_ptr remoteConfigThread(std::make_shared(config())); remoteConfigThread->start(); #endif #if !DOCS_SHARE_PROCESS // Force a uniform UTF-8 locale for ourselves & our children. char* locale = std::setlocale(LC_ALL, "C.UTF-8"); if (!locale) { // rhbz#1590680 - C.UTF-8 is unsupported on RH7 LOG_WRN("Could not set locale to C.UTF-8, will try en_US.UTF-8"); locale = std::setlocale(LC_ALL, "en_US.UTF-8"); if (!locale) LOG_WRN("Could not set locale to en_US.UTF-8. Without UTF-8 support documents with non-ASCII file names cannot be opened."); } if (locale) { LOG_INF("Locale is set to " << std::string(locale)); ::setenv("LC_ALL", locale, 1); } #endif // !DOCS_SHARE_PROCESS #if !MOBILEAPP // We use the same option set for both parent and child coolwsd, // so must check options required in the parent (but not in the // child) separately now. Also check for options that are // meaningless for the parent. if (LoTemplate.empty()) { LOG_FTL("Missing --lo-template-path option"); throw Poco::Util::MissingOptionException("lotemplate"); } if (FileServerRoot.empty()) FileServerRoot = Util::getApplicationPath(); FileServerRoot = Poco::Path(FileServerRoot).absolute().toString(); LOG_DBG("FileServerRoot: " << FileServerRoot); const auto fetchUpdateCheck = std::chrono::duration_cast( std::chrono::hours(std::max(ConfigUtil::getConfigValue("fetch_update_check", 10), 0))); #endif ClientRequestDispatcher::InitStaticFileContentCache(); // Allocate our port - passed to prisoners. assert(COOLWSDServer::Instance && "The COOLWSDServer instance does not exist."); // allocate port & hold temporarily. std::shared_ptr serverPort = COOLWSDServer::Instance->findClientPort(); #if !MOBILEAPP TmpFontDir = ChildRoot + JailUtil::CHILDROOT_TMP_INCOMING_PATH; #endif // Start the internal prisoner server and spawn forkit, // which in turn forks first child. COOLWSDServer::Instance->startPrisoners(); // No need to "have at least one child" beforehand on mobile #if !MOBILEAPP if (!Util::isKitInProcess()) { // Make sure we have at least one child before moving forward. std::unique_lock lock(NewChildrenMutex); // If we are debugging, it's not uncommon to wait for several minutes before first // child is born. Don't use an expiry timeout in that case. const bool debugging = std::getenv("SLEEPFORDEBUGGER") || std::getenv("SLEEPKITFORDEBUGGER"); if (debugging) { LOG_DBG("Waiting for new child without timeout."); NewChildrenCV.wait(lock, []() { return !NewChildren.empty(); }); } else { int retry = (COOLWSD::NoCapsForKit ? 150 : 50); const auto timeout = ChildSpawnTimeoutMs.load(); while (retry-- > 0 && !SigUtil::getShutdownRequestFlag()) { LOG_INF("Waiting for a new child for a max of " << timeout); if (NewChildrenCV.wait_for(lock, timeout, []() { return !NewChildren.empty(); })) { break; } } } // Check we have at least one. LOG_TRC("Have " << NewChildren.size() << " new children."); if (NewChildren.empty()) { if (SigUtil::getShutdownRequestFlag()) LOG_FTL("Shutdown requested while starting up. Exiting."); else LOG_FTL("No child process could be created. Exiting."); Util::forcedExit(EX_SOFTWARE); } assert(NewChildren.size() > 0); } if (LogLevel != "trace") { LOG_INF("WSD initialization complete: setting log-level to [" << LogLevel << "] as configured."); Log::setLevel(LogLevel); } Log::setDisabledAreas(LogDisabledAreas); if (Log::getLevel() >= Log::Level::INF) LOG_ERR("Log level is set very high to '" << LogLevel << "' this will have a " "significant performance impact. Do not use this in production."); // Start the remote font downloading polling thread. std::unique_ptr remoteFontConfigThread; try { // Fetch font settings from server if configured remoteFontConfigThread = std::make_unique(config()); remoteFontConfigThread->start(); } catch (const Poco::Exception&) { LOG_DBG("No remote_font_config"); } #elif defined __EMSCRIPTEN__ // Hard-code a somewhat random log level: Log::setLevel("information"); #endif // URI with /contents are public and we don't need to anonymize them. Anonymizer::mapAnonymized("contents", "contents"); // Start the server. COOLWSDServer::Instance->start(std::move(serverPort)); #if WASMAPP // It is not at all obvious that this is the ideal place to do the HULLO thing and call onopen // on TheFakeWebSocket. But it seems to work. handle_cool_message("HULLO"); MAIN_THREAD_EM_ASM(globalThis.TheFakeWebSocket.onopen()); #endif /// The main-poll does next to nothing: std::shared_ptr mainWait = std::make_shared("main"); mainWait->runOnClientThread(); SigUtil::addActivity("coolwsd accepting connections"); #if !MOBILEAPP std::cerr << "Ready to accept connections on port " << ClientPortNumber << ".\n" << std::endl; if (SignalParent) { kill(getppid(), SIGUSR2); } #endif #if !MOBILEAPP && ENABLE_DEBUG std::ostringstream oss; oss << "\nPick a document to open in your browser, including one from your own machine:\n\n" << getServiceURI(COOLWSD_TEST_DOCUMENT_PICKER) << '\n'; // The page cannot fill in the admin user name and password, so these two carry the // credentials from the configuration. const std::string adminURI = getServiceURI(COOLWSD_TEST_ADMIN_CONSOLE, true); if (!adminURI.empty()) oss << "\nThe admin console and the metrics, ready to open without logging in:\n\n" << adminURI << '\n' << getServiceURI(COOLWSD_TEST_METRICS, true) << '\n'; oss << "\nThe capabilities, the discovery and the health checks:\n\n" << getServiceURI("/hosting/capabilities") << '\n' << getServiceURI("/hosting/discovery") << '\n' << getServiceURI("/livez?verbose") << '\n' << getServiceURI("/readyz?verbose") << '\n'; oss << std::endl; std::cerr << oss.str(); #endif const auto startStamp = std::chrono::steady_clock::now(); #if !MOBILEAPP auto stampFetch = startStamp - (fetchUpdateCheck - std::chrono::milliseconds(60000)); #ifdef __linux__ if (ConfigUtil::getConfigValue("stop_on_config_change", false)) { std::shared_ptr inotifySocket = std::make_shared(startStamp); mainWait->insertNewSocket(inotifySocket); } #endif LOG_DBG("trimming memory post startup"); Util::trimMalloc(); time_t prevTrimTrigger = 0; #endif SigUtil::addActivity("coolwsd running"); while (!SigUtil::getShutdownRequestFlag()) { // This timeout affects the recovery time of prespawned children. std::chrono::microseconds waitMicroS = SocketPoll::DefaultPollTimeoutMicroS * 4; if (UnitWSD::isUnitTesting() && !SigUtil::getShutdownRequestFlag()) { UNITWSD_CALL(invokeTest()); // More frequent polling while testing, to reduce total test time. waitMicroS = std::min(UnitWSD::get().getTimeoutMilliSeconds(), std::chrono::milliseconds(1000)); waitMicroS /= 4; } mainWait->poll(waitMicroS); // Wake the prisoner poll to spawn some children, if necessary. PrisonerPoll->wakeup(); if (SigUtil::getShutdownRequestFlag()) { // The code below can be unsafe as other threads start shutting down. break; } const auto timeNow = std::chrono::steady_clock::now(); const std::chrono::milliseconds timeSinceStartMs = std::chrono::duration_cast(timeNow - startStamp); // Unit test timeout if (UnitWSD::isUnitTesting() && !SigUtil::getShutdownRequestFlag()) { if (auto const unit = UnitWSD::getMaybeNull()) { unit->checkTimeout(timeSinceStartMs); } } #if !MOBILEAPP SavedClipboards->checkexpiry(timeNow); const std::chrono::milliseconds durationFetch = std::chrono::duration_cast(timeNow - stampFetch); if (fetchUpdateCheck > std::chrono::milliseconds::zero() && durationFetch > fetchUpdateCheck) { processFetchUpdate(mainWait); stampFetch = timeNow; } // if Admin hasn't seen any document activity for over 10 mins them malloc_trim constexpr time_t idleTrimCheck(10 * 60); const time_t lastAdminActivity = Admin::instance().getLastActivityTime(); if (lastAdminActivity != prevTrimTrigger) { const time_t adminIdle = time(nullptr) - lastAdminActivity; if (adminIdle > idleTrimCheck) { LOG_DBG("trimming memory on idle"); Util::trimMalloc(); // Don't bother repeating until LastActivityTime changes. prevTrimTrigger = lastAdminActivity; } } #endif #if ENABLE_DEBUG && !MOBILEAPP if (careerSpanMs > std::chrono::milliseconds::zero() && timeSinceStartMs > careerSpanMs) { LOG_INF("Setting ShutdownRequestFlag: " << timeSinceStartMs << " gone, career of " << careerSpanMs << " expired."); SigUtil::requestShutdown(); } #endif } #if !defined(IOS) // SigUtil::getShutdownRequestFlag() always returns false on iOS, thus the above while // loop never exits. COOLWSD::alertAllUsersInternal("close: shuttingdown"); SigUtil::addActivity("shutting down"); #if !MOBILEAPP // Lots of polls will stop; stop watching them first. SocketPoll::PollWatchdog.reset(); #endif // Stop the listening to new connections // and wait until sockets close. LOG_INF("Stopping server socket listening. ShutdownRequestFlag: " << SigUtil::getShutdownRequestFlag() << ", TerminationFlag: " << SigUtil::getTerminationFlag()); if (!UnitWSD::isUnitTesting()) { // When running unit-tests the listening port will // get recycled and another test will be listening. // This is very problematic if a DocBroker here is // saving and uploading before shutting down, because // the test that gets the same port will receive this // unexpected upload and fail. // Otherwise, in production, we should probably respond // with some error that we are recycling. But for now, // don't change the behavior and stop listening. COOLWSDServer::Instance->stop(); } // atexit handlers tend to free Admin before Documents LOG_INF("Exiting. Cleaning up lingering documents."); #if !MOBILEAPP if (remoteFontConfigThread) { LOG_DBG("Stopping remote font config thread"); remoteFontConfigThread->stop(); } #endif if (!SigUtil::getShutdownRequestFlag()) { // This shouldn't happen, but it's fail safe to always cleanup properly. LOG_WRN("Setting ShutdownRequestFlag: Exiting WSD without ShutdownRequestFlag. Setting it " "now."); SigUtil::requestShutdown(); } SigUtil::addActivity("wait save & close"); // Wait until documents are saved and sessions closed. // Don't stop the DocBroker, they will exit. constexpr size_t sleepMs = 200; constexpr size_t count = (COMMAND_TIMEOUT_MS * 6) / sleepMs; for (size_t i = 0; i < count; ++i) { std::unique_lock docBrokersLock(DocBrokersMutex); if (DocBrokers.empty()) break; LOG_DBG("Waiting for " << DocBrokers.size() << " documents to stop."); cleanupDocBrokers(); docBrokersLock.unlock(); // Give them time to save and cleanup. std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); } if (UnitWSD::isUnitTesting() && !SigUtil::getTerminationFlag()) { LOG_INF("Setting TerminationFlag to avoid deadlocking unittest."); SigUtil::setTerminationFlag(); } // Disable thread checking - we'll now cleanup lots of things if we can ThreadChecks::Inhibit = true; // Wait for the DocumentBrokers. They must be saving/uploading now. // Do not stop them! Otherwise they might not save/upload the document. // We block until they finish, or the service stopping times out. { std::unique_lock docBrokersLock(DocBrokersMutex); for (const auto& docBrokerIt : DocBrokers) { const std::shared_ptr& docBroker = docBrokerIt.second; if (docBroker && docBroker->isAlive()) { LOG_DBG("Joining docBroker [" << docBrokerIt.first << "]."); docBroker->joinThread(); } } // Now should be safe to destroy what's left. cleanupDocBrokers(); DocBrokers.clear(); } SigUtil::addActivity("save traces"); if (TraceEventFile != nullptr) { // If we have written any objects to it, it ends with a comma and newline. Back over those. if (ftell(TraceEventFile) > 2) (void)fseek(TraceEventFile, -2, SEEK_CUR); // Close the JSON array. fprintf(TraceEventFile, "\n]\n"); fclose(TraceEventFile); TraceEventFile = nullptr; } #if !MOBILEAPP if (!Util::isKitInProcess()) { // Terminate child processes LOG_INF("Requesting forkit process " << ForKitProcId << " to terminate."); #if CODE_COVERAGE || VALGRIND_COOLFORKIT constexpr auto signal = SIGTERM; #else constexpr auto signal = SIGKILL; #endif SigUtil::killChild(ForKitProcId, signal); } #endif COOLWSDServer::stopPrisoners(); SigUtil::addActivity("prisoners stopped"); if (UnitWSD::isUnitTesting()) { COOLWSDServer::Instance->stop(); COOLWSDServer::Instance.reset(); } PrisonerPoll.reset(); #if !MOBILEAPP net::AsyncDNS::stopAsyncDNS(); #endif SigUtil::addActivity("async DNS stopped"); COOLWSDServer::WebServerPoll.reset(); // Terminate child processes LOG_INF("Requesting child processes to terminate."); for (const auto& child : NewChildren) { child->terminate(); } NewChildren.clear(); SigUtil::addActivity("terminated unused children"); #if !MOBILEAPP if (!Util::isKitInProcess()) { SigUtil::addActivity("waiting for forkit to exit"); // Wait for forkit process finish. LOG_INF("Waiting for forkit process to exit"); int status = 0; waitpid(ForKitProcId, &status, WUNTRACED); ForKitProcId = -1; ForKitProc.reset(); } // In unit-tests remove only our jails, otherwise cleanup all jails. JailUtil::cleanupJails(UnitWSD::isUnitTesting() ? ChildRoot : CleanupChildRoot); #endif // !MOBILEAPP if (Util::isMobileApp()) { LOG_INF("Process [coolwsd] finished with exit status: " << EXIT_OK); Util::forcedExit(EXIT_OK); } #endif // !IOS } std::shared_ptr COOLWSD:: getWebServerPoll () { return COOLWSDServer::WebServerPoll; } int COOLWSD::cleanup(int returnValue) { try { #ifndef IOS if (UnitBase::isUnitTesting()) { LOG_DBG("Coolwsd finished with exit status " << returnValue << "; uninitializing UnitBase"); const int unitReturnValue = UnitBase::uninit(); if (unitReturnValue != EXIT_OK) { // Overwrite the return value if the unit-test failed. LOG_INF("Overwriting process [coolwsd] exit status [" << returnValue << "] with unit-test status: " << unitReturnValue); returnValue = unitReturnValue; } } LOG_INF("Process [coolwsd] finished with exit status: " << returnValue); SigUtil::addActivity("finished with status " + std::to_string(returnValue)); #endif // !IOS COOLWSDServer::Instance.reset(); PrisonerPoll.reset(); COOLWSDServer::WebServerPoll.reset(); ClientRequestDispatcher::uninitialize(); #if !MOBILEAPP SavedClipboards.reset(); FileRequestHandler.reset(); JWTAuth::cleanup(); RemoteDocumentBroker::uninitialize(); Admin::uninitialize(); Util::forcedExit(returnValue); #if 0 TraceDumper.reset(); ThreadChecks::Inhibit = true; // Delete these while the static Admin instance is still alive. { std::lock_guard docBrokersLock(DocBrokersMutex); DocBrokers.clear(); } SigUtil::uninitialize(); #if ENABLE_SSL // Finally, we no longer need SSL. if (ConfigUtil::isSslEnabled()) { #if !ENABLE_DEBUG // At least on centos7, Poco deadlocks while // cleaning up its SSL context singleton. Util::forcedExit(returnValue); #endif // !ENABLE_DEBUG Poco::Net::uninitializeSSL(); Poco::Crypto::uninitializeCrypto(); ssl::Manager::uninitializeClientContext(); ssl::Manager::uninitializeServerContext(); } #endif #endif #endif } catch (const std::exception& ex) { LOG_ERR("Failed to uninitialize: " << ex.what()); throw; } return returnValue; } // coverity[root_function] : don't warn about uncaught exceptions int COOLWSD::main(const std::vector& /*args*/) { SigUtil::resetTerminationFlags(); int returnValue = EXIT_SOFTWARE; try { innerMain(); } catch (const std::exception& e) { LOG_FTL("Exception: " << e.what()); cleanup(EXIT_SOFTWARE); throw; } catch (...) { cleanup(EXIT_SOFTWARE); throw; } returnValue = cleanup(EXIT_OK); LOG_INF("Process [coolwsd] finished with exit status: " << returnValue); #if CODE_COVERAGE __gcov_dump(); #endif return returnValue; } int COOLWSD::getClientPortNumber() { return ClientPortNumber; } /// Only for unit testing ... std::string COOLWSD::getJailRoot(int pid) { std::lock_guard docBrokersLock(DocBrokersMutex); for (const auto &it : DocBrokers) { if (pid < 0 || it.second->getPid() == pid) return it.second->getJailRoot(); } return std::string(); } #if !MOBILEAPP std::vector> COOLWSD::getBrokersTestOnly() { std::lock_guard docBrokersLock(DocBrokersMutex); std::vector> result; result.reserve(DocBrokers.size()); for (const auto& brokerIt : DocBrokers) result.push_back(brokerIt.second); return result; } void COOLWSD::runInPrisonerPollTestOnly(const std::function& callback) { if (PrisonerPoll) PrisonerPoll->addCallback(callback); } std::set COOLWSD::getKitPids() { std::set pids = getSpareKitPids(); pids.merge(getDocKitPids()); return pids; } std::set COOLWSD::getSpareKitPids() { std::set pids; pid_t pid; { std::unique_lock lock(NewChildrenMutex); for (const auto &child : NewChildren) { pid = child->getPid(); if (pid > 0) pids.emplace(pid); } } return pids; } std::set COOLWSD::getDocKitPids() { std::set pids; pid_t pid; { std::unique_lock lock(DocBrokersMutex); for (const auto &it : DocBrokers) { pid = it.second->getPid(); if (pid > 0) pids.emplace(pid); } } return pids; } namespace Util { void alertAllUsers(const std::string& cmd, const std::string& kind) { alertAllUsers("error: cmd=" + cmd + " kind=" + kind); } void alertAllUsers(const std::string& msg) { COOLWSD::alertAllUsersInternal(msg); } } static void forwardSignal(int signum); #endif // [-loplugin:external] void dump_state() { std::ostringstream oss(Util::makeDumpStateStream()); oss << "Start WSD " << ProcUtil::getProcessId() << " Dump State:\n"; if (COOLWSDServer::Instance) COOLWSDServer::Instance->dumpState(oss); oss << "\nMalloc info [" << ProcUtil::getProcessId() << "]: \n\t" << Util::replace(Util::getMallocInfo(), "\n", "\n\t") << '\n'; oss << "\nEnd WSD " << ProcUtil::getProcessId() << " Dump State.\n"; const std::string msg = oss.str(); fprintf(stderr, "%s", msg.c_str()); // Log in the journal. LOG_WRN(msg); #if !MOBILEAPP Admin::dumpMetrics(); std::lock_guard docBrokerLock(DocBrokersMutex); std::lock_guard newChildLock(NewChildrenMutex); forwardSignal(SIGUSR1); #endif } #if !MOBILEAPP // The intent is that this function can be called from a debugger. It is not used otherwise. // [-loplugin:external] [-loplugin:unreffun] void lslr_childroot() { std::cout << "lslr: " << COOLWSD::ChildRoot << "\n"; FileUtil::lslr(COOLWSD::ChildRoot); std::cout << std::flush; } #endif void forwardSigUsr2() { #if !MOBILEAPP LOG_TRC("forwardSigUsr2"); if (Util::isKitInProcess()) return; Util::assertIsLocked(DocBrokersMutex); std::lock_guard newChildLock(NewChildrenMutex); forwardSignal(SIGUSR2); #endif } #if !MOBILEAPP void forwardSignal(const int signum) { Util::assertIsLocked(DocBrokersMutex); Util::assertIsLocked(NewChildrenMutex); const char* name = SigUtil::signalName(signum); if (COOLWSD::ForKitProcId > 0) { LOG_INF("Sending " << name << " to forkit " << COOLWSD::ForKitProcId); ::kill(COOLWSD::ForKitProcId, signum); } for (const auto& child : NewChildren) { if (child && child->getPid() > 0) { LOG_INF("Sending " << name << " to child " << child->getPid()); ::kill(child->getPid(), signum); } } for (const auto& pair : DocBrokers) { const std::shared_ptr& docBroker = pair.second; if (docBroker && docBroker->getPid() > 0) { LOG_INF("Sending " << name << " to docBroker " << docBroker->getPid()); ::kill(docBroker->getPid(), signum); } } } #endif /* vim:set shiftwidth=4 softtabstop=4 expandtab: */