Utils: Add deviceshell generic class

Change-Id: Ibfb16a0f13f9fe119d27055db5897213127dd104
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
Marcus Tillmanns
2022-05-05 15:08:25 +02:00
parent f9800e5a26
commit fea67b17f9
4 changed files with 369 additions and 0 deletions

View File

@@ -31,6 +31,7 @@ add_qtc_library(Utils
delegates.cpp delegates.h
detailsbutton.cpp detailsbutton.h
detailswidget.cpp detailswidget.h
deviceshell.cpp deviceshell.h
differ.cpp differ.h
displayname.cpp displayname.h
dropsupport.cpp dropsupport.h

View File

@@ -0,0 +1,285 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "deviceshell.h"
#include "processinterface.h"
#include <qtcassert.h>
#include <qtcprocess.h>
#include <QLoggingCategory>
Q_LOGGING_CATEGORY(deviceShellLog, "qtc.utils.deviceshell", QtWarningMsg)
namespace Utils {
DeviceShell::DeviceShell()
{
m_thread.setObjectName("Shell Thread");
m_thread.start();
}
DeviceShell::~DeviceShell()
{
if (m_thread.isRunning()) {
m_thread.quit();
m_thread.wait();
}
}
bool DeviceShell::waitForStarted()
{
QTC_ASSERT(m_shellProcess, return false);
Q_ASSERT(QThread::currentThread() != &m_thread);
bool result;
QMetaObject::invokeMethod(
m_shellProcess,
[this] { return m_shellProcess->waitForStarted(); },
Qt::BlockingQueuedConnection,
&result);
return result;
}
/*!
* \brief DeviceShell::runInShell
* \param cmd The command to run
* \param stdInData Data to send to the stdin of the command
* \return true if the command finished with EXIT_SUCCESS(0)
*
* Runs the cmd inside the internal shell process and return whether it exited with EXIT_SUCCESS
*
* Will automatically defer to the internal thread
*/
bool DeviceShell::runInShell(const CommandLine &cmd, const QByteArray &stdInData)
{
QTC_ASSERT(m_shellProcess, return false);
Q_ASSERT(QThread::currentThread() != &m_thread);
bool result = false;
QMetaObject::invokeMethod(
m_shellProcess,
[this, &cmd, &stdInData] { return runInShellImpl(cmd, stdInData); },
Qt::BlockingQueuedConnection,
&result);
return result;
}
bool DeviceShell::runInShellImpl(const CommandLine &cmd, const QByteArray &stdInData)
{
QTC_ASSERT(QThread::currentThread() == &m_thread, return false);
QTC_ASSERT(m_shellProcess->isRunning(), return false);
QTC_ASSERT(m_shellProcess, return false);
QTC_CHECK(m_shellProcess->readAllStandardOutput().isNull()); // clean possible left-overs
QTC_CHECK(m_shellProcess->readAllStandardError().isNull()); // clean possible left-overs
auto cleanup = qScopeGuard(
[this] { m_shellProcess->readAllStandardOutput(); }); // clean on assert
QString prefix;
if (!stdInData.isEmpty())
prefix = "echo '" + QString::fromUtf8(stdInData.toBase64()) + "' | base64 -d | ";
const QString suffix = " > /dev/null 2>&1\necho $?\n";
const QString command = prefix + cmd.toUserOutput() + suffix;
qCDebug(deviceShellLog) << "Running:" << command;
m_shellProcess->write(command);
m_shellProcess->waitForReadyRead();
const QByteArray output = m_shellProcess->readAllStandardOutput();
bool ok = false;
const int result = output.toInt(&ok);
qCInfo(deviceShellLog) << "Run command in shell:" << cmd.toUserOutput() << "result: " << output
<< " ==>" << result;
QTC_ASSERT(ok, return false);
return result == EXIT_SUCCESS;
}
/*!
* \brief DeviceShell::outputForRunInShell
* \param cmd The command to run
* \param stdInData Data to send to the stdin of the command
* \return The stdout of the command
*
* Runs a command inside the running shell and returns the stdout that was generated by it.
*
* Will automatically defer to the internal thread
*/
DeviceShell::RunResult DeviceShell::outputForRunInShell(const CommandLine &cmd,
const QByteArray &stdInData)
{
QTC_ASSERT(m_shellProcess, return {});
Q_ASSERT(QThread::currentThread() != &m_thread);
RunResult result;
QMetaObject::invokeMethod(
m_shellProcess,
[this, &cmd, &stdInData] { return outputForRunInShellImpl(cmd, stdInData); },
Qt::BlockingQueuedConnection,
&result);
return result;
}
DeviceShell::RunResult DeviceShell::outputForRunInShellImpl(const CommandLine &cmd,
const QByteArray &stdInData)
{
QTC_ASSERT(QThread::currentThread() == &m_thread, return {});
QTC_ASSERT(m_shellProcess, return {});
QTC_CHECK(m_shellProcess->readAllStandardOutput().isNull()); // clean possible left-overs
QTC_CHECK(m_shellProcess->readAllStandardError().isNull()); // clean possible left-overs
auto cleanup = qScopeGuard(
[this] { m_shellProcess->readAllStandardOutput(); }); // clean on assert
QString prefix;
if (!stdInData.isEmpty())
prefix = "echo '" + QString::fromUtf8(stdInData.toBase64()) + "' | base64 -d | ";
const QString markerCmd = "echo __qtc$?qtc__ 1>&2\n";
const QString suffix = "\n" + markerCmd;
const QString command = prefix + cmd.toUserOutput() + suffix;
qCDebug(deviceShellLog) << "Running:" << command;
m_shellProcess->write(command);
RunResult result;
while (true) {
m_shellProcess->waitForReadyRead();
QByteArray stdErr = m_shellProcess->readAllStandardError();
if (stdErr.endsWith("qtc__\n")) {
QByteArray marker = stdErr.right(stdErr.length() - stdErr.lastIndexOf("__qtc"));
QByteArray exitCodeStr = marker.mid(5, marker.length() - 11);
bool ok = false;
const int exitCode = exitCodeStr.toInt(&ok);
result.stdOutput = m_shellProcess->readAllStandardOutput();
result.exitCode = ok ? exitCode : -1;
break;
}
}
//const QByteArray output = m_shellProcess->readAllStandardOutput();
qCDebug(deviceShellLog) << "Received output:" << result.stdOutput;
qCInfo(deviceShellLog) << "Run command in shell:" << cmd.toUserOutput()
<< "output size:" << result.stdOutput.size()
<< "exit code:" << result.exitCode;
return result;
}
void DeviceShell::close()
{
QTC_ASSERT(QThread::currentThread() == thread(), return );
QTC_ASSERT(m_thread.isRunning(), return );
m_thread.quit();
m_thread.wait();
}
/*!
* \brief DeviceShell::setupShellProcess
*
* Override this function to setup the shell process.
* The default implementation just sets the command line to "bash"
*/
void DeviceShell::setupShellProcess(QtcProcess* shellProcess)
{
shellProcess->setCommand(CommandLine{"bash"});
}
/*!
* \brief DeviceShell::startupFailed
*
* Override to display custom error messages
*/
void DeviceShell::startupFailed(const CommandLine &cmdLine)
{
qCDebug(deviceShellLog) << "Failed to start shell via:" << cmdLine.toUserOutput();
}
/*!
* \brief DeviceShell::start
* \return Returns true if starting the Shell process succeeded
*
* \note You have to call this function when deriving from DeviceShell. Current implementations call the function from their constructor.
*/
bool DeviceShell::start()
{
m_shellProcess = new QtcProcess();
connect(m_shellProcess, &QtcProcess::done, this, [this] { emit done(); });
connect(m_shellProcess, &QtcProcess::errorOccurred, this, &DeviceShell::errorOccurred);
connect(m_shellProcess, &QObject::destroyed, this, [this] { m_shellProcess = nullptr; });
connect(&m_thread, &QThread::finished, m_shellProcess, [this] { closeShellProcess(); });
setupShellProcess(m_shellProcess);
m_shellProcess->setProcessMode(ProcessMode::Writer);
m_shellProcess->setWriteData("echo\n");
// Moving the process into its own thread ...
m_shellProcess->moveToThread(&m_thread);
bool result = false;
QMetaObject::invokeMethod(
m_shellProcess,
[this] {
m_shellProcess->start();
if (!m_shellProcess->waitForStarted() || !m_shellProcess->waitForReadyRead()
|| m_shellProcess->readAllStandardOutput() != "\n") {
closeShellProcess();
return false;
}
// TODO: Check if necessary tools are available ( e.g. base64, /bin/sh etc. )
return true;
},
Qt::BlockingQueuedConnection,
&result);
if (!result) {
startupFailed(m_shellProcess->commandLine());
}
return result;
}
void DeviceShell::closeShellProcess()
{
if (m_shellProcess) {
if (m_shellProcess->isRunning()) {
m_shellProcess->write("exit\n");
if (!m_shellProcess->waitForFinished(2000))
m_shellProcess->terminate();
}
m_shellProcess->deleteLater();
}
}
} // namespace Utils

View File

@@ -0,0 +1,81 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "utils_global.h"
#include <QProcess>
#include <QThread>
#include <memory>
namespace Utils {
class CommandLine;
class QtcProcess;
class DeviceShellImpl;
class QTCREATOR_UTILS_EXPORT DeviceShell : public QObject
{
Q_OBJECT
public:
struct RunResult
{
int exitCode;
QByteArray stdOutput;
};
DeviceShell();
virtual ~DeviceShell();
bool runInShell(const CommandLine &cmd, const QByteArray &stdInData = {});
RunResult outputForRunInShell(const CommandLine &cmd, const QByteArray &stdInData = {});
bool waitForStarted();
signals:
void done();
void errorOccurred(QProcess::ProcessError error);
protected:
bool runInShellImpl(const CommandLine &cmd, const QByteArray &stdInData = {});
RunResult outputForRunInShellImpl(const CommandLine &cmd, const QByteArray &stdInData = {});
bool start();
void close();
private:
virtual void setupShellProcess(QtcProcess* shellProcess);
virtual void startupFailed(const CommandLine &cmdLine);
void closeShellProcess();
private:
QtcProcess *m_shellProcess;
QThread m_thread;
};
} // namespace Utils

View File

@@ -80,6 +80,8 @@ Project {
"detailsbutton.h",
"detailswidget.cpp",
"detailswidget.h",
"deviceshell.cpp",
"deviceshell.h",
"differ.cpp",
"differ.h",
"displayname.cpp",