From 0b943e801c16fb819abf671489f44d1a331371b9 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 4 Oct 2021 00:10:44 +0200 Subject: [PATCH] [libcalamares] Begin implementing Runner This copies a bunch of code from the System methods for running, and then #if 0's them to get it to compile. Add some basic directory-management. --- src/libcalamares/CMakeLists.txt | 1 + src/libcalamares/utils/Runner.cpp | 203 +++++++++++++++++++++++++++--- src/libcalamares/utils/Runner.h | 78 ++++++++++-- 3 files changed, 259 insertions(+), 23 deletions(-) diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index 645a26d25..2e002fd47 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -77,6 +77,7 @@ set( libSources utils/Permissions.cpp utils/PluginFactory.cpp utils/Retranslator.cpp + utils/Runner.cpp utils/String.cpp utils/UMask.cpp utils/Variant.cpp diff --git a/src/libcalamares/utils/Runner.cpp b/src/libcalamares/utils/Runner.cpp index 405ef68fb..8e81cb487 100644 --- a/src/libcalamares/utils/Runner.cpp +++ b/src/libcalamares/utils/Runner.cpp @@ -10,8 +10,82 @@ #include "Runner.h" -namespace +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" + +/** @brief Descend from directory, always relative + * + * If @p subdir begins with a "/" or "../" or "./" those are stripped + * until none are left, then changes @p directory into that + * subdirectory. + * + * Returns @c false if the @p subdir doesn't make sense. + */ +STATICTEST bool +relativeChangeDirectory( QDir& directory, const QString& subdir ) { + const QString rootPath = directory.absolutePath(); + const QString concatenatedPath = rootPath + '/' + subdir; + const QString relPath = QDir::cleanPath( concatenatedPath ); + + if ( !relPath.startsWith( rootPath ) ) + { + cWarning() << "Relative path" << subdir << "escapes from" << rootPath; + return false; + } + + return directory.cd( relPath ); +} + + +STATICTEST std::pair< bool, QDir > +calculateWorkingDirectory( Calamares::Utils::RunLocation location, QString directory ) +{ + Calamares::GlobalStorage* gs + = Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr; + + if ( location == Calamares::Utils::RunLocation::RunInTarget ) + { + if ( !gs || !gs->contains( "rootMountPoint" ) ) + { + cWarning() << "No rootMountPoint in global storage, while RunInTarget is specified"; + return std::make_pair( false, QDir() ); + } + + QDir rootMountPoint( gs->value( "rootMountPoint" ).toString() ); + if ( !rootMountPoint.exists() ) + { + cWarning() << "rootMountPoint points to a dir which does not exist"; + return std::make_pair( false, QDir() ); + } + + if ( !directory.isEmpty() ) + { + + if ( !relativeChangeDirectory( rootMountPoint, directory ) || !rootMountPoint.exists() ) + { + cWarning() << "Working directory" << directory << "does not exist in target"; + return std::make_pair( false, QDir() ); + } + } + return std::make_pair( true, rootMountPoint ); // Now changed to subdir + } + else + { + QDir root; + if ( !directory.isEmpty() ) + { + root = QDir::root(); + + if ( !relativeChangeDirectory( root, directory ) || !root.exists() ) + { + cWarning() << "Working directory" << directory << "does not exist in host"; + return std::make_pair( false, QDir() ); + } + } + return std::make_pair( true, root ); // Now changed to subdir + } } namespace Calamares @@ -21,25 +95,122 @@ namespace Utils struct Runner::Private { - QStringList m_command; - RunLocation m_location = RunLocation::RunInHost; + QProcess m_process; }; + Runner::Runner() {} -Runner::Runner( const QStringList& command ) - : d( std::make_unique< Private >() ) -{ - d->m_command = command; -} -Runner::~Runner() {} - -Runner& -Runner::setCommand( const QStringList& command ) -{ - d->m_command = command; - return *this; -} } // namespace Utils } // namespace Calamares + + +Calamares::Utils::Runner::Runner( const QStringList& command ) +{ + setCommand( command ); +} + +Calamares::Utils::Runner::~Runner() {} + +Calamares::Utils::ProcessResult +Calamares::Utils::Runner::run() +{ + if ( m_command.isEmpty() ) + { + cWarning() << "Cannot run an empty program list"; + return ProcessResult::Code::FailedToStart; + } + + auto [ ok, workingDirectory ] = calculateWorkingDirectory( m_location, m_directory ); + if ( !ok || !workingDirectory.exists() ) + { + // Warnings have already been printed + return ProcessResult::Code::NoWorkingDirectory; + } + + + QProcess process; + process.setProcessChannelMode( QProcess::MergedChannels ); + process.setWorkingDirectory( workingDirectory.absolutePath() ); + if ( m_location == RunLocation::RunInTarget ) + { + process.setProgram( "chroot" ); + process.setArguments( QStringList { workingDirectory.absolutePath() } << m_command ); + } + else + { + process.setProgram( "env" ); + process.setArguments( m_command ); + } + + return ProcessResult::Code::Crashed; +#if 0 + if ( !workingPath.isEmpty() ) + { + if ( QDir( workingPath ).exists() ) + { + process.setWorkingDirectory( QDir( workingPath ).absolutePath() ); + } + else + { + cWarning() << "Invalid working directory:" << workingPath; + return ProcessResult::Code::NoWorkingDirectory; + } + } + + cDebug() << Logger::SubEntry << "Running" << program << RedactedList( arguments ); + process.start(); + if ( !process.waitForStarted() ) + { + cWarning() << "Process" << args.first() << "failed to start" << process.error(); + return ProcessResult::Code::FailedToStart; + } + + if ( !stdInput.isEmpty() ) + { + process.write( stdInput.toLocal8Bit() ); + } + process.closeWriteChannel(); + + if ( !process.waitForFinished( timeoutSec > std::chrono::seconds::zero() + ? ( static_cast< int >( std::chrono::milliseconds( timeoutSec ).count() ) ) + : -1 ) ) + { + cWarning() << "Process" << args.first() << "timed out after" << timeoutSec.count() << "s. Output so far:\n" + << Logger::NoQuote << process.readAllStandardOutput(); + return ProcessResult::Code::TimedOut; + } + + QString output = QString::fromLocal8Bit( process.readAllStandardOutput() ).trimmed(); + + if ( process.exitStatus() == QProcess::CrashExit ) + { + cWarning() << "Process" << args.first() << "crashed. Output so far:\n" << Logger::NoQuote << output; + return ProcessResult::Code::Crashed; + } + + auto r = process.exitCode(); + bool showDebug = ( !Calamares::Settings::instance() ) || ( Calamares::Settings::instance()->debugMode() ); + if ( r == 0 ) + { + if ( showDebug && !output.isEmpty() ) + { + cDebug() << Logger::SubEntry << "Finished. Exit code:" << r << "output:\n" << Logger::NoQuote << output; + } + } + else // if ( r != 0 ) + { + if ( !output.isEmpty() ) + { + cDebug() << Logger::SubEntry << "Target cmd:" << RedactedList( args ) << "Exit code:" << r << "output:\n" + << Logger::NoQuote << output; + } + else + { + cDebug() << Logger::SubEntry << "Target cmd:" << RedactedList( args ) << "Exit code:" << r << "(no output)"; + } + } + return ProcessResult( r, output ); +#endif +} diff --git a/src/libcalamares/utils/Runner.h b/src/libcalamares/utils/Runner.h index 85c679180..d42d1a709 100644 --- a/src/libcalamares/utils/Runner.h +++ b/src/libcalamares/utils/Runner.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -27,25 +28,88 @@ namespace Utils { using RunLocation = CalamaresUtils::System::RunLocation; +using ProcessResult = CalamaresUtils::ProcessResult; -/** @brief A Runner wraps a process and handles running it and signalling */ +/** @brief A Runner wraps a process and handles running it and processing output + * + * This is basically a QProcess, but handles both running in the + * host system (natively) or in the target (by calling chroot). + * It has an output signal that handles output one line at a time + * (unlike QProcess that lets you do the buffering yourself). + * This output processing is only enabled if you do so explicitly. + */ class Runner : public QObject { Q_OBJECT public: + /** @brief Create an empty runner + * + * This is a runner with no commands, nothing; call set*() methods + * to configure it. + */ Runner(); + /** @brief Create a runner with a specified command + * + * Equivalent to Calamares::Utils::Runner::Runner() + */ Runner( const QStringList& command ); virtual ~Runner() override; - Runner& setCommand( const QStringList& command ); - Runner& setLocation( RunLocation r ); - Runner& setWorkingDirectory( const QString& directory ); - Runner& setWorkingDirectory( const QDir& directory ); - Runner& setTimeout( std::chrono::seconds timeout ); - Runner& setInput( const QString& stdin ); + Runner& setCommand( const QStringList& command ) + { + m_command = command; + return *this; + } + Runner& setLocation( RunLocation r ) + { + m_location = r; + return *this; + } + Runner& setWorkingDirectory( const QDir& directory ) + { + m_directory = directory.absolutePath(); + return *this; + } + Runner& setWorkingDirectory( const QString& directory ) + { + m_directory = directory; + return *this; + } + Runner& setTimeout( std::chrono::seconds timeout ) + { + m_timeout = timeout; + return *this; + } + Runner& setInput( const QString& stdin ) + { + m_input = stdin; + return *this; + } + + Runner& enableOutputProcessing( bool enable = true ) + { + m_output = enable; + return *this; + } + + ProcessResult run(); + +signals: + void output( QString line ); private: + // What to run, and where. + QStringList m_command; + QString m_directory; + RunLocation m_location { RunLocation::RunInHost }; + + // Settings for when it actually runs + QString m_input; + std::chrono::milliseconds m_timeout { 0 }; + bool m_output = false; + + // Internals for when it really does run struct Private; std::unique_ptr< Private > d; };