RemoteLinux: Implement some of the remote file API

Implementation of remote file API for correct FilePath
work with RemoteLinux.
Added tests for this functionality

Run ssh shell in separate thread.
The linux device instance keeps its own thread for running
SshRemoteProcess. In this way all calls to filepath
interface of linux device coming from different threads
are executed in one thread (SshRemoteProcess is reentrant,
but not thread safe). The redirection to the device thread
is done by invoking SshRemoteProcess' methods through
BlockingQueuedConnection.

Done-by: Artem Sokolovskii
Change-Id: Id8756738d3a4597f175c8ef000c148d0c8536eeb
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
hjk
2021-07-27 09:50:43 +02:00
committed by Artem Sokolovskii
parent 8d06519211
commit 20d19aa5bf
11 changed files with 722 additions and 39 deletions

View File

@@ -66,6 +66,46 @@ SshConnectionParameters::SshConnectionParameters()
url.setPort(0); url.setPort(0);
} }
QStringList SshConnectionParameters::connectionOptions(const FilePath &binary) const
{
QString hostKeyCheckingString;
switch (hostKeyCheckingMode) {
case SshHostKeyCheckingNone:
case SshHostKeyCheckingAllowNoMatch:
// There is "accept-new" as well, but only since 7.6.
hostKeyCheckingString = "no";
break;
case SshHostKeyCheckingStrict:
hostKeyCheckingString = "yes";
break;
}
QStringList args{"-o", "StrictHostKeyChecking=" + hostKeyCheckingString,
"-o", "Port=" + QString::number(port())};
if (!userName().isEmpty())
args.append({"-o", "User=" + userName()});
const bool keyOnly = authenticationType ==
SshConnectionParameters::AuthenticationTypeSpecificKey;
if (keyOnly) {
args << "-o" << "IdentitiesOnly=yes";
args << "-i" << privateKeyFile.path();
}
if (keyOnly || SshSettings::askpassFilePath().isEmpty())
args << "-o" << "BatchMode=yes";
bool useTimeout = timeout != 0;
if (useTimeout && HostOsInfo::isWindowsHost()
&& binary.toString().toLower().contains("/system32/")) {
useTimeout = false;
}
if (useTimeout)
args << "-o" << ("ConnectTimeout=" + QString::number(timeout));
return args;
}
static inline bool equals(const SshConnectionParameters &p1, const SshConnectionParameters &p2) static inline bool equals(const SshConnectionParameters &p1, const SshConnectionParameters &p2)
{ {
return p1.url == p2.url return p1.url == p2.url
@@ -113,38 +153,10 @@ struct SshConnection::SshConnectionPrivate
QStringList connectionOptions(const FilePath &binary) const QStringList connectionOptions(const FilePath &binary) const
{ {
QString hostKeyCheckingString; QStringList options = connParams.connectionOptions(binary);
switch (connParams.hostKeyCheckingMode) {
case SshHostKeyCheckingNone:
case SshHostKeyCheckingAllowNoMatch:
// There is "accept-new" as well, but only since 7.6.
hostKeyCheckingString = "no";
break;
case SshHostKeyCheckingStrict:
hostKeyCheckingString = "yes";
break;
}
QStringList args{"-o", "StrictHostKeyChecking=" + hostKeyCheckingString,
"-o", "User=" + connParams.userName(),
"-o", "Port=" + QString::number(connParams.port())};
const bool keyOnly = connParams.authenticationType ==
SshConnectionParameters::AuthenticationTypeSpecificKey;
if (keyOnly) {
args << "-o" << "IdentitiesOnly=yes";
args << "-i" << connParams.privateKeyFile.path();
}
if (keyOnly || SshSettings::askpassFilePath().isEmpty())
args << "-o" << "BatchMode=yes";
if (sharingEnabled) if (sharingEnabled)
args << "-o" << ("ControlPath=" + socketFilePath()); options << "-o" << ("ControlPath=" + socketFilePath());
bool useTimeout = connParams.timeout != 0; return options;
if (useTimeout && HostOsInfo::isWindowsHost()
&& binary.toString().toLower().contains("/system32/")) {
useTimeout = false;
}
if (useTimeout)
args << "-o" << ("ConnectTimeout=" + QString::number(connParams.timeout));
return args;
} }
QStringList connectionArgs(const FilePath &binary) const QStringList connectionArgs(const FilePath &binary) const

View File

@@ -65,6 +65,8 @@ public:
void setPort(int port) { url.setPort(port); } void setPort(int port) { url.setPort(port); }
void setUserName(const QString &name) { url.setUserName(name); } void setUserName(const QString &name) { url.setUserName(name); }
QStringList connectionOptions(const Utils::FilePath &binary) const;
QUrl url; QUrl url;
Utils::FilePath privateKeyFile; Utils::FilePath privateKeyFile;
QString x11DisplayName; QString x11DisplayName;

View File

@@ -25,21 +25,21 @@
#pragma once #pragma once
#include "ssh_global.h"
#include "sshprocess.h" #include "sshprocess.h"
namespace QSsh { namespace Utils { class CommandLine; }
class SshConnection; namespace QSsh {
class QSSH_EXPORT SshRemoteProcess : public SshProcess class QSSH_EXPORT SshRemoteProcess : public SshProcess
{ {
Q_OBJECT Q_OBJECT
friend class SshConnection; public:
SshRemoteProcess(const QString &command, const QStringList &connectionArgs, SshRemoteProcess(const QString &command, const QStringList &connectionArgs,
Utils::ProcessMode processMode = Utils::ProcessMode::Reader); Utils::ProcessMode processMode = Utils::ProcessMode::Reader);
public:
void requestX11Forwarding(const QString &displayName); void requestX11Forwarding(const QString &displayName);
void start(); void start();
@@ -49,6 +49,8 @@ signals:
void done(const QString &error); void done(const QString &error);
private: private:
void doStart();
QString m_remoteCommand; QString m_remoteCommand;
QStringList m_connectionArgs; QStringList m_connectionArgs;
QString m_displayName; QString m_displayName;

View File

@@ -1519,7 +1519,6 @@ static void filterEntriesHelper(const FilePath &base,
// FIXME: Handle filters. For now bark on unsupported options. // FIXME: Handle filters. For now bark on unsupported options.
QTC_CHECK(filters == QDir::NoFilter); QTC_CHECK(filters == QDir::NoFilter);
FilePaths result;
for (const QString &entry : entries) { for (const QString &entry : entries) {
if (!nameMatches(entry)) if (!nameMatches(entry))
continue; continue;

View File

@@ -7,6 +7,7 @@ add_qtc_plugin(RemoteLinux
abstractremotelinuxdeploystep.cpp abstractremotelinuxdeploystep.h abstractremotelinuxdeploystep.cpp abstractremotelinuxdeploystep.h
abstractuploadandinstallpackageservice.cpp abstractuploadandinstallpackageservice.h abstractuploadandinstallpackageservice.cpp abstractuploadandinstallpackageservice.h
deploymenttimeinfo.cpp deploymenttimeinfo.h deploymenttimeinfo.cpp deploymenttimeinfo.h
filesystemaccess_test.cpp filesystemaccess_test.h
genericdirectuploadservice.cpp genericdirectuploadservice.h genericdirectuploadservice.cpp genericdirectuploadservice.h
genericdirectuploadstep.cpp genericdirectuploadstep.h genericdirectuploadstep.cpp genericdirectuploadstep.h
genericlinuxdeviceconfigurationwidget.cpp genericlinuxdeviceconfigurationwidget.h genericlinuxdeviceconfigurationwidget.ui genericlinuxdeviceconfigurationwidget.cpp genericlinuxdeviceconfigurationwidget.h genericlinuxdeviceconfigurationwidget.ui

View File

@@ -0,0 +1,141 @@
/****************************************************************************
**
** Copyright (C) 2016 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 "filesystemaccess_test.h"
#include "linuxdevice.h"
#include "remotelinux_constants.h"
#include <utils/filepath.h>
#include <projectexplorer/devicesupport/devicemanager.h>
#include <ssh/sshconnection.h>
#include <QTest>
#include <QDebug>
using namespace ProjectExplorer;
using namespace Utils;
namespace RemoteLinux {
namespace Internal {
static const char TEST_IP[] = "127.0.0.1";
TestLinuxDeviceFactory::TestLinuxDeviceFactory()
: IDeviceFactory("test")
{
setDisplayName("Generic Linux Device");
setIcon(QIcon());
setCanCreate(true);
setConstructionFunction(&LinuxDevice::create);
}
IDevice::Ptr TestLinuxDeviceFactory::create() const
{
LinuxDevice::Ptr newDev = LinuxDevice::create();
qDebug() << "device : " << newDev->type();
newDev->setType("test");
QSsh::SshConnectionParameters sshParams = newDev->sshParameters();
sshParams.setHost(TEST_IP);
sshParams.setPort(22);
newDev->setSshParameters(sshParams);
return newDev;
}
void FileSystemAccessTest::initTestCase()
{
FilePath filePath = FilePath::fromString("ssh://"+ QString(TEST_IP) +"/tmp");
if (DeviceManager::deviceForPath(filePath) == nullptr) {
DeviceManager *const devMgr = DeviceManager::instance();
const IDevice::Ptr newDev = m_testLinuxDeviceFactory.create();
QVERIFY(!newDev.isNull());
devMgr->addDevice(newDev);
}
}
void FileSystemAccessTest::testDirStatuses()
{
FilePath filePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/tmp");
QVERIFY(filePath.exists());
QVERIFY(filePath.isDir());
QVERIFY(filePath.isWritableDir());
FilePath testFilePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/tmp/test");
FilePath dummyFilePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/dev/null");
dummyFilePath.copyFile(testFilePath);
QVERIFY(testFilePath.exists());
QVERIFY(testFilePath.isFile());
bool fileExists = false;
filePath.iterateDirectory(
[&fileExists](const FilePath &filePath) {
if (filePath.baseName() == "test"){
fileExists = true;
return true;
}
return false;
},
{"test"},
QDir::Files);
QVERIFY(fileExists);
QVERIFY(testFilePath.removeFile());
QVERIFY(!testFilePath.exists());
}
void FileSystemAccessTest::testFileActions()
{
FilePath testFilePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/tmp/test");
FilePath dummyFilePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/dev/null");
dummyFilePath.copyFile(testFilePath);
QVERIFY(testFilePath.exists());
QVERIFY(testFilePath.isFile());
testFilePath.setPermissions(QFile::ReadOther | QFile::WriteOther | QFile::ExeOther
| QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup
| QFile::ReadUser | QFile::WriteUser | QFile::ExeUser);
QVERIFY(testFilePath.isWritableFile());
QVERIFY(testFilePath.isReadableFile());
QVERIFY(testFilePath.isExecutableFile());
QByteArray content("Test");
testFilePath.writeFileContents(content);
// ToDo: remove ".contains", make fileContents exact equal content
QVERIFY(testFilePath.fileContents().contains(content));
QVERIFY(testFilePath.renameFile(FilePath::fromString("ssh://" + QString(TEST_IP) + "/tmp/test1")));
// It is Ok that FilePath doesn't change itself after rename.
FilePath newTestFilePath = FilePath::fromString("ssh://" + QString(TEST_IP) + "/tmp/test1");
QVERIFY(newTestFilePath.exists());
QVERIFY(!testFilePath.removeFile());
QVERIFY(newTestFilePath.exists());
QVERIFY(newTestFilePath.removeFile());
QVERIFY(!newTestFilePath.exists());
}
} // Internal
} // RemoteLinux

View File

@@ -0,0 +1,61 @@
/****************************************************************************
**
** Copyright (C) 2021 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.
**
****************************************************************************/
#pragma once
#include <projectexplorer/devicesupport/idevice.h>
#include <projectexplorer/devicesupport/idevicefactory.h>
#include <utils/fileutils.h>
namespace RemoteLinux {
namespace Internal {
class TestLinuxDeviceFactory final : public ProjectExplorer::IDeviceFactory
{
public:
TestLinuxDeviceFactory();
Utils::FilePaths defaultKeys() const;
ProjectExplorer::IDevice::Ptr create() const override;
};
class FileSystemAccessTest : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testDirStatuses();
void testFileActions();
private:
TestLinuxDeviceFactory m_testLinuxDeviceFactory;
};
} // Internal
} // RemoteLinux

View File

@@ -40,7 +40,9 @@
#include <projectexplorer/devicesupport/sshdeviceprocesslist.h> #include <projectexplorer/devicesupport/sshdeviceprocesslist.h>
#include <projectexplorer/runcontrol.h> #include <projectexplorer/runcontrol.h>
#include <ssh/sshconnectionmanager.h>
#include <ssh/sshremoteprocessrunner.h> #include <ssh/sshremoteprocessrunner.h>
#include <ssh/sshsettings.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/environment.h> #include <utils/environment.h>
@@ -48,8 +50,16 @@
#include <utils/port.h> #include <utils/port.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/stringutils.h> #include <utils/stringutils.h>
#include <utils/temporaryfile.h>
#include <QDateTime>
#include <QLoggingCategory>
#include <QMutex>
#include <QRegularExpression>
#include <QThread>
using namespace ProjectExplorer; using namespace ProjectExplorer;
using namespace QSsh;
using namespace Utils; using namespace Utils;
namespace RemoteLinux { namespace RemoteLinux {
@@ -57,6 +67,12 @@ namespace RemoteLinux {
const char Delimiter0[] = "x--"; const char Delimiter0[] = "x--";
const char Delimiter1[] = "---"; const char Delimiter1[] = "---";
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)
static QString visualizeNull(QString s) static QString visualizeNull(QString s)
{ {
return s.replace(QLatin1Char('\0'), QLatin1String("<null>")); return s.replace(QLatin1Char('\0'), QLatin1String("<null>"));
@@ -170,12 +186,104 @@ class LinuxPortsGatheringMethod : public PortsGatheringMethod
} }
}; };
IDeviceWidget *LinuxDevice::createWidget() // ShellThreadHandler
class ShellThreadHandler : public QObject
{ {
return new GenericLinuxDeviceConfigurationWidget(sharedFromThis()); public:
} ~ShellThreadHandler()
{
if (m_shell)
delete m_shell;
}
bool start(const SshConnectionParameters &parameters)
{
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;
}
QString outputForRunInShell(const CommandLine &cmd)
{
QTC_ASSERT(m_shell, return {});
static int val = 0;
const QByteArray delim = QString::number(++val, 16).toUtf8();
DEBUG("RUN2 " << cmd.toUserOutput());
m_shell->readAllStandardOutput(); // clean possible left-overs
const QByteArray marker = "___QTC___" + delim + "_OUTPUT_MARKER___";
DEBUG(" CMD: " << cmd.toUserOutput().toUtf8() + "\necho " + marker + "\n");
m_shell->write(cmd.toUserOutput().toUtf8() + "\necho " + marker + "\n");
QByteArray output;
while (!output.contains(marker)) {
DEBUG("OUTPUT" << output);
m_shell->waitForReadyRead();
output.append(m_shell->readAllStandardOutput());
}
DEBUG("GOT2 " << output);
LOG("Run command in shell:" << cmd.toUserOutput() << "output size:" << output.size());
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 = {});
QString outputForRunInShell(const CommandLine &cmd);
LinuxDevice *q = nullptr;
QThread m_shellThread;
ShellThreadHandler *m_handler = nullptr;
mutable QMutex m_shellMutex;
};
// LinuxDevice
LinuxDevice::LinuxDevice() LinuxDevice::LinuxDevice()
: d(new LinuxDevicePrivate(this))
{ {
setDisplayType(tr("Generic Linux")); setDisplayType(tr("Generic Linux"));
setDefaultDisplayName(tr("Generic Linux Device")); setDefaultDisplayName(tr("Generic Linux Device"));
@@ -222,6 +330,16 @@ LinuxDevice::LinuxDevice()
} }
} }
LinuxDevice::~LinuxDevice()
{
delete d;
}
IDeviceWidget *LinuxDevice::createWidget()
{
return new GenericLinuxDeviceConfigurationWidget(sharedFromThis());
}
DeviceProcess *LinuxDevice::createProcess(QObject *parent) const DeviceProcess *LinuxDevice::createProcess(QObject *parent) const
{ {
return new LinuxDeviceProcess(sharedFromThis(), parent); return new LinuxDeviceProcess(sharedFromThis(), parent);
@@ -277,6 +395,305 @@ DeviceEnvironmentFetcher::Ptr LinuxDevice::environmentFetcher() const
return DeviceEnvironmentFetcher::Ptr(new LinuxDeviceEnvironmentFetcher(sharedFromThis())); return DeviceEnvironmentFetcher::Ptr(new LinuxDeviceEnvironmentFetcher(sharedFromThis()));
} }
QString LinuxDevice::userAtHost() const
{
if (sshParameters().userName().isEmpty())
return sshParameters().host();
return sshParameters().userName() + '@' + sshParameters().host();
}
bool LinuxDevice::handlesFile(const FilePath &filePath) const
{
DEBUG("handlesFile " << filePath.scheme() << filePath.host() << userAtHost());
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;
}
QString LinuxDevicePrivate::outputForRunInShell(const CommandLine &cmd)
{
QMutexLocker locker(&m_shellMutex);
DEBUG(cmd.toUserOutput());
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;
}
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);
const int levelsNeeded = path.startsWith("/home/") ? 4 : 3;
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();
}
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);
}
namespace Internal { namespace Internal {
// Factory // Factory

View File

@@ -40,6 +40,8 @@ public:
using Ptr = QSharedPointer<LinuxDevice>; using Ptr = QSharedPointer<LinuxDevice>;
using ConstPtr = QSharedPointer<const LinuxDevice>; using ConstPtr = QSharedPointer<const LinuxDevice>;
~LinuxDevice();
static Ptr create() { return Ptr(new LinuxDevice); } static Ptr create() { return Ptr(new LinuxDevice); }
ProjectExplorer::IDeviceWidget *createWidget() override; ProjectExplorer::IDeviceWidget *createWidget() override;
@@ -55,8 +57,39 @@ public:
ProjectExplorer::DeviceProcessSignalOperation::Ptr signalOperation() const override; ProjectExplorer::DeviceProcessSignalOperation::Ptr signalOperation() const override;
ProjectExplorer::DeviceEnvironmentFetcher::Ptr environmentFetcher() const override; ProjectExplorer::DeviceEnvironmentFetcher::Ptr environmentFetcher() const override;
QString userAtHost() const;
bool handlesFile(const Utils::FilePath &filePath) const override;
bool isExecutableFile(const Utils::FilePath &filePath) const override;
bool isReadableFile(const Utils::FilePath &filePath) const override;
bool isWritableFile(const Utils::FilePath &filePath) const override;
bool isReadableDirectory(const Utils::FilePath &filePath) const override;
bool isWritableDirectory(const Utils::FilePath &filePath) const override;
bool isFile(const Utils::FilePath &filePath) const override;
bool isDirectory(const Utils::FilePath &filePath) const override;
bool createDirectory(const Utils::FilePath &filePath) const override;
bool exists(const Utils::FilePath &filePath) const override;
bool ensureExistingFile(const Utils::FilePath &filePath) const override;
bool removeFile(const Utils::FilePath &filePath) const override;
bool removeRecursively(const Utils::FilePath &filePath) const override;
bool copyFile(const Utils::FilePath &filePath, const Utils::FilePath &target) const override;
bool renameFile(const Utils::FilePath &filePath, const Utils::FilePath &target) const override;
Utils::FilePath symLinkTarget(const Utils::FilePath &filePath) const override;
void iterateDirectory(const Utils::FilePath &filePath,
const std::function<bool(const Utils::FilePath &)> &callBack,
const QStringList &nameFilters,
QDir::Filters filters) const override;
QByteArray fileContents(const Utils::FilePath &filePath, qint64 limit, qint64 offset) const override;
bool writeFileContents(const Utils::FilePath &filePath, const QByteArray &data) const override;
QDateTime lastModified(const Utils::FilePath &filePath) const override;
void runProcess(Utils::QtcProcess &process) const override;
qint64 fileSize(const Utils::FilePath &filePath) const override;
QFileDevice::Permissions permissions(const Utils::FilePath &filePath) const override;
bool setPermissions(const Utils::FilePath &filePath, QFileDevice::Permissions permissions) const override;
protected: protected:
LinuxDevice(); LinuxDevice();
class LinuxDevicePrivate *d;
}; };
namespace Internal { namespace Internal {

View File

@@ -43,6 +43,10 @@
#include "tarpackagecreationstep.h" #include "tarpackagecreationstep.h"
#include "uploadandinstalltarpackagestep.h" #include "uploadandinstalltarpackagestep.h"
#ifdef WITH_TESTS
#include "filesystemaccess_test.h"
#endif
#include <projectexplorer/kitinformation.h> #include <projectexplorer/kitinformation.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
@@ -121,6 +125,15 @@ RemoteLinuxPlugin::~RemoteLinuxPlugin()
delete dd; delete dd;
} }
QVector<QObject *> RemoteLinuxPlugin::createTestObjects() const
{
return {
#ifdef WITH_TESTS
new FileSystemAccessTest,
#endif
};
}
bool RemoteLinuxPlugin::initialize(const QStringList &arguments, QString *errorMessage) bool RemoteLinuxPlugin::initialize(const QStringList &arguments, QString *errorMessage)
{ {
Q_UNUSED(arguments) Q_UNUSED(arguments)

View File

@@ -39,6 +39,8 @@ public:
RemoteLinuxPlugin(); RemoteLinuxPlugin();
~RemoteLinuxPlugin() final; ~RemoteLinuxPlugin() final;
QVector<QObject *> createTestObjects() const override;
private: private:
bool initialize(const QStringList &arguments, QString *errorMessage) final; bool initialize(const QStringList &arguments, QString *errorMessage) final;
}; };