Basic CLI support

Closes #221
This commit is contained in:
Floris Bos 2021-05-06 01:47:34 +02:00
parent 258f9d77aa
commit 62e9969afb
9 changed files with 307 additions and 10 deletions

View file

@ -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}

224
cli.cpp Normal file
View file

@ -0,0 +1,224 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (C) 2020 Raspberry Pi (Trading) Limited
*/
#include "cli.h"
#include "imagewriter.h"
#include <iostream>
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QFileInfo>
#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 <expected hash>] [--debug] [--quiet] <image file to write> <destination drive device>" << 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;
}
}

39
cli.h Normal file
View file

@ -0,0 +1,39 @@
#ifndef CLI_H
#define CLI_H
#include <QObject>
#include <QVariant>
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

View file

@ -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)
{

View file

@ -63,7 +63,16 @@ ImageWriter::ImageWriter(QObject *parent)
{
connect(&_polltimer, SIGNAL(timeout()), SLOT(pollProgress()));
QString platform = QGuiApplication::platformName();
QString platform;
if (qobject_cast<QGuiApplication*>(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<QApplication*>(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<QApplication*>(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();
}

View file

@ -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());

View file

@ -13,6 +13,7 @@
#include "imagewriter.h"
#include "drivelistmodel.h"
#include "networkaccessmanagerfactory.h"
#include "cli.h"
#include <QMessageLogContext>
#include <QQuickWindow>
#include <QTranslator>
@ -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 <repository URL>] [--qm <custom qm translation file>] [--disable-telemetry] [<image file to write>]" << endl;
cerr << "-OR- " << args[0] << " --cli [--disable-verify] [--sha256 <expected hash>] [--debug] [--quiet] <image file to write> <destination drive device>" << endl;
return 0;
}
else if (args[i] == "--version")

View file

@ -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 %*

View file

@ -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"