1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
4  *   SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
5  *   SPDX-License-Identifier: GPL-3.0-or-later
6  *
7  *   Calamares is Free Software: see the License-Identifier above.
8  *
9  */
10 
11 #include "ModuleManager.h"
12 
13 #include "ViewManager.h"
14 
15 #include "Settings.h"
16 #include "modulesystem/Module.h"
17 #include "modulesystem/RequirementsChecker.h"
18 #include "modulesystem/RequirementsModel.h"
19 #include "utils/Logger.h"
20 #include "utils/Yaml.h"
21 #include "viewpages/ExecutionViewStep.h"
22 
23 #include <QApplication>
24 #include <QDir>
25 #include <QTimer>
26 
27 namespace Calamares
28 {
29 ModuleManager* ModuleManager::s_instance = nullptr;
30 
31 ModuleManager*
instance()32 ModuleManager::instance()
33 {
34     return s_instance;
35 }
36 
37 
ModuleManager(const QStringList & paths,QObject * parent)38 ModuleManager::ModuleManager( const QStringList& paths, QObject* parent )
39     : QObject( parent )
40     , m_paths( paths )
41     , m_requirementsModel( new RequirementsModel( this ) )
42 {
43     Q_ASSERT( !s_instance );
44     s_instance = this;
45 }
46 
47 
~ModuleManager()48 ModuleManager::~ModuleManager()
49 {
50     // The map is populated with Module::fromDescriptor(), which allocates on the heap.
51     for ( auto moduleptr : m_loadedModulesByInstanceKey )
52     {
53         delete moduleptr;
54     }
55 }
56 
57 
58 void
init()59 ModuleManager::init()
60 {
61     QTimer::singleShot( 0, this, &ModuleManager::doInit );
62 }
63 
64 void
doInit()65 ModuleManager::doInit()
66 {
67     // We start from a list of paths in m_paths. Each of those is a directory that
68     // might (should) contain Calamares modules of any type/interface.
69     // For each modules search path (directory), it is expected that each module
70     // lives in its own subdirectory. This subdirectory must have the same name as
71     // the module name, and must contain a settings file named module.desc.
72     // If at any time the module loading procedure finds something unexpected, it
73     // silently skips to the next module or search path. --Teo 6/2014
74     Logger::Once deb;
75     for ( const QString& path : m_paths )
76     {
77         QDir currentDir( path );
78         if ( currentDir.exists() && currentDir.isReadable() )
79         {
80             const QStringList subdirs = currentDir.entryList( QDir::AllDirs | QDir::NoDotAndDotDot );
81             for ( const QString& subdir : subdirs )
82             {
83                 currentDir.setPath( path );
84                 bool success = currentDir.cd( subdir );
85                 if ( success )
86                 {
87                     static const char bad_descriptor[] = "ModuleManager potential module descriptor is bad";
88                     QFileInfo descriptorFileInfo( currentDir.absoluteFilePath( QLatin1String( "module.desc" ) ) );
89                     if ( !descriptorFileInfo.exists() )
90                     {
91                         cDebug() << deb << bad_descriptor << descriptorFileInfo.absoluteFilePath() << "(missing)";
92                         continue;
93                     }
94                     if ( !descriptorFileInfo.isReadable() )
95                     {
96                         cDebug() << deb << bad_descriptor << descriptorFileInfo.absoluteFilePath() << "(unreadable)";
97                         continue;
98                     }
99 
100                     bool ok = false;
101                     QVariantMap moduleDescriptorMap = CalamaresUtils::loadYaml( descriptorFileInfo, &ok );
102                     QString moduleName = ok ? moduleDescriptorMap.value( "name" ).toString() : QString();
103 
104                     if ( ok && !moduleName.isEmpty() && ( moduleName == currentDir.dirName() )
105                          && !m_availableDescriptorsByModuleName.contains( moduleName ) )
106                     {
107                         auto descriptor
108                             = Calamares::ModuleSystem::Descriptor::fromDescriptorData( moduleDescriptorMap );
109                         descriptor.setDirectory( descriptorFileInfo.absoluteDir().absolutePath() );
110                         m_availableDescriptorsByModuleName.insert( moduleName, descriptor );
111                     }
112                 }
113                 else
114                 {
115                     cWarning() << "ModuleManager module directory is not accessible:" << path << "/" << subdir;
116                 }
117             }
118         }
119         else
120         {
121             cDebug() << deb << "ModuleManager module search path does not exist:" << path;
122         }
123     }
124     // At this point m_availableDescriptorsByModuleName is filled with
125     // the modules that were found in the search paths.
126     cDebug() << deb << "Found" << m_availableDescriptorsByModuleName.count() << "modules";
127     QTimer::singleShot( 10, this, &ModuleManager::initDone );
128 }
129 
130 
131 QList< ModuleSystem::InstanceKey >
loadedInstanceKeys()132 ModuleManager::loadedInstanceKeys()
133 {
134     return m_loadedModulesByInstanceKey.keys();
135 }
136 
137 
138 Calamares::ModuleSystem::Descriptor
moduleDescriptor(const QString & name)139 ModuleManager::moduleDescriptor( const QString& name )
140 {
141     return m_availableDescriptorsByModuleName.value( name );
142 }
143 
144 Module*
moduleInstance(const ModuleSystem::InstanceKey & instanceKey)145 ModuleManager::moduleInstance( const ModuleSystem::InstanceKey& instanceKey )
146 {
147     return m_loadedModulesByInstanceKey.value( instanceKey );
148 }
149 
150 
151 /** @brief Returns the config file name for the given @p instanceKey
152  *
153  * Custom instances have custom config files, non-custom ones
154  * have a <modulename>.conf file. Returns an empty QString on
155  * errors.
156  */
157 static QString
getConfigFileName(const Settings::InstanceDescriptionList & descriptorList,const ModuleSystem::InstanceKey & instanceKey,const ModuleSystem::Descriptor & thisModule)158 getConfigFileName( const Settings::InstanceDescriptionList& descriptorList,
159                    const ModuleSystem::InstanceKey& instanceKey,
160                    const ModuleSystem::Descriptor& thisModule )
161 {
162     if ( !thisModule.hasConfig() )
163     {
164         // Explicitly set to no-configuration. This doesn't apply
165         // to custom instances (above) since the only reason to
166         // **have** a custom instance is to specify a different
167         // config file for more than one module.
168         return QString();
169     }
170 
171     for ( const auto& descriptor : descriptorList )
172     {
173         if ( descriptor.key() == instanceKey )
174         {
175             return descriptor.configFileName();
176         }
177     }
178 
179 
180     // This should already have been checked and failed the module already
181     return QString();
182 }
183 
184 void
loadModules()185 ModuleManager::loadModules()
186 {
187     if ( checkDependencies() )
188     {
189         cWarning() << "Some installed modules have unmet dependencies.";
190     }
191     Settings::InstanceDescriptionList customInstances = Settings::instance()->moduleInstances();
192 
193     QStringList failedModules;
194     const auto modulesSequence = Settings::instance()->modulesSequence();
195     for ( const auto& modulePhase : modulesSequence )
196     {
197         ModuleSystem::Action currentAction = modulePhase.first;
198 
199         for ( const auto& instanceKey : modulePhase.second )
200         {
201             if ( !instanceKey.isValid() )
202             {
203                 cError() << "Wrong module entry format for module" << instanceKey;
204                 failedModules.append( instanceKey.toString() );
205                 continue;
206             }
207 
208             ModuleSystem::Descriptor descriptor
209                 = m_availableDescriptorsByModuleName.value( instanceKey.module(), ModuleSystem::Descriptor() );
210             if ( !descriptor.isValid() )
211             {
212                 cError() << "Module" << instanceKey.toString() << "not found in module search paths."
213                          << Logger::DebugList( m_paths );
214                 failedModules.append( instanceKey.toString() );
215                 continue;
216             }
217 
218             QString configFileName = getConfigFileName( customInstances, instanceKey, descriptor );
219 
220             // So now we can assume that the module entry is at least valid,
221             // that we have a descriptor on hand (and therefore that the
222             // module exists), and that the instance is either default or
223             // defined in the custom instances section.
224             // We still don't know whether the config file for the entry
225             // exists and is valid, but that's the only thing that could fail
226             // from this point on. -- Teo 8/2015
227             Module* thisModule = m_loadedModulesByInstanceKey.value( instanceKey, nullptr );
228             if ( thisModule )
229             {
230                 if ( thisModule->isLoaded() )
231                 {
232                     // It's been listed before, don't bother loading again.
233                     // This can happen for a module listed twice (e.g. with custom instances)
234                     cDebug() << "Module" << instanceKey.toString() << "already loaded.";
235                 }
236                 else
237                 {
238                     // An attempt was made, earlier, and that failed.
239                     // This can happen for a module listed twice (e.g. with custom instances)
240                     cError() << "Module" << instanceKey.toString() << "exists but not loaded.";
241                     failedModules.append( instanceKey.toString() );
242                     continue;
243                 }
244             }
245             else
246             {
247                 thisModule = Calamares::moduleFromDescriptor(
248                     descriptor, instanceKey.id(), configFileName, descriptor.directory() );
249                 if ( !thisModule )
250                 {
251                     cError() << "Module" << instanceKey.toString() << "cannot be created from descriptor"
252                              << configFileName;
253                     failedModules.append( instanceKey.toString() );
254                     continue;
255                 }
256 
257                 if ( !addModule( thisModule ) )
258                 {
259                     // Error message is already printed
260                     failedModules.append( instanceKey.toString() );
261                     continue;
262                 }
263             }
264 
265             // At this point we most certainly have a pointer to a loaded module in
266             // thisModule. We now need to enqueue jobs info into an EVS.
267             if ( currentAction == ModuleSystem::Action::Exec )
268             {
269                 ExecutionViewStep* evs
270                     = qobject_cast< ExecutionViewStep* >( Calamares::ViewManager::instance()->viewSteps().last() );
271                 if ( !evs )  // If the last step is not an EVS, we must create it.
272                 {
273                     evs = new ExecutionViewStep( ViewManager::instance() );
274                     ViewManager::instance()->addViewStep( evs );
275                 }
276 
277                 evs->appendJobModuleInstanceKey( instanceKey );
278             }
279         }
280     }
281     if ( !failedModules.isEmpty() )
282     {
283         ViewManager::instance()->onInitFailed( failedModules );
284         QTimer::singleShot( 10, [=]() { emit modulesFailed( failedModules ); } );
285     }
286     else
287     {
288         QTimer::singleShot( 10, this, &ModuleManager::modulesLoaded );
289     }
290 }
291 
292 bool
addModule(Module * module)293 ModuleManager::addModule( Module* module )
294 {
295     if ( !module )
296     {
297         return false;
298     }
299     if ( !module->instanceKey().isValid() )
300     {
301         cWarning() << "Module" << module->location() << Logger::Pointer( module ) << "has invalid instance key.";
302         return false;
303     }
304     if ( !checkModuleDependencies( *module ) )
305     {
306         return false;
307     }
308 
309     if ( !module->isLoaded() )
310     {
311         module->loadSelf();
312     }
313 
314     // Even if the load failed, we keep the module, so that if it tried to
315     // get loaded **again**, we already know.
316     m_loadedModulesByInstanceKey.insert( module->instanceKey(), module );
317     if ( !module->isLoaded() )
318     {
319         cError() << "Module" << module->instanceKey().toString() << "loading FAILED.";
320         return false;
321     }
322 
323     return true;
324 }
325 
326 void
checkRequirements()327 ModuleManager::checkRequirements()
328 {
329     cDebug() << "Checking module requirements ..";
330 
331     QVector< Module* > modules( m_loadedModulesByInstanceKey.count() );
332     int count = 0;
333     for ( const auto& module : m_loadedModulesByInstanceKey )
334     {
335         modules[ count++ ] = module;
336     }
337 
338     RequirementsChecker* rq = new RequirementsChecker( modules, m_requirementsModel, this );
339     connect( rq, &RequirementsChecker::done, rq, &RequirementsChecker::deleteLater );
340     connect( rq, &RequirementsChecker::done, this, [=]() {
341         this->requirementsComplete( m_requirementsModel->satisfiedMandatory() );
342     } );
343 
344     QTimer::singleShot( 0, rq, &RequirementsChecker::run );
345 }
346 
347 static QStringList
missingRequiredModules(const QStringList & required,const QMap<QString,ModuleSystem::Descriptor> & available)348 missingRequiredModules( const QStringList& required, const QMap< QString, ModuleSystem::Descriptor >& available )
349 {
350     QStringList l;
351     for ( const QString& depName : required )
352     {
353         if ( !available.contains( depName ) )
354         {
355             l.append( depName );
356         }
357     }
358 
359     return l;
360 }
361 
362 size_t
checkDependencies()363 ModuleManager::checkDependencies()
364 {
365     size_t numberRemoved = 0;
366     bool somethingWasRemovedBecauseOfUnmetDependencies = false;
367 
368     // This goes through the map of available modules, and deletes those whose
369     // dependencies are not met, if any.
370     do
371     {
372         somethingWasRemovedBecauseOfUnmetDependencies = false;
373         for ( auto it = m_availableDescriptorsByModuleName.begin(); it != m_availableDescriptorsByModuleName.end();
374               ++it )
375         {
376             QStringList unmet = missingRequiredModules( it->requiredModules(), m_availableDescriptorsByModuleName );
377 
378             if ( unmet.count() > 0 )
379             {
380                 QString moduleName = it->name();
381                 somethingWasRemovedBecauseOfUnmetDependencies = true;
382                 m_availableDescriptorsByModuleName.erase( it );
383                 numberRemoved++;
384                 cWarning() << "Module" << moduleName << "requires missing modules" << Logger::DebugList( unmet );
385                 break;
386             }
387         }
388     } while ( somethingWasRemovedBecauseOfUnmetDependencies );
389 
390     return numberRemoved;
391 }
392 
393 bool
checkModuleDependencies(const Module & m)394 ModuleManager::checkModuleDependencies( const Module& m )
395 {
396     if ( !m_availableDescriptorsByModuleName.contains( m.name() ) )
397     {
398         cWarning() << "Module" << m.name() << "loaded externally, no dependency information.";
399         return true;
400     }
401 
402     bool allRequirementsFound = true;
403     QStringList requiredModules = m_availableDescriptorsByModuleName[ m.name() ].requiredModules();
404 
405     for ( const QString& required : requiredModules )
406     {
407         bool requirementFound = false;
408         for ( const Module* v : m_loadedModulesByInstanceKey )
409             if ( required == v->name() )
410             {
411                 requirementFound = true;
412                 break;
413             }
414         if ( !requirementFound )
415         {
416             cError() << "Module" << m.name() << "requires" << required << "before it in sequence.";
417             allRequirementsFound = false;
418         }
419     }
420 
421     return allRequirementsFound;
422 }
423 
424 }  // namespace Calamares
425