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