From 62e9969afb66c58a0e7304fa904364814286fd6f Mon Sep 17 00:00:00 2001 From: Floris Bos Date: Thu, 6 May 2021 01:47:34 +0200 Subject: [PATCH] Basic CLI support Closes #221 --- CMakeLists.txt | 7 +- cli.cpp | 224 +++++++++++++++++++++++++++++++++++++ cli.h | 39 +++++++ downloadthread.cpp | 2 +- imagewriter.cpp | 18 ++- linux/udisks2api.cpp | 4 +- main.cpp | 12 ++ windows/rpi-imager-cli.cmd | 9 ++ windows/rpi-imager.nsi.in | 2 + 9 files changed, 307 insertions(+), 10 deletions(-) create mode 100644 cli.cpp create mode 100644 cli.h create mode 100644 windows/rpi-imager-cli.cmd diff --git a/CMakeLists.txt b/CMakeLists.txt index 93e9278..591d9bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) # Adding headers explicity so they are displayed in Qt Creator -set(HEADERS config.h imagewriter.h networkaccessmanagerfactory.h nan.h drivelistitem.h drivelistmodel.h drivelistmodelpollthread.h driveformatthread.h powersaveblocker.h +set(HEADERS config.h imagewriter.h networkaccessmanagerfactory.h nan.h drivelistitem.h drivelistmodel.h drivelistmodelpollthread.h driveformatthread.h powersaveblocker.h cli.h downloadthread.h downloadextractthread.h localfileextractthread.h downloadstatstelemetry.h dependencies/mountutils/src/mountutils.hpp dependencies/sha256crypt/sha256crypt.h) # Add dependencies @@ -69,7 +69,7 @@ endif() set(SOURCES "main.cpp" "imagewriter.cpp" "networkaccessmanagerfactory.cpp" "drivelistitem.cpp" "drivelistmodel.cpp" "drivelistmodelpollthread.cpp" "downloadthread.cpp" "downloadextractthread.cpp" - "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc" "dependencies/sha256crypt/sha256crypt.c") + "driveformatthread.cpp" "localfileextractthread.cpp" "powersaveblocker.cpp" "downloadstatstelemetry.cpp" "qml.qrc" "dependencies/sha256crypt/sha256crypt.c" "cli.cpp") find_package(Qt5 COMPONENTS Core Quick LinguistTools Svg OPTIONAL_COMPONENTS Widgets) if (Qt5Widgets_FOUND) @@ -157,7 +157,8 @@ if (WIN32) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy - "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.exe" "${CMAKE_BINARY_DIR}/dependencies/fat32format/fat32format.exe" "${CMAKE_SOURCE_DIR}/license.txt" + "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.exe" "${CMAKE_BINARY_DIR}/dependencies/fat32format/fat32format.exe" + "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_SOURCE_DIR}/windows/rpi-imager-cli.cmd" "${CMAKE_BINARY_DIR}/deploy") add_custom_command(TARGET ${PROJECT_NAME} diff --git a/cli.cpp b/cli.cpp new file mode 100644 index 0000000..6b91b8d --- /dev/null +++ b/cli.cpp @@ -0,0 +1,224 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (C) 2020 Raspberry Pi (Trading) Limited + */ + +#include "cli.h" +#include "imagewriter.h" +#include +#include +#include +#include +#include "drivelistmodel.h" +#include "dependencies/drivelist/src/drivelist.hpp" + +/* Message handler to discard qDebug() output if using cli (unless --debug is set) */ +static void devnullMsgHandler(QtMsgType, const QMessageLogContext &, const QString &) +{ +} + +Cli::Cli(int &argc, char *argv[]) : QObject(nullptr) +{ +#ifdef Q_OS_WIN + /* Allocate console on Windows (only needed if compiled as GUI program) */ + if (::AttachConsole(ATTACH_PARENT_PROCESS) || ::AllocConsole()) + { + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + std::ios::sync_with_stdio(); + } +#endif + _app = new QCoreApplication(argc, argv); + _app->setOrganizationName("Raspberry Pi"); + _app->setOrganizationDomain("raspberrypi.org"); + _app->setApplicationName("Imager"); + _imageWriter = new ImageWriter; + connect(_imageWriter, &ImageWriter::success, this, &Cli::onSuccess); + connect(_imageWriter, &ImageWriter::error, this, &Cli::onError); + connect(_imageWriter, &ImageWriter::preparationStatusUpdate, this, &Cli::onPreparationStatusUpdate); + connect(_imageWriter, &ImageWriter::downloadProgress, this, &Cli::onDownloadProgress); + connect(_imageWriter, &ImageWriter::verifyProgress, this, &Cli::onVerifyProgress); +} + +Cli::~Cli() +{ + delete _imageWriter; + delete _app; +} + +int Cli::main() +{ + QCommandLineParser parser; + QCommandLineOption cli("cli"); + parser.addOption(cli); + QCommandLineOption disableVerify("disable-verify", "Disable verification"); + parser.addOption(disableVerify); + QCommandLineOption writeSystemDrive("enable-writing-system-drives", "Only use this if you know what you are doing"); + parser.addOption(writeSystemDrive); + QCommandLineOption sha256Option("sha256", "Expected hash", "sha256", ""); + parser.addOption(sha256Option); + QCommandLineOption debugOption("debug", "Output debug messages to console"); + parser.addOption(debugOption); + QCommandLineOption quietOption("quiet", "Only write to console on error"); + parser.addOption(quietOption); + + parser.addPositionalArgument("src", "Image file/URL"); + parser.addPositionalArgument("dst", "Destination device"); + parser.process(*_app); + + const QStringList args = parser.positionalArguments(); + if (args.count() != 2) + { + std::cerr << "Usage: --cli [--disable-verify] [--sha256 ] [--debug] [--quiet] " << std::endl; + return 1; + } + + if (!parser.isSet(debugOption)) + { + qInstallMessageHandler(devnullMsgHandler); + } + _quiet = parser.isSet(quietOption); + + if (args[0].startsWith("http:", Qt::CaseInsensitive) || args[0].startsWith("https:", Qt::CaseInsensitive)) + { + _imageWriter->setSrc(args[0], 0, 0, parser.value(sha256Option).toLatin1() ); + } + else + { + QFileInfo fi(args[0]); + + if (fi.isFile()) + { + _imageWriter->setSrc(QUrl::fromLocalFile(args[0]), fi.size(), 0, parser.value(sha256Option).toLatin1() ); + } + else if (!fi.exists()) + { + std::cerr << "Error: source file does not exists" << std::endl; + return 1; + } + else + { + std::cerr << "Error: source is not a regular file" << std::endl; + return 1; + } + } + + if (parser.isSet(writeSystemDrive)) + { + std::cerr << "WARNING: writing to system drives is enabled." << std::endl; + } + else + { + DriveListModel dlm; + dlm.processDriveList(Drivelist::ListStorageDevices() ); + bool foundDrive = false; + int numDrives = dlm.rowCount( QModelIndex() ); + + for (int i = 0; i < numDrives; i++) + { + if (dlm.index(i, 0).data(dlm.deviceRole) == args[1]) + { + foundDrive = true; + break; + } + } + + if (!foundDrive) + { + std::cerr << "Destination drive is not in list of removable volumes. Choose one of the following:" << std::endl << std::endl; + + for (int i = 0; i < numDrives; i++) + { + QModelIndex idx = dlm.index(i, 0); + QByteArray line = idx.data(dlm.deviceRole).toByteArray()+" ("+idx.data(dlm.descriptionRole).toByteArray()+")"; + + std::cerr << line.constData() << std::endl; + } + + std::cerr << std::endl << "Or use --enable-writing-system-drives to overrule." << std::endl; + return 1; + } + } + + _imageWriter->setDst(args[1]); + _imageWriter->setVerifyEnabled(!parser.isSet(disableVerify)); + + /* Run startWrite() in event loop (otherwise calling _app->exit() on error does not work) */ + QTimer::singleShot(1, _imageWriter, &ImageWriter::startWrite); + return _app->exec(); +} + +void Cli::onSuccess() +{ + if (!_quiet) + { + _clearLine(); + std::cerr << "Write successful." << std::endl; + } + _app->exit(0); +} + +void Cli::_clearLine() +{ + /* Properly clearing line requires platform specific code. + Just write some spaces for now, and return to beginning of line. */ + std::cerr << " \r"; +} + +void Cli::onError(QVariant msg) +{ + QByteArray m = msg.toByteArray(); + + if (!_quiet) + { + _clearLine(); + } + std::cerr << "Error: " << m.constData() << std::endl; + _app->exit(1); +} + +void Cli::onDownloadProgress(QVariant dlnow, QVariant dltotal) +{ + _printProgress("Writing", dlnow, dltotal); +} + +void Cli::onVerifyProgress(QVariant now, QVariant total) +{ + _printProgress("Verifying", now, total); +} + +void Cli::onPreparationStatusUpdate(QVariant msg) +{ + if (!_quiet) + { + QByteArray ascii = QByteArray(" ")+msg.toByteArray()+"\r"; + _clearLine(); + std::cerr << ascii.constData(); + } +} + +void Cli::_printProgress(const QByteArray &msg, QVariant now, QVariant total) +{ + if (_quiet) + return; + + float n = now.toFloat(); + float t = total.toFloat(); + + if (t) + { + int percent = n/t*100; + if (percent != _lastPercent || msg != _lastMsg) + { + QByteArray txt = QByteArray(" ")+msg+": ["+QByteArray(percent/5, '-')+'>'+QByteArray(20-percent/5, ' ')+"] "+QByteArray::number(percent)+" %\r"; + std::cerr << txt.constData(); + _lastPercent = percent; + _lastMsg = msg; + } + } + else if (msg != _lastMsg) + { + std::cerr << msg.constData() << "\r"; + _lastMsg = msg; + } +} diff --git a/cli.h b/cli.h new file mode 100644 index 0000000..09e2b97 --- /dev/null +++ b/cli.h @@ -0,0 +1,39 @@ +#ifndef CLI_H +#define CLI_H + +#include +#include + +class ImageWriter; +class QCoreApplication; + +class Cli : public QObject +{ + Q_OBJECT +public: + explicit Cli(int &argc, char *argv[]); + virtual ~Cli(); + int main(); + +protected: + QCoreApplication *_app; + ImageWriter *_imageWriter; + int _lastPercent; + QByteArray _lastMsg; + bool _quiet; + + void _printProgress(const QByteArray &msg, QVariant now, QVariant total); + void _clearLine(); + +protected slots: + void onSuccess(); + void onError(QVariant msg); + void onDownloadProgress(QVariant dlnow, QVariant dltotal); + void onVerifyProgress(QVariant now, QVariant total); + void onPreparationStatusUpdate(QVariant msg); + +signals: + +}; + +#endif // CLI_H diff --git a/downloadthread.cpp b/downloadthread.cpp index 1633286..d8e9bc7 100644 --- a/downloadthread.cpp +++ b/downloadthread.cpp @@ -747,8 +747,8 @@ bool DownloadThread::_verify() } qFreeAligned(verifyBuf); - qDebug() << "Verify done in" << t1.elapsed() / 1000.0 << "seconds"; qDebug() << "Verify hash:" << _verifyhash.result().toHex(); + qDebug() << "Verify done in" << t1.elapsed() / 1000.0 << "seconds"; if (_verifyhash.result() == _writehash.result() || !_verifyEnabled || _cancelled) { diff --git a/imagewriter.cpp b/imagewriter.cpp index a9f61b1..f39cb3f 100644 --- a/imagewriter.cpp +++ b/imagewriter.cpp @@ -63,7 +63,16 @@ ImageWriter::ImageWriter(QObject *parent) { connect(&_polltimer, SIGNAL(timeout()), SLOT(pollProgress())); - QString platform = QGuiApplication::platformName(); + QString platform; + if (qobject_cast(QCoreApplication::instance()) ) + { + platform = QGuiApplication::platformName(); + } + else + { + platform = "cli"; + } + if (platform == "eglfs" || platform == "linuxfb") { _embeddedMode = true; @@ -469,7 +478,7 @@ void ImageWriter::onSuccess() emit success(); #ifndef QT_NO_WIDGETS - if (_settings.value("beep").toBool()) + if (_settings.value("beep").toBool() && qobject_cast(QCoreApplication::instance()) ) { QApplication::beep(); } @@ -482,7 +491,7 @@ void ImageWriter::onError(QString msg) emit error(msg); #ifndef QT_NO_WIDGETS - if (_settings.value("beep").toBool()) + if (_settings.value("beep").toBool() && qobject_cast(QCoreApplication::instance()) ) QApplication::beep(); #endif } @@ -1014,5 +1023,6 @@ bool ImageWriter::hasSavedCustomizationSettings() } void MountUtilsLog(std::string msg) { - qDebug() << "mountutils:" << msg.c_str(); + Q_UNUSED(msg) + //qDebug() << "mountutils:" << msg.c_str(); } diff --git a/linux/udisks2api.cpp b/linux/udisks2api.cpp index c2e9078..3a23f32 100644 --- a/linux/udisks2api.cpp +++ b/linux/udisks2api.cpp @@ -61,7 +61,7 @@ QString UDisks2Api::_resolveDevice(const QString &device) void UDisks2Api::_unmountDrive(const QString &driveDbusPath) { - qDebug() << "Drive:" << driveDbusPath; + //qDebug() << "Drive:" << driveDbusPath; QDBusInterface manager("org.freedesktop.UDisks2", "/org/freedesktop/UDisks2/Manager", "org.freedesktop.UDisks2.Manager", QDBusConnection::systemBus()); @@ -81,7 +81,7 @@ void UDisks2Api::_unmountDrive(const QString &driveDbusPath) if (driveOfDev != driveDbusPath) continue; - qDebug() << "Device:" << devpathStr << "belongs to same drive"; + //qDebug() << "Device:" << devpathStr << "belongs to same drive"; QDBusInterface filesystem("org.freedesktop.UDisks2", devpathStr, "org.freedesktop.UDisks2.Filesystem", QDBusConnection::systemBus()); diff --git a/main.cpp b/main.cpp index 65e771f..26923db 100644 --- a/main.cpp +++ b/main.cpp @@ -13,6 +13,7 @@ #include "imagewriter.h" #include "drivelistmodel.h" #include "networkaccessmanagerfactory.h" +#include "cli.h" #include #include #include @@ -36,6 +37,16 @@ static void consoleMsgHandler(QtMsgType, const QMessageLogContext &, const QStri int main(int argc, char *argv[]) { + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "--cli") == 0) + { + /* CLI mode */ + Cli cli(argc, argv); + return cli.main(); + } + } + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #ifdef Q_OS_WIN // prefer ANGLE (DirectX) over desktop OpenGL @@ -149,6 +160,7 @@ int main(int argc, char *argv[]) else if (args[i] == "--help") { cerr << args[0] << " [--debug] [--version] [--repo ] [--qm ] [--disable-telemetry] []" << endl; + cerr << "-OR- " << args[0] << " --cli [--disable-verify] [--sha256 ] [--debug] [--quiet] " << endl; return 0; } else if (args[i] == "--version") diff --git a/windows/rpi-imager-cli.cmd b/windows/rpi-imager-cli.cmd new file mode 100644 index 0000000..99c15f7 --- /dev/null +++ b/windows/rpi-imager-cli.cmd @@ -0,0 +1,9 @@ +@echo off + +rem +rem For scripting: call rpi-imager.exe and wait until it finished before continuing +rem This is necessary because it is compiled as GUI application, and Windows +rem normalling does not wait until those exit +rem + +start /WAIT rpi-imager.exe --cli %* diff --git a/windows/rpi-imager.nsi.in b/windows/rpi-imager.nsi.in index 3580336..58e9d6d 100644 --- a/windows/rpi-imager.nsi.in +++ b/windows/rpi-imager.nsi.in @@ -244,6 +244,7 @@ File "deploy\Qt5Svg.dll" File "deploy\Qt5Widgets.dll" File "deploy\Qt5WinExtras.dll" File "deploy\rpi-imager.exe" +File "deploy\rpi-imager-cli.cmd" SetOutPath "$INSTDIR\styles" File "deploy\styles\qwindowsvistastyle.dll" SetOutPath "$INSTDIR\QtQuick.2" @@ -700,6 +701,7 @@ Delete "$INSTDIR\Qt5WinExtras.dll" # Old name Delete "$INSTDIR\imagingutility.exe" Delete "$INSTDIR\rpi-imager.exe" +Delete "$INSTDIR\rpi-imager-cli.cmd" Delete "$INSTDIR\styles\qwindowsvistastyle.dll" Delete "$INSTDIR\QtQuick.2\plugins.qmltypes" Delete "$INSTDIR\QtQuick.2\qmldir"