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