1 /*
2  * DesktopInfo.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "DesktopInfo.hpp"
17 
18 #include <atomic>
19 #include <set>
20 
21 #include <boost/algorithm/string.hpp>
22 
23 #include <QThread>
24 
25 #include <core/Algorithm.hpp>
26 #include <shared_core/SafeConvert.hpp>
27 #include <core/system/Process.hpp>
28 #include <core/system/Environment.hpp>
29 #include <core/system/Process.hpp>
30 #include <core/FileSerializer.hpp>
31 
32 #include "DesktopOptions.hpp"
33 #include "DesktopSynctex.hpp"
34 
35 #define kLsbRelease    "/etc/lsb-release"
36 #define kRedhatRelease "/etc/redhat-release"
37 #define kOsRelease     "/etc/os-release"
38 
39 #define kUnknown QStringLiteral("(unknown)")
40 
41 using namespace rstudio::core;
42 
43 namespace rstudio {
44 namespace desktop {
45 
46 namespace {
47 
48 #ifndef Q_OS_MAC
49 std::atomic<bool> s_abortRequested(false);
50 QThread* s_fontDatabaseWorker = nullptr;
51 #endif
52 
53 QString s_platform             = kUnknown;
54 QString s_version              = kUnknown;
55 QString s_sumatraPdfExePath    = kUnknown;
56 QString s_fixedWidthFontList   = QStringLiteral("");
57 int     s_chromiumDevtoolsPort = -1;
58 double  s_zoomLevel            = 1.0;
59 
60 #ifndef Q_OS_MAC
61 
buildFontDatabaseImpl()62 void buildFontDatabaseImpl()
63 {
64    QFontDatabase db;
65 
66    QStringList fontList;
67    for (const QString& family : db.families())
68    {
69       if (s_abortRequested)
70          return;
71 
72 #ifdef _WIN32
73       // screen out annoying Qt warnings when attempting to
74       // initialize incompatible fonts
75       static std::set<std::string> ignored = {
76          "8514oem",
77          "Fixedsys",
78          "Modern",
79          "MS Sans Serif",
80          "MS Serif",
81          "Roman",
82          "Script",
83          "Small Fonts",
84          "System",
85          "Terminal"
86       };
87 
88       if (ignored.count(family.toStdString()))
89          continue;
90 #endif
91 
92       if (isFixedWidthFont(QFont(family, 12)))
93          fontList.append(family);
94    }
95 
96    QString fonts = fontList.join(QStringLiteral("\n"));
97 
98    // NOTE: invokeMethod() is used to ensure thread-safe communication
99    QMetaObject::invokeMethod(
100             &desktopInfo(),
101             "setFixedWidthFontList",
102             Q_ARG(QString, fonts));
103 }
104 
buildFontDatabase()105 void buildFontDatabase()
106 {
107 #ifdef Q_OS_LINUX
108 
109    if (desktop::options().useFontConfigDatabase())
110    {
111       // if fontconfig is installed, we can use it to query monospace
112       // fonts using its own cache (and it should be much more performant
113       // than asking Qt to build the font database on demand)
114       core::system::ProcessOptions options;
115       core::system::ProcessResult result;
116       Error error = core::system::runCommand(
117                "{ fc-list :mono -f '%{family}\n' & fc-list :dual -f '%{family}\n' & fc-list :charcell -f '%{family}\n'; } | cut -d ',' -f 1 | sort | uniq",
118                options,
119                &result);
120 
121       bool didReturnFonts =
122             !error &&
123             result.exitStatus == EXIT_SUCCESS &&
124             !result.stdOut.empty();
125 
126       if (didReturnFonts)
127       {
128          s_fixedWidthFontList = QString::fromStdString(result.stdOut);
129          return;
130       }
131    }
132 
133 #endif
134 
135    s_fontDatabaseWorker = QThread::create(buildFontDatabaseImpl);
136    s_fontDatabaseWorker->start();
137 }
138 
139 #else
140 
buildFontDatabase()141 void buildFontDatabase()
142 {
143    s_fixedWidthFontList = desktop::getFixedWidthFontList();
144 }
145 
146 #endif
147 
148 #ifdef Q_OS_LINUX
149 
readEntry(const std::map<std::string,std::string> & entries,const char * key,QString * pOutput)150 void readEntry(
151       const std::map<std::string, std::string>& entries,
152       const char* key,
153       QString* pOutput)
154 {
155    if (entries.count(key))
156    {
157       *pOutput = QString::fromStdString(entries.at(key)).toLower();
158    }
159 }
160 
initializeLsbRelease()161 void initializeLsbRelease()
162 {
163    std::map<std::string, std::string> entries;
164    Error error = core::readStringMapFromFile(FilePath(kLsbRelease), &entries);
165 
166    if (error)
167       LOG_ERROR(error);
168 
169    readEntry(entries, "DISTRIB_ID", &s_platform);
170    readEntry(entries, "DISTRIB_RELEASE", &s_version);
171 }
172 
initializeRedhatRelease()173 void initializeRedhatRelease()
174 {
175    std::string contents;
176    Error error = core::readStringFromFile(
177             FilePath(kRedhatRelease),
178             &contents);
179    if (error)
180       LOG_ERROR(error);
181 
182    if (contents.find("CentOS") != std::string::npos)
183       s_platform = QStringLiteral("centos");
184    else if (contents.find("Red Hat Enterprise Linux"))
185       s_platform = QStringLiteral("rhel");
186 }
187 
initializeOsRelease()188 void initializeOsRelease()
189 {
190    std::map<std::string, std::string> entries;
191    Error error = core::readStringMapFromFile(
192             FilePath(kOsRelease),
193             &entries);
194 
195    if (error)
196       LOG_ERROR(error);
197 
198    readEntry(entries, "ID", &s_platform);
199    readEntry(entries, "VERSION_ID", &s_version);
200 }
201 
202 #endif /* Q_OS_LINUX */
203 
initialize()204 void initialize()
205 {
206    buildFontDatabase();
207 
208 #ifdef Q_OS_LINUX
209    if (FilePath(kOsRelease).exists())
210    {
211       initializeOsRelease();
212       return;
213    }
214 
215    if (FilePath(kLsbRelease).exists())
216    {
217       initializeLsbRelease();
218       return;
219    }
220 
221    if (FilePath(kRedhatRelease).exists())
222    {
223       initializeRedhatRelease();
224       return;
225    }
226 #endif
227 }
228 
229 } // end anonymous namespace
230 
onClose()231 void DesktopInfo::onClose()
232 {
233 #ifndef Q_OS_MAC
234    if (s_fontDatabaseWorker && s_fontDatabaseWorker->isRunning())
235    {
236       s_abortRequested = true;
237       s_fontDatabaseWorker->wait(1000);
238    }
239 #endif
240 }
241 
DesktopInfo(QObject * parent)242 DesktopInfo::DesktopInfo(QObject* parent)
243    : QObject(parent)
244 {
245    initialize();
246 }
247 
getPlatform()248 QString DesktopInfo::getPlatform()
249 {
250    return s_platform;
251 }
252 
getVersion()253 QString DesktopInfo::getVersion()
254 {
255    return s_version;
256 }
257 
desktopHooksAvailable()258 bool DesktopInfo::desktopHooksAvailable()
259 {
260    return true;
261 }
262 
getScrollingCompensationType()263 QString DesktopInfo::getScrollingCompensationType()
264 {
265 #if defined(Q_OS_WIN32)
266    return QStringLiteral("Win");
267 #elif defined(Q_OS_MAC)
268    return QStringLiteral("Mac");
269 #else
270    return QStringLiteral("None");
271 #endif
272 }
273 
getFixedWidthFontList()274 QString DesktopInfo::getFixedWidthFontList()
275 {
276    return s_fixedWidthFontList;
277 }
278 
setFixedWidthFontList(QString fontList)279 void DesktopInfo::setFixedWidthFontList(QString fontList)
280 {
281    if (s_fixedWidthFontList != fontList)
282    {
283       s_fixedWidthFontList = fontList;
284       emit fixedWidthFontListChanged(fontList);
285    }
286 }
287 
getFixedWidthFont()288 QString DesktopInfo::getFixedWidthFont()
289 {
290    return options().fixedWidthFont();
291 }
292 
setFixedWidthFont(QString font)293 void DesktopInfo::setFixedWidthFont(QString font)
294 {
295    if (font != options().fixedWidthFont())
296    {
297       options().setFixedWidthFont(font);
298       emit fixedWidthFontChanged(font);
299    }
300 }
301 
getProportionalFont()302 QString DesktopInfo::getProportionalFont()
303 {
304    return options().proportionalFont();
305 }
306 
setProportionalFont(QString font)307 void DesktopInfo::setProportionalFont(QString font)
308 {
309    if (font != options().proportionalFont())
310    {
311       options().setProportionalFont(font);
312       emit proportionalFontChanged(font);
313    }
314 }
315 
getDesktopSynctexViewer()316 QString DesktopInfo::getDesktopSynctexViewer()
317 {
318    return Synctex::desktopViewerInfo().name;
319 }
320 
setDesktopSynctexViewer(QString)321 void DesktopInfo::setDesktopSynctexViewer(QString)
322 {
323    qWarning() << "setDesktopSynctexViewer() not implemented";
324 }
325 
getSumatraPdfExePath()326 QString DesktopInfo::getSumatraPdfExePath()
327 {
328    return s_sumatraPdfExePath;
329 }
330 
setSumatraPdfExePath(QString path)331 void DesktopInfo::setSumatraPdfExePath(QString path)
332 {
333    if (s_sumatraPdfExePath != path)
334    {
335       s_sumatraPdfExePath = path;
336       emit sumatraPdfExePathChanged(path);
337    }
338 }
339 
getZoomLevel()340 double DesktopInfo::getZoomLevel()
341 {
342    return s_zoomLevel;
343 }
344 
setZoomLevel(double zoomLevel)345 void DesktopInfo::setZoomLevel(double zoomLevel)
346 {
347    if (zoomLevel != s_zoomLevel)
348    {
349       s_zoomLevel = zoomLevel;
350       emit zoomLevelChanged(zoomLevel);
351    }
352 }
353 
getChromiumDevtoolsPort()354 int DesktopInfo::getChromiumDevtoolsPort()
355 {
356    return s_chromiumDevtoolsPort;
357 }
358 
setChromiumDevtoolsPort(int port)359 void DesktopInfo::setChromiumDevtoolsPort(int port)
360 {
361    if (s_chromiumDevtoolsPort != port)
362    {
363       s_chromiumDevtoolsPort = port;
364       core::system::setenv("QT_WEBENGINE_REMOTE_DEBUGGING", safe_convert::numberToString(port));
365       emit chromiumDevtoolsPortChanged(port);
366    }
367 }
368 
369 } // end namespace desktop
370 } // end namespace rstudio
371