1 /* === This file is part of Calamares - <https://calamares.io> ===
2 *
3 * SPDX-FileCopyrightText: 2014-2017 Teo Mrnjavac <teo@kde.org>
4 * SPDX-FileCopyrightText: 2017-2018 2020, Adriaan de Groot <groot@kde.org>
5 * SPDX-FileCopyrightText: 2017 Gabriel Craciunescu <crazy@frugalware.org>
6 * SPDX-FileCopyrightText: 2019 Collabora Ltd <arnaud.ferraris@collabora.com>
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 *
9 * Calamares is Free Software: see the License-Identifier above.
10 *
11 */
12
13 #include "GeneralRequirements.h"
14
15 #include "CheckerContainer.h"
16 #include "partman_devices.h"
17
18 #include "Settings.h"
19 #include "modulesystem/Requirement.h"
20 #include "network/Manager.h"
21 #include "utils/CalamaresUtilsGui.h"
22 #include "utils/CalamaresUtilsSystem.h"
23 #include "utils/Logger.h"
24 #include "utils/Retranslator.h"
25 #include "utils/Units.h"
26 #include "utils/Variant.h"
27 #include "widgets/WaitingWidget.h"
28
29 #include "GlobalStorage.h"
30 #include "JobQueue.h"
31
32 #include <QDBusConnection>
33 #include <QDBusInterface>
34 #include <QDir>
35 #include <QFile>
36 #include <QFileInfo>
37 #include <QGuiApplication>
38 #include <QScreen>
39
40 #include <unistd.h> //geteuid
41
GeneralRequirements(QObject * parent)42 GeneralRequirements::GeneralRequirements( QObject* parent )
43 : QObject( parent )
44 , m_requiredStorageGiB( -1 )
45 , m_requiredRamGiB( -1 )
46 {
47 }
48
49 static QSize
biggestSingleScreen()50 biggestSingleScreen()
51 {
52 QSize s;
53 for ( const auto* screen : QGuiApplication::screens() )
54 {
55 QSize thisScreen = screen->availableSize();
56 if ( !s.isValid() || ( s.width() * s.height() < thisScreen.width() * thisScreen.height() ) )
57 {
58 s = thisScreen;
59 }
60 }
61 return s;
62 }
63
64 /** @brief Distinguish has-not-been-checked-at-all from false.
65 *
66 */
67 struct MaybeChecked
68 {
69 bool hasBeenChecked = false;
70 bool value = false;
71
operator =MaybeChecked72 MaybeChecked& operator=( bool b )
73 {
74 hasBeenChecked = true;
75 value = b;
76 return *this;
77 }
78
operator boolMaybeChecked79 operator bool() const { return value; }
80 };
81
82 QDebug&
operator <<(QDebug & s,const MaybeChecked & c)83 operator<<( QDebug& s, const MaybeChecked& c )
84 {
85 if ( c.hasBeenChecked )
86 {
87 s << c.value;
88 }
89 else
90 {
91 s << "unchecked";
92 }
93 return s;
94 }
95
96 Calamares::RequirementsList
checkRequirements()97 GeneralRequirements::checkRequirements()
98 {
99 QSize availableSize = biggestSingleScreen();
100
101 MaybeChecked enoughStorage;
102 MaybeChecked enoughRam;
103 MaybeChecked hasPower;
104 MaybeChecked hasInternet;
105 MaybeChecked isRoot;
106 bool enoughScreen = availableSize.isValid() && ( availableSize.width() >= CalamaresUtils::windowMinimumWidth )
107 && ( availableSize.height() >= CalamaresUtils::windowMinimumHeight );
108
109 qint64 requiredStorageB = CalamaresUtils::GiBtoBytes( m_requiredStorageGiB );
110 if ( m_entriesToCheck.contains( "storage" ) )
111 {
112 enoughStorage = checkEnoughStorage( requiredStorageB );
113 }
114
115 qint64 requiredRamB = CalamaresUtils::GiBtoBytes( m_requiredRamGiB );
116 if ( m_entriesToCheck.contains( "ram" ) )
117 {
118 enoughRam = checkEnoughRam( requiredRamB );
119 }
120
121 if ( m_entriesToCheck.contains( "power" ) )
122 {
123 hasPower = checkHasPower();
124 }
125
126 if ( m_entriesToCheck.contains( "internet" ) )
127 {
128 hasInternet = checkHasInternet();
129 }
130
131 if ( m_entriesToCheck.contains( "root" ) )
132 {
133 isRoot = checkIsRoot();
134 }
135
136 using TNum = Logger::DebugRow< const char*, qint64 >;
137 using TR = Logger::DebugRow< const char*, MaybeChecked >;
138 // clang-format off
139 cDebug() << "GeneralRequirements output:"
140 << TNum( "storage", requiredStorageB )
141 << TR( "enoughStorage", enoughStorage )
142 << TNum( "RAM", requiredRamB )
143 << TR( "enoughRam", enoughRam )
144 << TR( "hasPower", hasPower )
145 << TR( "hasInternet", hasInternet )
146 << TR( "isRoot", isRoot );
147 // clang-format on
148 Calamares::RequirementsList checkEntries;
149 foreach ( const QString& entry, m_entriesToCheck )
150 {
151 if ( entry == "storage" )
152 {
153 checkEntries.append(
154 { entry,
155 [req = m_requiredStorageGiB] { return tr( "has at least %1 GiB available drive space" ).arg( req ); },
156 [req = m_requiredStorageGiB] {
157 return tr( "There is not enough drive space. At least %1 GiB is required." ).arg( req );
158 },
159 enoughStorage,
160 m_entriesToRequire.contains( entry ) } );
161 }
162 else if ( entry == "ram" )
163 {
164 checkEntries.append(
165 { entry,
166 [req = m_requiredRamGiB] { return tr( "has at least %1 GiB working memory" ).arg( req ); },
167 [req = m_requiredRamGiB] {
168 return tr( "The system does not have enough working memory. At least %1 GiB is required." )
169 .arg( req );
170 },
171 enoughRam,
172 m_entriesToRequire.contains( entry ) } );
173 }
174 else if ( entry == "power" )
175 {
176 checkEntries.append( { entry,
177 [] { return tr( "is plugged in to a power source" ); },
178 [] { return tr( "The system is not plugged in to a power source." ); },
179 hasPower,
180 m_entriesToRequire.contains( entry ) } );
181 }
182 else if ( entry == "internet" )
183 {
184 checkEntries.append( { entry,
185 [] { return tr( "is connected to the Internet" ); },
186 [] { return tr( "The system is not connected to the Internet." ); },
187 hasInternet,
188 m_entriesToRequire.contains( entry ) } );
189 }
190 else if ( entry == "root" )
191 {
192 checkEntries.append( { entry,
193 [] { return tr( "is running the installer as an administrator (root)" ); },
194 [] {
195 return Calamares::Settings::instance()->isSetupMode()
196 ? tr( "The setup program is not running with administrator rights." )
197 : tr( "The installer is not running with administrator rights." );
198 },
199 isRoot,
200 m_entriesToRequire.contains( entry ) } );
201 }
202 else if ( entry == "screen" )
203 {
204 checkEntries.append( { entry,
205 [] { return tr( "has a screen large enough to show the whole installer" ); },
206 [] {
207 return Calamares::Settings::instance()->isSetupMode()
208 ? tr( "The screen is too small to display the setup program." )
209 : tr( "The screen is too small to display the installer." );
210 },
211 enoughScreen,
212 false } );
213 }
214 }
215 return checkEntries;
216 }
217
218 /** @brief Loads the check-internet URLs
219 *
220 * There may be zero or one or more URLs specified; returns
221 * @c true if the configuration is incomplete or damaged in some way.
222 */
223 static bool
getCheckInternetUrls(const QVariantMap & configurationMap)224 getCheckInternetUrls( const QVariantMap& configurationMap )
225 {
226 const QString exampleUrl = QStringLiteral( "http://example.com" );
227
228 bool incomplete = false;
229 QStringList checkInternetSetting = CalamaresUtils::getStringList( configurationMap, "internetCheckUrl" );
230 if ( !checkInternetSetting.isEmpty() )
231 {
232 QVector< QUrl > urls;
233 for ( const auto& urlString : qAsConst( checkInternetSetting ) )
234 {
235 QUrl url( urlString.trimmed() );
236 if ( url.isValid() )
237 {
238 urls.append( url );
239 }
240 else
241 {
242 cWarning() << "GeneralRequirements entry 'internetCheckUrl' in welcome.conf contains invalid"
243 << urlString;
244 }
245 }
246
247 if ( urls.empty() )
248 {
249 cWarning() << "GeneralRequirements entry 'internetCheckUrl' contains no valid URLs, "
250 << "reverting to default (" << exampleUrl << ").";
251 CalamaresUtils::Network::Manager::instance().setCheckHasInternetUrl( QUrl( exampleUrl ) );
252 incomplete = true;
253 }
254 else
255 {
256 CalamaresUtils::Network::Manager::instance().setCheckHasInternetUrl( urls );
257 }
258 }
259 else
260 {
261 cWarning() << "GeneralRequirements entry 'internetCheckUrl' is undefined in welcome.conf, "
262 "reverting to default ("
263 << exampleUrl << ").";
264 CalamaresUtils::Network::Manager::instance().setCheckHasInternetUrl( QUrl( exampleUrl ) );
265 incomplete = true;
266 }
267 return incomplete;
268 }
269
270
271 void
setConfigurationMap(const QVariantMap & configurationMap)272 GeneralRequirements::setConfigurationMap( const QVariantMap& configurationMap )
273 {
274 bool incompleteConfiguration = false;
275
276 if ( configurationMap.contains( "check" ) && configurationMap.value( "check" ).type() == QVariant::List )
277 {
278 m_entriesToCheck.clear();
279 m_entriesToCheck.append( configurationMap.value( "check" ).toStringList() );
280 }
281 else
282 {
283 cWarning() << "GeneralRequirements entry 'check' is incomplete.";
284 incompleteConfiguration = true;
285 }
286
287 if ( configurationMap.contains( "required" ) && configurationMap.value( "required" ).type() == QVariant::List )
288 {
289 m_entriesToRequire.clear();
290 m_entriesToRequire.append( configurationMap.value( "required" ).toStringList() );
291 }
292 else
293 {
294 cWarning() << "GeneralRequirements entry 'required' is incomplete.";
295 incompleteConfiguration = true;
296 }
297
298 #ifdef WITHOUT_LIBPARTED
299 if ( m_entriesToCheck.contains( "storage" ) || m_entriesToRequire.contains( "storage" ) )
300 {
301 // Warn, but also drop the required bit because otherwise installation
302 // will be impossible (because the check always returns false).
303 cWarning() << "GeneralRequirements checks 'storage' but libparted is disabled.";
304 m_entriesToCheck.removeAll( "storage" );
305 m_entriesToRequire.removeAll( "storage" );
306 }
307 #endif
308
309 // Help out with consistency, but don't fix
310 for ( const auto& r : m_entriesToRequire )
311 if ( !m_entriesToCheck.contains( r ) )
312 {
313 cWarning() << "GeneralRequirements requires" << r << "but does not check it.";
314 }
315
316 if ( configurationMap.contains( "requiredStorage" )
317 && ( configurationMap.value( "requiredStorage" ).type() == QVariant::Double
318 || configurationMap.value( "requiredStorage" ).type() == QVariant::LongLong ) )
319 {
320 bool ok = false;
321 m_requiredStorageGiB = configurationMap.value( "requiredStorage" ).toDouble( &ok );
322 if ( !ok )
323 {
324 cWarning() << "GeneralRequirements entry 'requiredStorage' is invalid.";
325 m_requiredStorageGiB = 3.;
326 }
327
328 Calamares::JobQueue::instance()->globalStorage()->insert( "requiredStorageGiB", m_requiredStorageGiB );
329 }
330 else
331 {
332 cWarning() << "GeneralRequirements entry 'requiredStorage' is missing.";
333 m_requiredStorageGiB = 3.;
334 incompleteConfiguration = true;
335 }
336
337 if ( configurationMap.contains( "requiredRam" )
338 && ( configurationMap.value( "requiredRam" ).type() == QVariant::Double
339 || configurationMap.value( "requiredRam" ).type() == QVariant::LongLong ) )
340 {
341 bool ok = false;
342 m_requiredRamGiB = configurationMap.value( "requiredRam" ).toDouble( &ok );
343 if ( !ok )
344 {
345 cWarning() << "GeneralRequirements entry 'requiredRam' is invalid.";
346 m_requiredRamGiB = 1.;
347 incompleteConfiguration = true;
348 }
349 }
350 else
351 {
352 cWarning() << "GeneralRequirements entry 'requiredRam' is missing.";
353 m_requiredRamGiB = 1.;
354 incompleteConfiguration = true;
355 }
356
357 incompleteConfiguration |= getCheckInternetUrls( configurationMap );
358
359 if ( incompleteConfiguration )
360 {
361 cWarning() << "GeneralRequirements configuration map:" << Logger::DebugMap( configurationMap );
362 }
363 }
364
365
366 bool
checkEnoughStorage(qint64 requiredSpace)367 GeneralRequirements::checkEnoughStorage( qint64 requiredSpace )
368 {
369 #ifdef WITHOUT_LIBPARTED
370 Q_UNUSED( requiredSpace )
371 cWarning() << "GeneralRequirements is configured without libparted.";
372 return false;
373 #else
374 return check_big_enough( requiredSpace );
375 #endif
376 }
377
378
379 bool
checkEnoughRam(qint64 requiredRam)380 GeneralRequirements::checkEnoughRam( qint64 requiredRam )
381 {
382 // Ignore the guesstimate-factor; we get an under-estimate
383 // which is probably the usable RAM for programs.
384 quint64 availableRam = CalamaresUtils::System::instance()->getTotalMemoryB().first;
385 return double( availableRam ) >= double( requiredRam ) * 0.95; // cast to silence 64-bit-int conversion to double
386 }
387
388
389 bool
checkBatteryExists()390 GeneralRequirements::checkBatteryExists()
391 {
392 const QFileInfo basePath( "/sys/class/power_supply" );
393
394 if ( !( basePath.exists() && basePath.isDir() ) )
395 {
396 return false;
397 }
398
399 QDir baseDir( basePath.absoluteFilePath() );
400 const auto entries = baseDir.entryList( QDir::AllDirs | QDir::Readable | QDir::NoDotAndDotDot );
401 for ( const auto& item : entries )
402 {
403 QFileInfo typePath( baseDir.absoluteFilePath( QString( "%1/type" ).arg( item ) ) );
404 QFile typeFile( typePath.absoluteFilePath() );
405 if ( typeFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
406 {
407 if ( typeFile.readAll().startsWith( "Battery" ) )
408 {
409 return true;
410 }
411 }
412 }
413
414 return false;
415 }
416
417
418 bool
checkHasPower()419 GeneralRequirements::checkHasPower()
420 {
421 const QString UPOWER_SVC_NAME( "org.freedesktop.UPower" );
422 const QString UPOWER_INTF_NAME( "org.freedesktop.UPower" );
423 const QString UPOWER_PATH( "/org/freedesktop/UPower" );
424
425 if ( !checkBatteryExists() )
426 {
427 return true;
428 }
429
430 cDebug() << "A battery exists, checking for mains power.";
431 QDBusInterface upowerIntf( UPOWER_SVC_NAME, UPOWER_PATH, UPOWER_INTF_NAME, QDBusConnection::systemBus() );
432
433 bool onBattery = upowerIntf.property( "OnBattery" ).toBool();
434
435 if ( !upowerIntf.isValid() )
436 {
437 // We can't talk to upower but we're obviously up and running
438 // so I guess we got that going for us, which is nice...
439 return true;
440 }
441
442 // If a battery exists but we're not using it, means we got mains
443 // power.
444 return !onBattery;
445 }
446
447
448 bool
checkHasInternet()449 GeneralRequirements::checkHasInternet()
450 {
451 auto& nam = CalamaresUtils::Network::Manager::instance();
452 bool hasInternet = nam.checkHasInternet();
453 Calamares::JobQueue::instance()->globalStorage()->insert( "hasInternet", hasInternet );
454 return hasInternet;
455 }
456
457
458 bool
checkIsRoot()459 GeneralRequirements::checkIsRoot()
460 {
461 return !geteuid();
462 }
463