[Libreoffice-commits] online.git: common/Common.hpp common/UnitHTTP.hpp kit/ForKit.cpp kit/Kit.cpp net/ServerSocket.hpp net/Socket.cpp net/Socket.hpp wsd/LOOLWSD.cpp
Libreoffice Gerrit user
logerrit at kemper.freedesktop.org
Sat Mar 30 17:34:06 UTC 2019
common/Common.hpp | 5 +
common/UnitHTTP.hpp | 2
kit/ForKit.cpp | 6 +-
kit/Kit.cpp | 23 ++++----
net/ServerSocket.hpp | 18 ++++++
net/Socket.cpp | 136 ++++++++++++++++++++++++++++++++++++++++-----------
net/Socket.hpp | 25 ++++++---
wsd/LOOLWSD.cpp | 68 +++++++++++--------------
8 files changed, 194 insertions(+), 89 deletions(-)
New commits:
commit 81a27e26aab2fed410310c3cda46090b8a2c8a50
Author: Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 30 14:06:16 2019 +0000
Commit: Michael Meeks <michael.meeks at collabora.com>
CommitDate: Sat Mar 30 16:51:06 2019 +0000
Switch local prisoner sockets to abstract UDS
Unix Domain Sockets are inaddressable remotely, and more efficient,
as well as allowing future SCM_CREDENTIALS / SCM_RIGHTS.
Change-Id: Ia2472260f75feb43e9022cdfa0fe005ccd489454
diff --git a/common/Common.hpp b/common/Common.hpp
index 013b30798..af37a77dd 100644
--- a/common/Common.hpp
+++ b/common/Common.hpp
@@ -7,12 +7,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+#include <string>
+
// Default values and other shared data between processes.
#ifndef INCLUDED_COMMON_HPP
#define INCLUDED_COMMON_HPP
constexpr int DEFAULT_CLIENT_PORT_NUMBER = 9980;
-constexpr int DEFAULT_MASTER_PORT_NUMBER = 9981;
constexpr int COMMAND_TIMEOUT_MS = 5000;
constexpr long CHILD_TIMEOUT_MS = COMMAND_TIMEOUT_MS;
@@ -46,7 +47,7 @@ constexpr const char* WOPI_AGENT_STRING = "LOOLWSD WOPI Agent " LOOLWSD_VERSION;
// The client port number, both loolwsd and the kits have this.
extern int ClientPortNumber;
-extern int MasterPortNumber;
+extern std::string MasterLocation;
#endif
diff --git a/common/UnitHTTP.hpp b/common/UnitHTTP.hpp
index d87ce8e3f..2eb1c629f 100644
--- a/common/UnitHTTP.hpp
+++ b/common/UnitHTTP.hpp
@@ -74,7 +74,7 @@ public:
UnitHTTPServerRequest(UnitHTTPServerResponse& inResponse,
const std::string& uri) :
_response(inResponse),
- _serverAddress(MasterPortNumber)
+ _serverAddress(9981) // FIXME: Unix Sockets now ...
{
setURI(uri);
}
diff --git a/kit/ForKit.cpp b/kit/ForKit.cpp
index 38c15d215..1cbf59a13 100644
--- a/kit/ForKit.cpp
+++ b/kit/ForKit.cpp
@@ -60,7 +60,7 @@ static std::map<Process::PID, std::string> childJails;
#ifndef KIT_IN_PROCESS
int ClientPortNumber = DEFAULT_CLIENT_PORT_NUMBER;
-int MasterPortNumber = DEFAULT_MASTER_PORT_NUMBER;
+std::string MasterLocation;
#endif
/// Dispatcher class to demultiplex requests from
@@ -401,7 +401,7 @@ int main(int argc, char** argv)
ClientPortNumber = std::stoi(clientPort);
static const char* masterPort = std::getenv("LOOL_TEST_MASTER_PORT");
if (masterPort)
- MasterPortNumber = std::stoi(masterPort);
+ MasterLocation = masterPort;
#endif
for (int i = 0; i < argc; ++i)
@@ -436,7 +436,7 @@ int main(int argc, char** argv)
else if (std::strstr(cmd, "--masterport=") == cmd)
{
eq = std::strchr(cmd, '=');
- MasterPortNumber = std::stoll(std::string(eq+1));
+ MasterLocation = std::string(eq+1);
}
else if (std::strstr(cmd, "--version") == cmd)
{
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index faf6ad4f8..819fb2698 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -2486,20 +2486,21 @@ void lokit_main(
LOG_INF("Process is ready.");
static const std::string pid = std::to_string(Process::id());
-
- Poco::URI uri("ws://127.0.0.1");
- uri.setPort(MasterPortNumber);
- uri.setPath(NEW_CHILD_URI);
- uri.addQueryParameter("pid", std::to_string(Process::id()));
- uri.addQueryParameter("jailid", jailId);
-
+ std::string pathAndQuery(NEW_CHILD_URI);
+ pathAndQuery.append("?pid=");
+ pathAndQuery.append(pid);
+ pathAndQuery.append("&jailid=");
+ pathAndQuery.append(jailId);
if (queryVersion)
{
char* versionInfo = loKit->getVersionInfo();
std::string versionString(versionInfo);
if (displayVersion)
std::cout << "office version details: " << versionString << std::endl;
- uri.addQueryParameter("version", versionString);
+ std::string encodedVersion;
+ Poco::URI::encode(versionString, "?#/", encodedVersion);
+ pathAndQuery.append("&version=");
+ pathAndQuery.append(encodedVersion);
free(versionInfo);
}
@@ -2528,10 +2529,12 @@ void lokit_main(
SocketPoll mainKit("kit");
mainKit.runOnClientThread(); // We will do the polling on this thread.
+ std::shared_ptr<SocketHandlerInterface> websocketHandler =
+ std::make_shared<KitWebSocketHandler>("child_ws_" + pid, loKit, jailId, mainKit);
#if !MOBILEAPP
- mainKit.insertNewWebSocketSync(uri, std::make_shared<KitWebSocketHandler>("child_ws_" + pid, loKit, jailId, mainKit));
+ mainKit.insertNewUnixSocket(MasterLocation, pathAndQuery, websocketHandler);
#else
- mainKit.insertNewWebSocketSync(docBrokerSocket, std::make_shared<KitWebSocketHandler>("child_ws_" + pid, loKit, jailId, mainKit));
+ mainKit.insertNewFakeSocketSync(docBrokerSocket, websocketHandler);
#endif
LOG_INF("New kit client websocket inserted.");
diff --git a/net/ServerSocket.hpp b/net/ServerSocket.hpp
index eb0bb46ca..79a7795bc 100644
--- a/net/ServerSocket.hpp
+++ b/net/ServerSocket.hpp
@@ -35,12 +35,13 @@ public:
{
}
+ /// Control access to a bound TCP socket
enum Type { Local, Public };
/// Binds to a local address (Servers only).
/// Does not retry on error.
/// Returns true only on success.
- bool bind(Type type, int port);
+ virtual bool bind(Type type, int port);
/// Listen to incoming connections (Servers only).
/// Does not retry on error.
@@ -145,6 +146,21 @@ private:
std::shared_ptr<SocketFactory> _sockFactory;
};
+/// A non-blocking, streaming Unix Domain Socket for local use
+class LocalServerSocket : public ServerSocket
+{
+public:
+ LocalServerSocket(SocketPoll& clientPoller, std::shared_ptr<SocketFactory> sockFactory) :
+ ServerSocket(Socket::Type::Unix, clientPoller, sockFactory)
+ {
+ }
+ virtual bool bind(Type, int) { assert(false); return false; }
+ std::string bind();
+
+private:
+ std::string _name;
+};
+
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/net/Socket.cpp b/net/Socket.cpp
index 8d2cd3533..a1762352e 100644
--- a/net/Socket.cpp
+++ b/net/Socket.cpp
@@ -37,10 +37,20 @@ int SocketPoll::DefaultPollTimeoutMs = 5000;
std::atomic<bool> SocketPoll::InhibitThreadChecks(false);
std::atomic<bool> Socket::InhibitThreadChecks(false);
+#define SOCKET_ABSTRACT_UNIX_NAME "0loolwsd-"
+
int Socket::createSocket(Socket::Type type)
{
#if !MOBILEAPP
- int domain = type == Type::IPv4 ? AF_INET : AF_INET6;
+ int domain;
+ switch (type)
+ {
+ case Type::IPv4: domain = AF_INET; break;
+ case Type::IPv6: domain = AF_INET6; break;
+ case Type::All: domain = AF_INET6; break;
+ case Type::Unix: domain = AF_UNIX; break;
+ default: assert (false); break;
+ }
return socket(domain, SOCK_STREAM | SOCK_NONBLOCK, 0);
#else
return fakeSocketSocket();
@@ -190,14 +200,9 @@ void SocketPoll::wakeupWorld()
}
void SocketPoll::insertNewWebSocketSync(
-#if !MOBILEAPP
- const Poco::URI &uri,
-#else
- int peerSocket,
-#endif
- const std::shared_ptr<SocketHandlerInterface>& websocketHandler)
+ const Poco::URI &uri,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler)
{
-#if !MOBILEAPP
LOG_INF("Connecting to " << uri.getHost() << " : " << uri.getPort() << " : " << uri.getPath());
// FIXME: put this in a ClientSocket class ?
@@ -247,24 +252,7 @@ void SocketPoll::insertNewWebSocketSync(
if (socket)
{
LOG_DBG("Connected to client websocket " << uri.getHost() << " #" << socket->getFD());
-
- // cf. WebSocketHandler::upgradeToWebSocket (?)
- // send Sec-WebSocket-Key: <hmm> ... Sec-WebSocket-Protocol: chat, Sec-WebSocket-Version: 13
-
- std::ostringstream oss;
- oss << "GET " << uri.getPathAndQuery() << " HTTP/1.1\r\n"
- "Connection:Upgrade\r\n"
- "User-Foo: Adminbits\r\n"
- "Sec-WebSocket-Key:fxTaWTEMVhq1PkWsMoLxGw==\r\n"
- "Upgrade:websocket\r\n"
- "Accept-Language:en\r\n"
- "Cache-Control:no-cache\r\n"
- "Pragma:no-cache\r\n"
- "Sec-WebSocket-Version:13\r\n"
- "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
- "\r\n";
- socket->send(oss.str());
- websocketHandler->onConnect(socket);
+ clientRequestWebsocketUpgrade(socket, websocketHandler, uri.getPathAndQuery());
insertNewSocket(socket);
}
else
@@ -282,7 +270,71 @@ void SocketPoll::insertNewWebSocketSync(
}
else
LOG_ERR("Failed to lookup client websocket host '" << uri.getHost() << "' skipping");
-#else
+}
+
+// should this be a static method in the WebsocketHandler(?)
+void SocketPoll::clientRequestWebsocketUpgrade(const std::shared_ptr<StreamSocket>& socket,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler,
+ const std::string &pathAndQuery)
+{
+ // cf. WebSocketHandler::upgradeToWebSocket (?)
+ // send Sec-WebSocket-Key: <hmm> ... Sec-WebSocket-Protocol: chat, Sec-WebSocket-Version: 13
+
+ LOG_TRC("Requesting upgrade of websocket at path " << pathAndQuery << " #" << socket->getFD());
+
+ std::ostringstream oss;
+ oss << "GET " << pathAndQuery << " HTTP/1.1\r\n"
+ "Connection:Upgrade\r\n"
+ "User-Foo: Adminbits\r\n"
+ "Sec-WebSocket-Key:fxTaWTEMVhq1PkWsMoLxGw==\r\n"
+ "Upgrade:websocket\r\n"
+ "Accept-Language:en\r\n"
+ "Cache-Control:no-cache\r\n"
+ "Pragma:no-cache\r\n"
+ "Sec-WebSocket-Version:13\r\n"
+ "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+ "\r\n";
+ socket->send(oss.str());
+ websocketHandler->onConnect(socket);
+}
+
+void SocketPoll::insertNewUnixSocket(
+ const std::string &location,
+ const std::string &pathAndQuery,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler)
+{
+ int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
+
+ struct sockaddr_un addrunix;
+ std::memset(&addrunix, 0, sizeof(addrunix));
+ addrunix.sun_family = AF_UNIX;
+ addrunix.sun_path[0] = '\0'; // abstract name
+ memcpy(&addrunix.sun_path[1], location.c_str(), location.length());
+
+ int res = connect(fd, (const struct sockaddr *)&addrunix, sizeof(addrunix));
+ if (fd < 0 || (res < 0 && errno != EINPROGRESS))
+ {
+ LOG_ERR("Failed to connect to unix socket at " << location);
+ ::close(fd);
+ }
+ else
+ {
+ std::shared_ptr<StreamSocket> socket;
+ socket = StreamSocket::create<StreamSocket>(fd, true, websocketHandler);
+ if (socket)
+ {
+ LOG_DBG("Connected to local UDS " << location << " #" << socket->getFD());
+ clientRequestWebsocketUpgrade(socket, websocketHandler, pathAndQuery);
+ insertNewSocket(socket);
+ }
+ }
+}
+
+#if MOBILEAPP
+void SocketPoll::insertNewFakeSocket(
+ int peerSocket,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler)
+{
LOG_INF("Connecting to " << peerSocket);
int fd = fakeSocketSocket();
int res = fakeSocketConnect(fd, peerSocket);
@@ -307,8 +359,8 @@ void SocketPoll::insertNewWebSocketSync(
fakeSocketClose(fd);
}
}
-#endif
}
+#endif
void ServerSocket::dumpState(std::ostream& os)
{
@@ -390,6 +442,7 @@ bool ServerSocket::bind(Type type, int port)
int rc;
+ assert (_type != Socket::Type::Unix);
if (_type == Socket::Type::IPv4)
{
struct sockaddr_in addrv4;
@@ -430,6 +483,33 @@ bool ServerSocket::bind(Type type, int port)
#endif
}
+/// Returns true on success only.
+std::string LocalServerSocket::bind()
+{
+#if !MOBILEAPP
+ int rc;
+ struct sockaddr_un addrunix;
+ do
+ {
+ std::memset(&addrunix, 0, sizeof(addrunix));
+ addrunix.sun_family = AF_UNIX;
+ std::memcpy(addrunix.sun_path, SOCKET_ABSTRACT_UNIX_NAME, sizeof(SOCKET_ABSTRACT_UNIX_NAME));
+ addrunix.sun_path[0] = '\0'; // abstract name
+
+ std::string rand = Util::rng::getFilename(8);
+ memcpy(addrunix.sun_path + sizeof(SOCKET_ABSTRACT_UNIX_NAME) - 1, rand.c_str(), 8);
+
+ rc = ::bind(getFD(), (const sockaddr *)&addrunix, sizeof(struct sockaddr_un));
+ LOG_TRC("Bind to location " << std::string(&addrunix.sun_path[1]) <<
+ " result - " << rc << "errno: " << ((rc >= 0) ? "no error" : ::strerror(errno)));
+ } while (rc < 0 && errno == EADDRINUSE);
+
+ if (rc >= 0)
+ return std::string(&addrunix.sun_path[1]);
+#endif
+ return "";
+}
+
#if !MOBILEAPP
bool StreamSocket::parseHeader(const char *clientName,
diff --git a/net/Socket.hpp b/net/Socket.hpp
index 1a074ce2d..10460fe4b 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -101,7 +101,7 @@ public:
static const int MaximumSendBufferSize = 128 * 1024;
static std::atomic<bool> InhibitThreadChecks;
- enum Type { IPv4, IPv6, All };
+ enum Type { IPv4, IPv6, All, Unix };
Socket(Type type) :
_fd(createSocket(type)),
@@ -642,15 +642,21 @@ public:
}
}
- /// Inserts a new websocket to be polled.
- /// NOTE: The DNS lookup is synchronous.
- void insertNewWebSocketSync(
#if !MOBILEAPP
- const Poco::URI &uri,
+ /// Inserts a new remote websocket to be polled.
+ /// NOTE: The DNS lookup is synchronous.
+ void insertNewWebSocketSync(const Poco::URI &uri,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler);
+
+ void insertNewUnixSocket(
+ const std::string &location,
+ const std::string &pathAndQuery,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler);
#else
- int peerSocket,
+ void insertNewFakeSocket(
+ int peerSocket,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler);
#endif
- const std::shared_ptr<SocketHandlerInterface>& websocketHandler);
typedef std::function<void()> CallbackFn;
@@ -718,6 +724,11 @@ protected:
}
private:
+ /// Generate the request to connect & upgrade this socket to a given path
+ void clientRequestWebsocketUpgrade(const std::shared_ptr<StreamSocket>& socket,
+ const std::shared_ptr<SocketHandlerInterface>& websocketHandler,
+ const std::string &pathAndQuery);
+
/// Initialize the poll fds array with the right events
void setupPollFds(std::chrono::steady_clock::time_point now,
int &timeoutMaxMs)
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 095795cc1..0b3ccfe86 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -188,8 +188,10 @@ Socket::Type ClientPortProto = Socket::Type::All;
/// INET address to listen on
ServerSocket::Type ClientListenAddr = ServerSocket::Type::Public;
-/// Port for prisoners to connect to
-int MasterPortNumber = DEFAULT_MASTER_PORT_NUMBER;
+#if !MOBILEAPP
+/// UDS address for kits to connect to.
+std::string MasterLocation;
+#endif
// Tracks the set of prisoners / children waiting to be used.
static std::mutex NewChildrenMutex;
@@ -1255,8 +1257,7 @@ void LOOLWSD::defineOptions(OptionSet& optionSet)
.repeatable(false));
optionSet.addOption(Option("port", "", "Port number to listen to (default: " +
- std::to_string(DEFAULT_CLIENT_PORT_NUMBER) + "),"
- " must not be " + std::to_string(MasterPortNumber) + ".")
+ std::to_string(DEFAULT_CLIENT_PORT_NUMBER) + "),")
.required(false)
.repeatable(false)
.argument("port_number"));
@@ -1342,10 +1343,6 @@ void LOOLWSD::handleOption(const std::string& optionName,
if (clientPort)
ClientPortNumber = std::stoi(clientPort);
- static const char* masterPort = std::getenv("LOOL_TEST_MASTER_PORT");
- if (masterPort)
- MasterPortNumber = std::stoi(masterPort);
-
static const char* latencyMs = std::getenv("LOOL_DELAY_SOCKET_MS");
if (latencyMs)
SimulatedLatencyMs = std::stoi(latencyMs);
@@ -1556,7 +1553,7 @@ bool LOOLWSD::createForKit()
args.push_back("--lotemplate=" + LoTemplate);
args.push_back("--childroot=" + ChildRoot);
args.push_back("--clientport=" + std::to_string(ClientPortNumber));
- args.push_back("--masterport=" + std::to_string(MasterPortNumber));
+ args.push_back("--masterport=" + MasterLocation);
const DocProcSettings& docProcSettings = Admin::instance().getDefDocProcSettings();
std::ostringstream ossRLimits;
@@ -2869,10 +2866,10 @@ public:
stop();
}
- void startPrisoners(int& port)
+ void startPrisoners()
{
PrisonerPoll.startThread();
- PrisonerPoll.insertNewSocket(findPrisonerServerPort(port));
+ PrisonerPoll.insertNewSocket(findPrisonerServerPort());
}
void stopPrisoners()
@@ -2911,7 +2908,7 @@ public:
os << "LOOLWSDServer:\n"
<< " Ports: server " << ClientPortNumber
- << " prisoner " << MasterPortNumber << "\n"
+ << " prisoner " << MasterLocation << "\n"
<< " SSL: " << (LOOLWSD::isSSLEnabled() ? "https" : "http") << "\n"
<< " SSL-Termination: " << (LOOLWSD::isSSLTermination() ? "yes" : "no") << "\n"
<< " Security " << (LOOLWSD::NoCapsForKit ? "no" : "") << " chroot, "
@@ -2974,8 +2971,7 @@ private:
const std::shared_ptr<SocketFactory>& factory)
{
auto serverSocket = std::make_shared<ServerSocket>(
- type == ServerSocket::Type::Local ? Socket::Type::IPv4 : ClientPortProto,
- clientSocket, factory);
+ ClientPortProto, clientSocket, factory);
if (!serverSocket->bind(type, port))
return nullptr;
@@ -2987,35 +2983,37 @@ private:
}
/// Create the internal only, local socket for forkit / kits prisoners to talk to.
- std::shared_ptr<ServerSocket> findPrisonerServerPort(int& port)
+ std::shared_ptr<ServerSocket> findPrisonerServerPort()
{
std::shared_ptr<SocketFactory> factory = std::make_shared<PrisonerSocketFactory>();
- std::shared_ptr<ServerSocket> socket = getServerSocket(
- ServerSocket::Type::Local, port, PrisonerPoll, factory);
+#if !MOBILEAPP
+ std::string location;
+ auto socket = std::make_shared<LocalServerSocket>(PrisonerPoll, factory);;
-#ifdef BUILDING_TESTS
- // If we fail, try the next 100 ports.
- for (int i = 0; i < 100 && !socket; ++i)
+ location = socket->bind();
+ if (!location.length())
{
- ++port;
- LOG_INF("Prisoner port " << (port - 1) << " is busy, trying " << port << ".");
- socket = getServerSocket(
- ServerSocket::Type::Local, port, PrisonerPoll, factory);
+ LOG_FTL("Failed to create local unix domain socket. Exiting.");
+ Log::shutdown();
+ _exit(Application::EXIT_SOFTWARE);
+ return nullptr;
}
-#endif
- if (!socket)
+ if (!socket->listen())
{
- LOG_FTL("Failed to listen on Prisoner port(s) (" <<
- MasterPortNumber << '-' << port << "). Exiting.");
+ LOG_FTL("Failed to listen on local unix domain socket at " << location << ". Exiting.");
Log::shutdown();
_exit(Application::EXIT_SOFTWARE);
}
- MasterPortNumber = port;
-#if !MOBILEAPP
- LOG_INF("Listening to prisoner connections on port " << port);
+ LOG_INF("Listening to prisoner connections on " << location);
+ MasterLocation = location;
#else
+ // TESTME ...
+ constexpr int DEFAULT_MASTER_PORT_NUMBER = 9981;
+ std::shared_ptr<ServerSocket> socket = getServerSocket(
+ ServerSocket::Type::Public, DEFAULT_MASTER_PORT_NUMBER, PrisonerPoll, factory);
+
LOOLWSD::prisonerServerSocketFD = socket->getFD();
LOG_INF("Listening to prisoner connections on #" << LOOLWSD::prisonerServerSocketFD);
#endif
@@ -3041,8 +3039,7 @@ private:
{
++port;
LOG_INF("Client port " << (port - 1) << " is busy, trying " << port << ".");
- socket = getServerSocket(
- ServerSocket::Type::Public, port, WebServerPoll, factory);
+ socket = getServerSocket(port, WebServerPoll, factory);
}
#endif
@@ -3138,16 +3135,13 @@ int LOOLWSD::innerMain()
FileServerRoot = Poco::Path(Application::instance().commandPath()).parent().toString();
FileServerRoot = Poco::Path(FileServerRoot).absolute().toString();
LOG_DBG("FileServerRoot: " << FileServerRoot);
-
- if (ClientPortNumber == MasterPortNumber)
- throw IncompatibleOptionsException("port");
#endif
ClientRequestDispatcher::InitStaticFileContentCache();
// Start the internal prisoner server and spawn forkit,
// which in turn forks first child.
- srv.startPrisoners(MasterPortNumber);
+ srv.startPrisoners();
// No need to "have at least one child" beforehand on mobile
#if !MOBILEAPP
More information about the Libreoffice-commits
mailing list