2012-10-02 09:12:39 +02:00
|
|
|
/****************************************************************************
|
2011-05-31 12:47:53 +02:00
|
|
|
**
|
2016-01-15 14:57:40 +01:00
|
|
|
** Copyright (C) 2016 The Qt Company Ltd.
|
|
|
|
** Contact: https://www.qt.io/licensing/
|
2011-05-31 12:47:53 +02:00
|
|
|
**
|
2012-10-02 09:12:39 +02:00
|
|
|
** This file is part of Qt Creator.
|
2011-05-31 12:47:53 +02:00
|
|
|
**
|
2012-10-02 09:12:39 +02:00
|
|
|
** 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
|
2016-01-15 14:57:40 +01:00
|
|
|
** 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.
|
2011-05-31 12:47:53 +02:00
|
|
|
**
|
2016-01-15 14:57:40 +01:00
|
|
|
** 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.
|
2011-05-31 12:47:53 +02:00
|
|
|
**
|
2012-10-02 09:12:39 +02:00
|
|
|
****************************************************************************/
|
2012-04-24 15:49:09 +02:00
|
|
|
|
2012-07-27 13:31:13 +02:00
|
|
|
#include "linuxdevice.h"
|
2011-05-31 12:47:53 +02:00
|
|
|
|
2012-04-06 18:28:16 +02:00
|
|
|
#include "genericlinuxdeviceconfigurationwidget.h"
|
2019-02-20 19:13:28 +01:00
|
|
|
#include "genericlinuxdeviceconfigurationwizard.h"
|
2013-08-08 14:05:11 +02:00
|
|
|
#include "linuxdeviceprocess.h"
|
2013-06-27 17:12:08 +02:00
|
|
|
#include "linuxdevicetester.h"
|
2012-04-06 18:28:16 +02:00
|
|
|
#include "publickeydeploymentdialog.h"
|
2011-07-25 11:55:00 +02:00
|
|
|
#include "remotelinux_constants.h"
|
2013-09-16 15:30:30 +02:00
|
|
|
#include "remotelinuxsignaloperation.h"
|
2016-04-06 13:56:32 +02:00
|
|
|
#include "remotelinuxenvironmentreader.h"
|
2011-07-25 11:55:00 +02:00
|
|
|
|
2019-02-20 19:13:28 +01:00
|
|
|
#include <coreplugin/icore.h>
|
2019-01-10 12:58:04 +01:00
|
|
|
#include <coreplugin/messagemanager.h>
|
2019-03-13 08:06:08 +01:00
|
|
|
|
2012-08-02 14:45:27 +02:00
|
|
|
#include <projectexplorer/devicesupport/sshdeviceprocesslist.h>
|
2019-03-13 08:06:08 +01:00
|
|
|
#include <projectexplorer/runcontrol.h>
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
#include <ssh/sshconnectionmanager.h>
|
2013-09-16 15:30:30 +02:00
|
|
|
#include <ssh/sshremoteprocessrunner.h>
|
2021-07-27 09:50:43 +02:00
|
|
|
#include <ssh/sshsettings.h>
|
2019-03-13 08:06:08 +01:00
|
|
|
|
2014-06-16 18:25:52 +04:00
|
|
|
#include <utils/algorithm.h>
|
2019-05-21 18:15:29 +02:00
|
|
|
#include <utils/environment.h>
|
2019-01-10 12:58:04 +01:00
|
|
|
#include <utils/hostosinfo.h>
|
2016-04-19 16:43:30 +02:00
|
|
|
#include <utils/port.h>
|
2012-02-15 14:47:45 -08:00
|
|
|
#include <utils/qtcassert.h>
|
2020-06-17 06:35:31 +02:00
|
|
|
#include <utils/stringutils.h>
|
2021-07-27 09:50:43 +02:00
|
|
|
#include <utils/temporaryfile.h>
|
|
|
|
|
|
|
|
#include <QDateTime>
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
#include <QMutex>
|
|
|
|
#include <QRegularExpression>
|
|
|
|
#include <QThread>
|
2011-08-02 12:20:16 +02:00
|
|
|
|
2012-07-26 16:23:20 +02:00
|
|
|
using namespace ProjectExplorer;
|
2021-07-27 09:50:43 +02:00
|
|
|
using namespace QSsh;
|
2019-06-20 17:19:12 +02:00
|
|
|
using namespace Utils;
|
2012-07-26 16:23:20 +02:00
|
|
|
|
2011-05-31 12:47:53 +02:00
|
|
|
namespace RemoteLinux {
|
2011-08-02 12:20:16 +02:00
|
|
|
|
2012-07-26 17:41:52 +02:00
|
|
|
const char Delimiter0[] = "x--";
|
|
|
|
const char Delimiter1[] = "---";
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
static Q_LOGGING_CATEGORY(linuxDeviceLog, "qtc.remotelinux.device", QtWarningMsg);
|
|
|
|
#define LOG(x) qCDebug(linuxDeviceLog) << x << '\n'
|
|
|
|
//#define DEBUG(x) qDebug() << x;
|
|
|
|
//#define DEBUG(x) LOG(x)
|
|
|
|
#define DEBUG(x)
|
|
|
|
|
2012-07-26 17:41:52 +02:00
|
|
|
static QString visualizeNull(QString s)
|
|
|
|
{
|
|
|
|
return s.replace(QLatin1Char('\0'), QLatin1String("<null>"));
|
|
|
|
}
|
|
|
|
|
2012-08-02 14:45:27 +02:00
|
|
|
class LinuxDeviceProcessList : public SshDeviceProcessList
|
2012-08-01 16:26:27 +02:00
|
|
|
{
|
2012-08-02 14:45:27 +02:00
|
|
|
public:
|
|
|
|
LinuxDeviceProcessList(const IDevice::ConstPtr &device, QObject *parent)
|
|
|
|
: SshDeviceProcessList(device, parent)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2018-11-24 17:06:01 +01:00
|
|
|
QString listProcessesCommandLine() const override
|
2012-08-02 14:45:27 +02:00
|
|
|
{
|
|
|
|
return QString::fromLatin1(
|
2012-08-01 16:26:27 +02:00
|
|
|
"for dir in `ls -d /proc/[0123456789]*`; do "
|
|
|
|
"test -d $dir || continue;" // Decrease the likelihood of a race condition.
|
|
|
|
"echo $dir;"
|
|
|
|
"cat $dir/cmdline;echo;" // cmdline does not end in newline
|
|
|
|
"cat $dir/stat;"
|
|
|
|
"readlink $dir/exe;"
|
2014-05-02 14:32:00 +02:00
|
|
|
"printf '%1''%2';"
|
2012-11-26 14:41:39 +02:00
|
|
|
"done").arg(QLatin1String(Delimiter0)).arg(QLatin1String(Delimiter1));
|
2012-08-02 14:45:27 +02:00
|
|
|
}
|
2012-08-01 16:26:27 +02:00
|
|
|
|
2018-11-24 17:06:01 +01:00
|
|
|
QList<DeviceProcessItem> buildProcessList(const QString &listProcessesReply) const override
|
2012-08-02 14:45:27 +02:00
|
|
|
{
|
2013-08-08 11:13:20 +02:00
|
|
|
QList<DeviceProcessItem> processes;
|
2012-08-02 14:45:27 +02:00
|
|
|
const QStringList lines = listProcessesReply.split(QString::fromLatin1(Delimiter0)
|
2020-07-21 10:19:36 +02:00
|
|
|
+ QString::fromLatin1(Delimiter1), Qt::SkipEmptyParts);
|
2012-08-02 14:45:27 +02:00
|
|
|
foreach (const QString &line, lines) {
|
|
|
|
const QStringList elements = line.split(QLatin1Char('\n'));
|
|
|
|
if (elements.count() < 4) {
|
|
|
|
qDebug("%s: Expected four list elements, got %d. Line was '%s'.", Q_FUNC_INFO,
|
2020-07-21 15:47:35 +02:00
|
|
|
int(elements.count()), qPrintable(visualizeNull(line)));
|
2012-08-01 16:26:27 +02:00
|
|
|
continue;
|
2012-08-02 14:45:27 +02:00
|
|
|
}
|
|
|
|
bool ok;
|
2020-09-18 13:15:18 +02:00
|
|
|
const int pid = elements.first().mid(6).toInt(&ok);
|
2012-08-02 14:45:27 +02:00
|
|
|
if (!ok) {
|
|
|
|
qDebug("%s: Expected number in %s. Line was '%s'.", Q_FUNC_INFO,
|
|
|
|
qPrintable(elements.first()), qPrintable(visualizeNull(line)));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
QString command = elements.at(1);
|
|
|
|
command.replace(QLatin1Char('\0'), QLatin1Char(' '));
|
|
|
|
if (command.isEmpty()) {
|
|
|
|
const QString &statString = elements.at(2);
|
|
|
|
const int openParenPos = statString.indexOf(QLatin1Char('('));
|
|
|
|
const int closedParenPos = statString.indexOf(QLatin1Char(')'), openParenPos);
|
|
|
|
if (openParenPos == -1 || closedParenPos == -1)
|
|
|
|
continue;
|
|
|
|
command = QLatin1Char('[')
|
|
|
|
+ statString.mid(openParenPos + 1, closedParenPos - openParenPos - 1)
|
|
|
|
+ QLatin1Char(']');
|
|
|
|
}
|
|
|
|
|
2013-08-08 11:13:20 +02:00
|
|
|
DeviceProcessItem process;
|
2012-08-02 14:45:27 +02:00
|
|
|
process.pid = pid;
|
|
|
|
process.cmdLine = command;
|
|
|
|
process.exe = elements.at(3);
|
|
|
|
processes.append(process);
|
2012-08-01 16:26:27 +02:00
|
|
|
}
|
|
|
|
|
2014-06-16 18:25:52 +04:00
|
|
|
Utils::sort(processes);
|
2012-08-02 14:45:27 +02:00
|
|
|
return processes;
|
2012-08-01 16:26:27 +02:00
|
|
|
}
|
2012-08-02 14:45:27 +02:00
|
|
|
};
|
2012-08-01 16:26:27 +02:00
|
|
|
|
2015-02-03 23:51:02 +02:00
|
|
|
class LinuxPortsGatheringMethod : public PortsGatheringMethod
|
2012-08-01 16:26:27 +02:00
|
|
|
{
|
2018-11-24 17:06:01 +01:00
|
|
|
Runnable runnable(QAbstractSocket::NetworkLayerProtocol protocol) const override
|
2012-08-01 16:26:27 +02:00
|
|
|
{
|
2016-04-15 12:23:23 +02:00
|
|
|
// We might encounter the situation that protocol is given IPv6
|
|
|
|
// but the consumer of the free port information decides to open
|
|
|
|
// an IPv4(only) port. As a result the next IPv6 scan will
|
|
|
|
// report the port again as open (in IPv6 namespace), while the
|
|
|
|
// same port in IPv4 namespace might still be blocked, and
|
|
|
|
// re-use of this port fails.
|
|
|
|
// GDBserver behaves exactly like this.
|
|
|
|
|
|
|
|
Q_UNUSED(protocol)
|
|
|
|
|
|
|
|
// /proc/net/tcp* covers /proc/net/tcp and /proc/net/tcp6
|
2018-05-16 15:42:03 +02:00
|
|
|
Runnable runnable;
|
2021-08-10 16:19:02 +02:00
|
|
|
runnable.command.setExecutable("sed");
|
2021-08-10 09:19:30 +02:00
|
|
|
runnable.command.setArguments("-e 's/.*: [[:xdigit:]]*:\\([[:xdigit:]]\\{4\\}\\).*/\\1/g' /proc/net/tcp*");
|
2017-08-07 18:54:01 +02:00
|
|
|
return runnable;
|
2012-08-01 16:26:27 +02:00
|
|
|
}
|
|
|
|
|
2018-11-24 17:06:01 +01:00
|
|
|
QList<Utils::Port> usedPorts(const QByteArray &output) const override
|
2012-08-01 16:26:27 +02:00
|
|
|
{
|
2016-04-19 16:43:30 +02:00
|
|
|
QList<Utils::Port> ports;
|
2012-08-01 16:26:27 +02:00
|
|
|
QList<QByteArray> portStrings = output.split('\n');
|
|
|
|
foreach (const QByteArray &portString, portStrings) {
|
2016-04-15 12:23:23 +02:00
|
|
|
if (portString.size() != 4)
|
2012-08-01 16:26:27 +02:00
|
|
|
continue;
|
|
|
|
bool ok;
|
2016-04-19 16:43:30 +02:00
|
|
|
const Utils::Port port(portString.toInt(&ok, 16));
|
2012-08-01 16:26:27 +02:00
|
|
|
if (ok) {
|
|
|
|
if (!ports.contains(port))
|
|
|
|
ports << port;
|
|
|
|
} else {
|
|
|
|
qWarning("%s: Unexpected string '%s' is not a port.",
|
|
|
|
Q_FUNC_INFO, portString.data());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ports;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
// ShellThreadHandler
|
|
|
|
|
|
|
|
class ShellThreadHandler : public QObject
|
2012-04-06 18:28:16 +02:00
|
|
|
{
|
2021-07-27 09:50:43 +02:00
|
|
|
public:
|
|
|
|
~ShellThreadHandler()
|
|
|
|
{
|
|
|
|
if (m_shell)
|
|
|
|
delete m_shell;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool start(const SshConnectionParameters ¶meters)
|
|
|
|
{
|
|
|
|
m_shell = new SshRemoteProcess("/bin/sh",
|
|
|
|
parameters.connectionOptions(SshSettings::sshFilePath()) << parameters.host(),
|
|
|
|
ProcessMode::Writer);
|
|
|
|
m_shell->start();
|
|
|
|
const bool ret = m_shell->waitForStarted();
|
|
|
|
if (!ret) {
|
|
|
|
delete m_shell;
|
|
|
|
m_shell = nullptr;
|
|
|
|
DEBUG("Failed to connect to " << parameters.host());
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool runInShell(const CommandLine &cmd, const QByteArray &data = {})
|
|
|
|
{
|
|
|
|
QTC_ASSERT(m_shell, return false);
|
|
|
|
const QByteArray prefix = !data.isEmpty() ? QByteArray("echo " + data + " | ")
|
|
|
|
: QByteArray("");
|
|
|
|
|
|
|
|
m_shell->readAllStandardOutput(); // clean possible left-overs
|
|
|
|
m_shell->write(prefix + cmd.toUserOutput().toUtf8() + "\necho $?\n");
|
|
|
|
DEBUG("RUN1 " << cmd.toUserOutput());
|
|
|
|
m_shell->waitForReadyRead();
|
|
|
|
const QByteArray output = m_shell->readAllStandardOutput();
|
|
|
|
DEBUG("GOT1 " << output);
|
|
|
|
bool ok = false;
|
|
|
|
const int result = output.toInt(&ok);
|
|
|
|
LOG("Run command in shell:" << cmd.toUserOutput() << "result: " << output << " ==>" << result);
|
|
|
|
return ok && result == 0;
|
|
|
|
}
|
|
|
|
|
2022-01-04 15:46:55 +01:00
|
|
|
QString outputForRunInShell(const QString &cmd)
|
2021-07-27 09:50:43 +02:00
|
|
|
{
|
|
|
|
QTC_ASSERT(m_shell, return {});
|
|
|
|
|
|
|
|
static int val = 0;
|
|
|
|
const QByteArray delim = QString::number(++val, 16).toUtf8();
|
|
|
|
|
2022-01-04 15:46:55 +01:00
|
|
|
DEBUG("RUN2 " << cmd);
|
2021-07-27 09:50:43 +02:00
|
|
|
m_shell->readAllStandardOutput(); // clean possible left-overs
|
|
|
|
const QByteArray marker = "___QTC___" + delim + "_OUTPUT_MARKER___";
|
2022-01-04 15:46:55 +01:00
|
|
|
DEBUG(" CMD: " << cmd.toUtf8() + "\necho " + marker + "\n");
|
|
|
|
m_shell->write(cmd.toUtf8() + "\necho " + marker + "\n");
|
2021-07-27 09:50:43 +02:00
|
|
|
QByteArray output;
|
|
|
|
while (!output.contains(marker)) {
|
|
|
|
DEBUG("OUTPUT" << output);
|
|
|
|
m_shell->waitForReadyRead();
|
|
|
|
output.append(m_shell->readAllStandardOutput());
|
|
|
|
}
|
|
|
|
DEBUG("GOT2 " << output);
|
2022-01-04 15:46:55 +01:00
|
|
|
LOG("Run command in shell:" << cmd << "output size:" << output.size());
|
2021-07-27 09:50:43 +02:00
|
|
|
const int pos = output.indexOf(marker);
|
|
|
|
if (pos >= 0)
|
|
|
|
output = output.left(pos);
|
|
|
|
DEBUG("CHOPPED2 " << output);
|
|
|
|
return QString::fromUtf8(output);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isRunning() const { return m_shell; }
|
|
|
|
private:
|
|
|
|
SshRemoteProcess *m_shell = nullptr;
|
|
|
|
};
|
|
|
|
|
|
|
|
// LinuxDevicePrivate
|
|
|
|
|
|
|
|
class LinuxDevicePrivate
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
explicit LinuxDevicePrivate(LinuxDevice *parent);
|
|
|
|
~LinuxDevicePrivate();
|
|
|
|
|
|
|
|
bool setupShell();
|
|
|
|
bool runInShell(const CommandLine &cmd, const QByteArray &data = {});
|
2022-01-04 15:46:55 +01:00
|
|
|
QString outputForRunInShell(const QString &cmd);
|
2021-07-27 09:50:43 +02:00
|
|
|
QString outputForRunInShell(const CommandLine &cmd);
|
|
|
|
|
|
|
|
LinuxDevice *q = nullptr;
|
|
|
|
QThread m_shellThread;
|
|
|
|
ShellThreadHandler *m_handler = nullptr;
|
|
|
|
mutable QMutex m_shellMutex;
|
|
|
|
};
|
|
|
|
|
|
|
|
// LinuxDevice
|
2012-04-06 18:28:16 +02:00
|
|
|
|
2019-01-15 08:51:13 +01:00
|
|
|
LinuxDevice::LinuxDevice()
|
2021-07-27 09:50:43 +02:00
|
|
|
: d(new LinuxDevicePrivate(this))
|
2019-01-11 14:50:08 +01:00
|
|
|
{
|
2019-06-19 13:28:14 +02:00
|
|
|
setDisplayType(tr("Generic Linux"));
|
2019-08-01 14:30:10 +02:00
|
|
|
setDefaultDisplayName(tr("Generic Linux Device"));
|
2019-08-16 10:17:45 +02:00
|
|
|
setOsType(OsTypeLinux);
|
|
|
|
|
2019-01-11 14:50:08 +01:00
|
|
|
addDeviceAction({tr("Deploy Public Key..."), [](const IDevice::Ptr &device, QWidget *parent) {
|
|
|
|
if (auto d = PublicKeyDeploymentDialog::createDialog(device, parent)) {
|
|
|
|
d->exec();
|
|
|
|
delete d;
|
|
|
|
}
|
|
|
|
}});
|
|
|
|
|
2021-08-02 18:02:10 +02:00
|
|
|
setOpenTerminal([this](const Environment &env, const FilePath &workingDir) {
|
2019-06-13 17:11:00 +02:00
|
|
|
DeviceProcess * const proc = createProcess(nullptr);
|
|
|
|
QObject::connect(proc, &DeviceProcess::finished, [proc] {
|
|
|
|
if (!proc->errorString().isEmpty()) {
|
2020-12-17 09:20:20 +01:00
|
|
|
Core::MessageManager::writeDisrupting(
|
|
|
|
tr("Error running remote shell: %1").arg(proc->errorString()));
|
2019-06-13 17:11:00 +02:00
|
|
|
}
|
|
|
|
proc->deleteLater();
|
|
|
|
});
|
|
|
|
QObject::connect(proc, &DeviceProcess::error, [proc] {
|
2020-12-17 09:20:20 +01:00
|
|
|
Core::MessageManager::writeDisrupting(tr("Error starting remote shell."));
|
2019-06-13 17:11:00 +02:00
|
|
|
proc->deleteLater();
|
|
|
|
});
|
|
|
|
Runnable runnable;
|
|
|
|
runnable.device = sharedFromThis();
|
|
|
|
runnable.environment = env;
|
|
|
|
runnable.workingDirectory = workingDir;
|
|
|
|
|
|
|
|
// It seems we cannot pass an environment to OpenSSH dynamically
|
|
|
|
// without specifying an executable.
|
|
|
|
if (env.size() > 0)
|
2021-08-10 16:19:02 +02:00
|
|
|
runnable.command.setExecutable("/bin/sh");
|
2019-06-13 17:11:00 +02:00
|
|
|
|
|
|
|
proc->setRunInTerminal(true);
|
|
|
|
proc->start(runnable);
|
|
|
|
});
|
|
|
|
|
2019-01-11 14:50:08 +01:00
|
|
|
if (Utils::HostOsInfo::isAnyUnixHost()) {
|
|
|
|
addDeviceAction({tr("Open Remote Shell"), [](const IDevice::Ptr &device, QWidget *) {
|
2021-08-02 18:02:10 +02:00
|
|
|
device->openTerminal(Environment(), FilePath());
|
2019-01-11 14:50:08 +01:00
|
|
|
}});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
LinuxDevice::~LinuxDevice()
|
|
|
|
{
|
|
|
|
delete d;
|
|
|
|
}
|
|
|
|
|
|
|
|
IDeviceWidget *LinuxDevice::createWidget()
|
|
|
|
{
|
|
|
|
return new GenericLinuxDeviceConfigurationWidget(sharedFromThis());
|
|
|
|
}
|
|
|
|
|
2013-08-08 14:05:11 +02:00
|
|
|
DeviceProcess *LinuxDevice::createProcess(QObject *parent) const
|
|
|
|
{
|
|
|
|
return new LinuxDeviceProcess(sharedFromThis(), parent);
|
|
|
|
}
|
|
|
|
|
2013-04-18 09:31:22 +02:00
|
|
|
bool LinuxDevice::canAutoDetectPorts() const
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2012-08-01 16:26:27 +02:00
|
|
|
PortsGatheringMethod::Ptr LinuxDevice::portsGatheringMethod() const
|
2012-07-26 17:41:52 +02:00
|
|
|
{
|
2012-08-01 16:26:27 +02:00
|
|
|
return LinuxPortsGatheringMethod::Ptr(new LinuxPortsGatheringMethod);
|
2012-07-26 17:41:52 +02:00
|
|
|
}
|
|
|
|
|
2012-08-02 14:45:27 +02:00
|
|
|
DeviceProcessList *LinuxDevice::createProcessListModel(QObject *parent) const
|
|
|
|
{
|
|
|
|
return new LinuxDeviceProcessList(sharedFromThis(), parent);
|
|
|
|
}
|
|
|
|
|
2013-06-27 17:12:08 +02:00
|
|
|
DeviceTester *LinuxDevice::createDeviceTester() const
|
2013-05-30 13:53:28 +02:00
|
|
|
{
|
|
|
|
return new GenericLinuxDeviceTester;
|
|
|
|
}
|
|
|
|
|
2013-09-16 15:30:30 +02:00
|
|
|
DeviceProcessSignalOperation::Ptr LinuxDevice::signalOperation() const
|
|
|
|
{
|
|
|
|
return DeviceProcessSignalOperation::Ptr(new RemoteLinuxSignalOperation(sshParameters()));
|
|
|
|
}
|
|
|
|
|
2016-04-06 13:56:32 +02:00
|
|
|
class LinuxDeviceEnvironmentFetcher : public DeviceEnvironmentFetcher
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
LinuxDeviceEnvironmentFetcher(const IDevice::ConstPtr &device)
|
|
|
|
: m_reader(device)
|
|
|
|
{
|
|
|
|
connect(&m_reader, &Internal::RemoteLinuxEnvironmentReader::finished,
|
|
|
|
this, &LinuxDeviceEnvironmentFetcher::readerFinished);
|
|
|
|
connect(&m_reader, &Internal::RemoteLinuxEnvironmentReader::error,
|
|
|
|
this, &LinuxDeviceEnvironmentFetcher::readerError);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
void start() override { m_reader.start(); }
|
|
|
|
void readerFinished() { emit finished(m_reader.remoteEnvironment(), true); }
|
|
|
|
void readerError() { emit finished(Utils::Environment(), false); }
|
|
|
|
|
|
|
|
Internal::RemoteLinuxEnvironmentReader m_reader;
|
|
|
|
};
|
|
|
|
|
|
|
|
DeviceEnvironmentFetcher::Ptr LinuxDevice::environmentFetcher() const
|
|
|
|
{
|
|
|
|
return DeviceEnvironmentFetcher::Ptr(new LinuxDeviceEnvironmentFetcher(sharedFromThis()));
|
|
|
|
}
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
QString LinuxDevice::userAtHost() const
|
|
|
|
{
|
|
|
|
if (sshParameters().userName().isEmpty())
|
|
|
|
return sshParameters().host();
|
|
|
|
return sshParameters().userName() + '@' + sshParameters().host();
|
|
|
|
}
|
|
|
|
|
2022-01-04 18:06:42 +01:00
|
|
|
FilePath LinuxDevice::mapToGlobalPath(const FilePath &pathOnDevice) const
|
|
|
|
{
|
|
|
|
if (pathOnDevice.needsDevice()) {
|
|
|
|
// Already correct form, only sanity check it's ours...
|
|
|
|
QTC_CHECK(handlesFile(pathOnDevice));
|
|
|
|
return pathOnDevice;
|
|
|
|
}
|
|
|
|
FilePath result;
|
|
|
|
result.setScheme("ssh");
|
|
|
|
result.setHost(userAtHost());
|
|
|
|
result.setPath(pathOnDevice.path());
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
bool LinuxDevice::handlesFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
return filePath.scheme() == "ssh" && filePath.host() == userAtHost();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LinuxDevice::runProcess(QtcProcess &process) const
|
|
|
|
{
|
|
|
|
QTC_CHECK(false); // FIXME: Implement
|
|
|
|
}
|
|
|
|
|
|
|
|
LinuxDevicePrivate::LinuxDevicePrivate(LinuxDevice *parent)
|
|
|
|
: q(parent)
|
|
|
|
{
|
|
|
|
m_handler = new ShellThreadHandler();
|
|
|
|
m_handler->moveToThread(&m_shellThread);
|
|
|
|
QObject::connect(&m_shellThread, &QThread::finished, m_handler, &QObject::deleteLater);
|
|
|
|
m_shellThread.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
LinuxDevicePrivate::~LinuxDevicePrivate()
|
|
|
|
{
|
|
|
|
m_shellThread.quit();
|
|
|
|
m_shellThread.wait();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevicePrivate::setupShell()
|
|
|
|
{
|
|
|
|
bool ok = false;
|
|
|
|
QMetaObject::invokeMethod(m_handler, [this, parameters = q->sshParameters()] {
|
|
|
|
return m_handler->start(parameters);
|
|
|
|
}, Qt::BlockingQueuedConnection, &ok);
|
|
|
|
return ok;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevicePrivate::runInShell(const CommandLine &cmd, const QByteArray &data)
|
|
|
|
{
|
|
|
|
QMutexLocker locker(&m_shellMutex);
|
|
|
|
DEBUG(cmd.toUserOutput());
|
|
|
|
if (!m_handler->isRunning()) {
|
|
|
|
const bool ok = setupShell();
|
|
|
|
QTC_ASSERT(ok, return false);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ret = false;
|
|
|
|
QMetaObject::invokeMethod(m_handler, [this, &cmd, &data] {
|
|
|
|
return m_handler->runInShell(cmd, data);
|
|
|
|
}, Qt::BlockingQueuedConnection, &ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2022-01-04 15:46:55 +01:00
|
|
|
QString LinuxDevicePrivate::outputForRunInShell(const QString &cmd)
|
2021-07-27 09:50:43 +02:00
|
|
|
{
|
|
|
|
QMutexLocker locker(&m_shellMutex);
|
2022-01-04 15:46:55 +01:00
|
|
|
DEBUG(cmd);
|
2021-07-27 09:50:43 +02:00
|
|
|
if (!m_handler->isRunning()) {
|
|
|
|
const bool ok = setupShell();
|
|
|
|
QTC_ASSERT(ok, return {});
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ret;
|
|
|
|
QMetaObject::invokeMethod(m_handler, [this, &cmd] {
|
|
|
|
return m_handler->outputForRunInShell(cmd);
|
|
|
|
}, Qt::BlockingQueuedConnection, &ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2022-01-04 15:46:55 +01:00
|
|
|
QString LinuxDevicePrivate::outputForRunInShell(const CommandLine &cmd)
|
|
|
|
{
|
|
|
|
return outputForRunInShell(cmd.toUserOutput());
|
|
|
|
}
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
bool LinuxDevice::isExecutableFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-x", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isReadableFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-r", path, "-a", "-f", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isWritableFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-w", path, "-a", "-f", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isReadableDirectory(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-r", path, "-a", "-d", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isWritableDirectory(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-w", path, "-a", "-d", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-f", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::isDirectory(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-d", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::createDirectory(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"mkdir", {"-p", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::exists(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
DEBUG("filepath " << filePath.path());
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"test", {"-e", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::ensureExistingFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const QString path = filePath.path();
|
|
|
|
return d->runInShell({"touch", {path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::removeFile(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
return d->runInShell({"rm", {filePath.path()}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::removeRecursively(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
QTC_ASSERT(filePath.path().startsWith('/'), return false);
|
|
|
|
|
|
|
|
const QString path = filePath.cleanPath().path();
|
|
|
|
// We are expecting this only to be called in a context of build directories or similar.
|
|
|
|
// Chicken out in some cases that _might_ be user code errors.
|
|
|
|
QTC_ASSERT(path.startsWith('/'), return false);
|
2022-01-04 15:11:49 +01:00
|
|
|
const int levelsNeeded = path.startsWith("/home/") ? 3 : 2;
|
2021-07-27 09:50:43 +02:00
|
|
|
QTC_ASSERT(path.count('/') >= levelsNeeded, return false);
|
|
|
|
|
|
|
|
return d->runInShell({"rm", {"-rf", "--", path}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::copyFile(const FilePath &filePath, const FilePath &target) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
QTC_ASSERT(handlesFile(target), return false);
|
|
|
|
return d->runInShell({"cp", {filePath.path(), target.path()}});
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::renameFile(const FilePath &filePath, const FilePath &target) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
QTC_ASSERT(handlesFile(target), return false);
|
|
|
|
return d->runInShell({"mv", {filePath.path(), target.path()}});
|
|
|
|
}
|
|
|
|
|
|
|
|
QDateTime LinuxDevice::lastModified(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return {});
|
|
|
|
const QString output = d->outputForRunInShell({"stat", {"-c", "%Y", filePath.path()}});
|
|
|
|
const qint64 secs = output.toLongLong();
|
|
|
|
const QDateTime dt = QDateTime::fromSecsSinceEpoch(secs, Qt::UTC);
|
|
|
|
return dt;
|
|
|
|
}
|
|
|
|
|
|
|
|
FilePath LinuxDevice::symLinkTarget(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return {});
|
|
|
|
const QString output = d->outputForRunInShell({"readlink", {"-n", "-e", filePath.path()}});
|
|
|
|
return output.isEmpty() ? FilePath() : filePath.withNewPath(output);
|
|
|
|
}
|
|
|
|
|
|
|
|
qint64 LinuxDevice::fileSize(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return -1);
|
|
|
|
const QString output = d->outputForRunInShell({"stat", {"-c", "%s", filePath.path()}});
|
|
|
|
return output.toLongLong();
|
|
|
|
}
|
|
|
|
|
2022-01-04 15:46:55 +01:00
|
|
|
qint64 LinuxDevice::bytesAvailable(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return -1);
|
|
|
|
CommandLine cmd("df", {"-k"});
|
|
|
|
cmd.addArg(filePath.path());
|
|
|
|
cmd.addArgs("|tail -n 1 |sed 's/ */ /g'|cut -d ' ' -f 4", CommandLine::Raw);
|
|
|
|
const QString output = d->outputForRunInShell(cmd.toUserOutput());
|
|
|
|
bool ok = false;
|
|
|
|
const qint64 size = output.toLongLong(&ok);
|
|
|
|
if (ok)
|
|
|
|
return size * 1024;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2021-07-27 09:50:43 +02:00
|
|
|
QFileDevice::Permissions LinuxDevice::permissions(const FilePath &filePath) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return {});
|
|
|
|
const QString output = d->outputForRunInShell({"stat", {"-c", "%a", filePath.path()}});
|
|
|
|
const uint bits = output.toUInt(nullptr, 8);
|
|
|
|
QFileDevice::Permissions perm = {};
|
|
|
|
#define BIT(n, p) if (bits & (1<<n)) perm |= QFileDevice::p
|
|
|
|
BIT(0, ExeOther);
|
|
|
|
BIT(1, WriteOther);
|
|
|
|
BIT(2, ReadOther);
|
|
|
|
BIT(3, ExeGroup);
|
|
|
|
BIT(4, WriteGroup);
|
|
|
|
BIT(5, ReadGroup);
|
|
|
|
BIT(6, ExeUser);
|
|
|
|
BIT(7, WriteUser);
|
|
|
|
BIT(8, ReadUser);
|
|
|
|
#undef BIT
|
|
|
|
return perm;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::setPermissions(const Utils::FilePath &filePath, QFileDevice::Permissions permissions) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return false);
|
|
|
|
const int flags = int(permissions);
|
|
|
|
return d->runInShell({"chmod", {QString::number(flags, 16), filePath.path()}});
|
|
|
|
}
|
|
|
|
|
|
|
|
static void filterEntriesHelper(const FilePath &base,
|
|
|
|
const std::function<bool(const FilePath &)> &callBack,
|
|
|
|
const QStringList &entries,
|
|
|
|
const QStringList &nameFilters,
|
|
|
|
QDir::Filters filters)
|
|
|
|
{
|
|
|
|
const QList<QRegularExpression> nameRegexps = transform(nameFilters, [](const QString &filter) {
|
|
|
|
QRegularExpression re;
|
|
|
|
re.setPattern(QRegularExpression::wildcardToRegularExpression(filter));
|
|
|
|
QTC_CHECK(re.isValid());
|
|
|
|
return re;
|
|
|
|
});
|
|
|
|
|
|
|
|
const auto nameMatches = [&nameRegexps](const QString &fileName) {
|
|
|
|
for (const QRegularExpression &re : nameRegexps) {
|
|
|
|
const QRegularExpressionMatch match = re.match(fileName);
|
|
|
|
if (match.hasMatch())
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return nameRegexps.isEmpty();
|
|
|
|
};
|
|
|
|
|
|
|
|
// FIXME: Handle filters. For now bark on unsupported options.
|
|
|
|
QTC_CHECK(filters == QDir::NoFilter);
|
|
|
|
|
|
|
|
for (const QString &entry : entries) {
|
|
|
|
if (!nameMatches(entry))
|
|
|
|
continue;
|
|
|
|
if (!callBack(base.pathAppended(entry)))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void LinuxDevice::iterateDirectory(const FilePath &filePath,
|
|
|
|
const std::function<bool(const FilePath &)> &callBack,
|
|
|
|
const QStringList &nameFilters,
|
|
|
|
QDir::Filters filters) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return);
|
|
|
|
// if we do not have find - use ls as fallback
|
|
|
|
const QString output = d->outputForRunInShell({"ls", {"-1", "-b", "--", filePath.path()}});
|
|
|
|
const QStringList entries = output.split('\n', Qt::SkipEmptyParts);
|
|
|
|
filterEntriesHelper(filePath, callBack, entries, nameFilters, filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray LinuxDevice::fileContents(const FilePath &filePath, qint64 limit, qint64 offset) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return {});
|
|
|
|
QString args = "if=" + filePath.path() + " status=none";
|
|
|
|
if (limit > 0 || offset > 0) {
|
|
|
|
const qint64 gcd = std::gcd(limit, offset);
|
|
|
|
args += QString(" bs=%1 count=%2 seek=%3").arg(gcd).arg(limit / gcd).arg(offset / gcd);
|
|
|
|
}
|
|
|
|
CommandLine cmd(FilePath::fromString("dd"), args, CommandLine::Raw);
|
|
|
|
|
|
|
|
const QString output = d->outputForRunInShell(cmd);
|
|
|
|
DEBUG(output << output.toLatin1() << QByteArray::fromHex(output.toLatin1()));
|
|
|
|
return output.toLatin1();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinuxDevice::writeFileContents(const FilePath &filePath, const QByteArray &data) const
|
|
|
|
{
|
|
|
|
QTC_ASSERT(handlesFile(filePath), return {});
|
|
|
|
|
|
|
|
// This following would be the generic Unix solution.
|
|
|
|
// But it doesn't pass input. FIXME: Why?
|
|
|
|
return d->runInShell({"dd", {"of=" + filePath.path()}}, data);
|
|
|
|
}
|
|
|
|
|
2019-02-20 19:13:28 +01:00
|
|
|
namespace Internal {
|
|
|
|
|
|
|
|
// Factory
|
|
|
|
|
|
|
|
LinuxDeviceFactory::LinuxDeviceFactory()
|
|
|
|
: IDeviceFactory(Constants::GenericLinuxOsType)
|
|
|
|
{
|
2020-01-21 16:17:59 +01:00
|
|
|
setDisplayName(LinuxDevice::tr("Generic Linux Device"));
|
2019-02-20 19:13:28 +01:00
|
|
|
setIcon(QIcon());
|
|
|
|
setCanCreate(true);
|
|
|
|
setConstructionFunction(&LinuxDevice::create);
|
|
|
|
}
|
|
|
|
|
|
|
|
IDevice::Ptr LinuxDeviceFactory::create() const
|
|
|
|
{
|
2020-06-02 09:10:40 +02:00
|
|
|
GenericLinuxDeviceConfigurationWizard wizard(Core::ICore::dialogParent());
|
2019-02-20 19:13:28 +01:00
|
|
|
if (wizard.exec() != QDialog::Accepted)
|
|
|
|
return IDevice::Ptr();
|
|
|
|
return wizard.device();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2011-05-31 12:47:53 +02:00
|
|
|
} // namespace RemoteLinux
|