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