forked from qt-creator/qt-creator
Utils: Add deviceshell generic class
Change-Id: Ibfb16a0f13f9fe119d27055db5897213127dd104 Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
@@ -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
|
||||
|
285
src/libs/utils/deviceshell.cpp
Normal file
285
src/libs/utils/deviceshell.cpp
Normal 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
|
81
src/libs/utils/deviceshell.h
Normal file
81
src/libs/utils/deviceshell.h
Normal 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
|
@@ -80,6 +80,8 @@ Project {
|
||||
"detailsbutton.h",
|
||||
"detailswidget.cpp",
|
||||
"detailswidget.h",
|
||||
"deviceshell.cpp",
|
||||
"deviceshell.h",
|
||||
"differ.cpp",
|
||||
"differ.h",
|
||||
"displayname.cpp",
|
||||
|
Reference in New Issue
Block a user