LinuxDevice: Add disconnected state

Change-Id: Ic2c909e3375a9fb025a335c1ca65621fa031d000
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
Marcus Tillmanns
2024-02-09 14:34:48 +01:00
parent 39527bbcad
commit e8ad29d3af
8 changed files with 278 additions and 23 deletions

View File

@@ -878,52 +878,73 @@ QByteArray DesktopDeviceFileAccess::fileId(const FilePath &filePath) const
// UnixDeviceAccess
static Utils::unexpected<QString> make_unexpected_disconnected()
{
return make_unexpected(Tr::tr("Device is not connected"));
}
UnixDeviceFileAccess::~UnixDeviceFileAccess() = default;
bool UnixDeviceFileAccess::runInShellSuccess(const CommandLine &cmdLine,
const QByteArray &stdInData) const
{
if (disconnected())
return false;
return runInShell(cmdLine, stdInData).exitCode == 0;
}
bool UnixDeviceFileAccess::isExecutableFile(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-x", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isReadableFile(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-r", path, "-a", "-f", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isWritableFile(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-w", path, "-a", "-f", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isReadableDirectory(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-r", path, "-a", "-d", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isWritableDirectory(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-w", path, "-a", "-d", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isFile(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-f", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::isDirectory(const FilePath &filePath) const
{
if (disconnected())
return false;
if (filePath.isRootPath())
return true;
@@ -933,12 +954,16 @@ bool UnixDeviceFileAccess::isDirectory(const FilePath &filePath) const
bool UnixDeviceFileAccess::isSymLink(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-h", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::hasHardLinks(const FilePath &filePath) const
{
if (disconnected())
return false;
const QStringList args = statArgs(filePath, "%h", "%l");
const RunResult result = runInShell({"stat", args, OsType::OsTypeLinux});
return result.stdOut.toLongLong() > 1;
@@ -946,29 +971,39 @@ bool UnixDeviceFileAccess::hasHardLinks(const FilePath &filePath) const
bool UnixDeviceFileAccess::ensureExistingFile(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"touch", {path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::createDirectory(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"mkdir", {"-p", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::exists(const FilePath &filePath) const
{
if (disconnected())
return false;
const QString path = filePath.path();
return runInShellSuccess({"test", {"-e", path}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::removeFile(const FilePath &filePath) const
{
if (disconnected())
return false;
return runInShellSuccess({"rm", {filePath.path()}, OsType::OsTypeLinux});
}
bool UnixDeviceFileAccess::removeRecursively(const FilePath &filePath, QString *error) const
{
if (disconnected())
return false;
QTC_ASSERT(filePath.path().startsWith('/'), return false);
const QString path = filePath.cleanPath().path();
@@ -989,6 +1024,8 @@ bool UnixDeviceFileAccess::removeRecursively(const FilePath &filePath, QString *
expected_str<void> UnixDeviceFileAccess::copyFile(const FilePath &filePath,
const FilePath &target) const
{
if (disconnected())
return make_unexpected_disconnected();
const RunResult result = runInShell(
{"cp", {filePath.path(), target.path()}, OsType::OsTypeLinux});
@@ -1003,11 +1040,17 @@ expected_str<void> UnixDeviceFileAccess::copyFile(const FilePath &filePath,
bool UnixDeviceFileAccess::renameFile(const FilePath &filePath, const FilePath &target) const
{
if (disconnected())
return false;
return runInShellSuccess({"mv", {filePath.path(), target.path()}, OsType::OsTypeLinux});
}
FilePath UnixDeviceFileAccess::symLinkTarget(const FilePath &filePath) const
{
if (disconnected())
return {};
const RunResult result = runInShell(
{"readlink", {"-n", "-e", filePath.path()}, OsType::OsTypeLinux});
const QString out = QString::fromUtf8(result.stdOut);
@@ -1018,6 +1061,9 @@ expected_str<QByteArray> UnixDeviceFileAccess::fileContents(const FilePath &file
qint64 limit,
qint64 offset) const
{
if (disconnected())
return make_unexpected_disconnected();
QStringList args = {"if=" + filePath.path()};
if (limit > 0 || offset > 0) {
const qint64 gcd = std::gcd(limit, offset);
@@ -1045,6 +1091,9 @@ expected_str<qint64> UnixDeviceFileAccess::writeFileContents(const FilePath &fil
const QByteArray &data,
qint64 offset) const
{
if (disconnected())
return make_unexpected_disconnected();
QStringList args = {"of=" + filePath.path()};
if (offset != 0) {
args.append("bs=1");
@@ -1061,6 +1110,9 @@ expected_str<qint64> UnixDeviceFileAccess::writeFileContents(const FilePath &fil
expected_str<FilePath> UnixDeviceFileAccess::createTempFile(const FilePath &filePath)
{
if (disconnected())
return make_unexpected_disconnected();
if (!m_hasMkTemp.has_value())
m_hasMkTemp = runInShellSuccess({"which", {"mktemp"}, OsType::OsTypeLinux});
@@ -1120,6 +1172,9 @@ expected_str<FilePath> UnixDeviceFileAccess::createTempFile(const FilePath &file
QDateTime UnixDeviceFileAccess::lastModified(const FilePath &filePath) const
{
if (disconnected())
return {};
const RunResult result = runInShell(
{"stat", {"-L", "-c", "%Y", filePath.path()}, OsType::OsTypeLinux});
qint64 secs = result.stdOut.toLongLong();
@@ -1131,6 +1186,9 @@ QStringList UnixDeviceFileAccess::statArgs(const FilePath &filePath,
const QString &linuxFormat,
const QString &macFormat) const
{
if (disconnected())
return {};
return (filePath.osType() == OsTypeMac ? QStringList{"-f", macFormat}
: QStringList{"-c", linuxFormat})
<< "-L" << filePath.path();
@@ -1138,6 +1196,9 @@ QStringList UnixDeviceFileAccess::statArgs(const FilePath &filePath,
QFile::Permissions UnixDeviceFileAccess::permissions(const FilePath &filePath) const
{
if (disconnected())
return {};
QStringList args = statArgs(filePath, "%a", "%p");
const RunResult result = runInShell({"stat", args, OsType::OsTypeLinux});
@@ -1161,6 +1222,9 @@ QFile::Permissions UnixDeviceFileAccess::permissions(const FilePath &filePath) c
bool UnixDeviceFileAccess::setPermissions(const FilePath &filePath, QFile::Permissions perms) const
{
if (disconnected())
return false;
const int flags = int(perms);
return runInShellSuccess(
{"chmod", {QString::number(flags, 16), filePath.path()}, OsType::OsTypeLinux});
@@ -1168,6 +1232,9 @@ bool UnixDeviceFileAccess::setPermissions(const FilePath &filePath, QFile::Permi
qint64 UnixDeviceFileAccess::fileSize(const FilePath &filePath) const
{
if (disconnected())
return -1;
const QStringList args = statArgs(filePath, "%s", "%z");
const RunResult result = runInShell({"stat", args, OsType::OsTypeLinux});
return result.stdOut.toLongLong();
@@ -1175,12 +1242,18 @@ qint64 UnixDeviceFileAccess::fileSize(const FilePath &filePath) const
qint64 UnixDeviceFileAccess::bytesAvailable(const FilePath &filePath) const
{
if (disconnected())
return -1;
const RunResult result = runInShell({"df", {"-k", filePath.path()}, OsType::OsTypeLinux});
return FileUtils::bytesAvailableFromDFOutput(result.stdOut);
}
QByteArray UnixDeviceFileAccess::fileId(const FilePath &filePath) const
{
if (disconnected())
return {};
const QStringList args = statArgs(filePath, "%D:%i", "%d:%i");
const RunResult result = runInShell({"stat", args, OsType::OsTypeLinux});
@@ -1192,6 +1265,9 @@ QByteArray UnixDeviceFileAccess::fileId(const FilePath &filePath) const
FilePathInfo UnixDeviceFileAccess::filePathInfo(const FilePath &filePath) const
{
if (disconnected())
return {};
if (filePath.path() == "/") // TODO: Add FilePath::isRoot()
{
const FilePathInfo r{4096,
@@ -1218,6 +1294,9 @@ bool UnixDeviceFileAccess::iterateWithFind(const FilePath &filePath,
const FileFilter &filter,
const FilePath::IterateDirCallback &callBack) const
{
if (disconnected())
return false;
QTC_CHECK(filePath.isAbsolutePath());
CommandLine cmdLine{"find", filter.asFindArguments(filePath.path()), OsType::OsTypeLinux};
@@ -1290,6 +1369,9 @@ void UnixDeviceFileAccess::findUsingLs(const QString &current,
QStringList *found,
const QString &start) const
{
if (disconnected())
return;
const RunResult result = runInShell(
{"ls", {"-1", "-a", "-p", "--", current}, OsType::OsTypeLinux});
const QStringList entries = QString::fromUtf8(result.stdOut).split('\n', Qt::SkipEmptyParts);
@@ -1349,6 +1431,9 @@ void UnixDeviceFileAccess::iterateDirectory(const FilePath &filePath,
const FilePath::IterateDirCallback &callBack,
const FileFilter &filter) const
{
if (disconnected())
return;
// We try to use 'find' first, because that can filter better directly.
// Unfortunately, it's not installed on all devices by default.
if (m_tryUseFind) {
@@ -1366,9 +1451,17 @@ void UnixDeviceFileAccess::iterateDirectory(const FilePath &filePath,
Environment UnixDeviceFileAccess::deviceEnvironment() const
{
if (disconnected())
return {};
const RunResult result = runInShell({"env", {}, OsType::OsTypeLinux});
const QString out = QString::fromUtf8(result.stdOut);
return Environment(out.split('\n', Qt::SkipEmptyParts), OsTypeLinux);
}
bool UnixDeviceFileAccess::disconnected() const
{
return false;
}
} // namespace Utils

View File

@@ -188,6 +188,8 @@ protected:
QStringList *found,
const QString &start) const;
virtual bool disconnected() const;
private:
bool iterateWithFind(const FilePath &filePath,
const FileFilter &filter,

View File

@@ -312,6 +312,9 @@ void DeviceSettingsWidget::testDevice()
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->setModal(true);
dlg->show();
connect(dlg, &QObject::destroyed, this, [this, id = device->id()] {
handleDeviceUpdated(id);
});
}
void DeviceSettingsWidget::handleDeviceUpdated(Id id)

View File

@@ -593,7 +593,7 @@ QString IDevice::deviceStateToString() const
QPixmap IDevice::deviceStateIcon() const
{
switch (d->deviceState) {
switch (deviceState()) {
case IDevice::DeviceReadyToUse: return Icons::DEVICE_READY_INDICATOR.pixmap();
case IDevice::DeviceConnected: return Icons::DEVICE_CONNECTED_INDICATOR.pixmap();
case IDevice::DeviceDisconnected: return Icons::DEVICE_DISCONNECTED_INDICATOR.pixmap();

View File

@@ -105,7 +105,7 @@ public:
virtual ~IDevice();
Ptr clone() const;
virtual Ptr clone() const;
DeviceSettings *settings() const;
@@ -154,9 +154,9 @@ public:
virtual DeviceProcessSignalOperation::Ptr signalOperation() const;
enum DeviceState { DeviceReadyToUse, DeviceConnected, DeviceDisconnected, DeviceStateUnknown };
DeviceState deviceState() const;
virtual DeviceState deviceState() const;
void setDeviceState(const DeviceState state);
QString deviceStateToString() const;
virtual QString deviceStateToString() const;
QPixmap deviceStateIcon() const;
static Utils::Id typeFromMap(const Utils::Store &map);

View File

@@ -21,12 +21,15 @@
#include <projectexplorer/devicesupport/processlist.h>
#include <projectexplorer/devicesupport/sshparameters.h>
#include <projectexplorer/devicesupport/sshsettings.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/devicefileaccess.h>
#include <utils/deviceshell.h>
#include <utils/environment.h>
#include <utils/hostosinfo.h>
#include <utils/infobar.h>
#include <utils/port.h>
#include <utils/portlist.h>
#include <utils/process.h>
@@ -292,6 +295,8 @@ public:
Environment deviceEnvironment() const override;
bool disconnected() const override;
LinuxDevicePrivate *m_dev;
};
@@ -307,7 +312,7 @@ public:
explicit LinuxDevicePrivate(LinuxDevice *parent);
~LinuxDevicePrivate();
bool setupShell();
bool setupShell(const SshParameters &sshParameters);
RunResult runInShell(const CommandLine &cmd, const QByteArray &stdInData = {});
void attachToSharedConnection(SshConnectionHandle *connectionHandle,
@@ -319,6 +324,10 @@ public:
void checkOsType();
void queryOsType(std::function<RunResult(const CommandLine &)> run);
void setDisconnected(bool disconnected);
bool disconnected() const;
bool checkDisconnectedWithWarning();
LinuxDevice *q = nullptr;
QThread m_shellThread;
ShellThreadHandler *m_handler = nullptr;
@@ -327,6 +336,7 @@ public:
QReadWriteLock m_environmentCacheLock;
std::optional<Environment> m_environmentCache;
bool m_disconnected = false;
};
void LinuxDevicePrivate::invalidateEnvironmentCache()
@@ -358,14 +368,24 @@ Environment LinuxDevicePrivate::getEnvironment()
RunResult LinuxDeviceFileAccess::runInShell(const CommandLine &cmdLine,
const QByteArray &stdInData) const
{
if (disconnected())
return {-1, {}, Tr::tr("Device is disconnected.").toUtf8()};
return m_dev->runInShell(cmdLine, stdInData);
}
Environment LinuxDeviceFileAccess::deviceEnvironment() const
{
if (disconnected())
return {};
return m_dev->getEnvironment();
}
bool LinuxDeviceFileAccess::disconnected() const
{
return m_dev->checkDisconnectedWithWarning();
}
// SshProcessImpl
class SshProcessInterfacePrivate : public QObject
@@ -1023,6 +1043,15 @@ LinuxDevice::~LinuxDevice()
delete d;
}
IDevice::Ptr LinuxDevice::clone() const
{
IDevice::Ptr clone = IDevice::clone();
Ptr linuxClone = std::dynamic_pointer_cast<LinuxDevice>(clone);
QTC_ASSERT(linuxClone, return clone);
linuxClone->d->setDisconnected(d->disconnected());
return clone;
}
IDeviceWidget *LinuxDevice::createWidget()
{
return new Internal::GenericLinuxDeviceConfigurationWidget(shared_from_this());
@@ -1106,15 +1135,31 @@ void LinuxDevicePrivate::queryOsType(std::function<RunResult(const CommandLine &
q->_setOsType(OsTypeLinux);
}
void LinuxDevicePrivate::setDisconnected(bool disconnected)
{
if (disconnected == m_disconnected)
return;
m_disconnected = disconnected;
if (m_disconnected)
m_handler->closeShell();
}
bool LinuxDevicePrivate::disconnected() const
{
return m_disconnected;
}
void LinuxDevicePrivate::checkOsType()
{
queryOsType([this](const CommandLine &cmd) { return runInShell(cmd); });
}
// Call me with shell mutex locked
bool LinuxDevicePrivate::setupShell()
bool LinuxDevicePrivate::setupShell(const SshParameters &sshParameters)
{
const SshParameters sshParameters = q->sshParameters();
if (m_handler->isRunning(sshParameters))
return true;
@@ -1126,8 +1171,12 @@ bool LinuxDevicePrivate::setupShell()
}, Qt::BlockingQueuedConnection, &ok);
if (ok) {
setDisconnected(false);
queryOsType([this](const CommandLine &cmd) { return m_handler->runInShell(cmd); });
} else {
setDisconnected(true);
}
return ok;
}
@@ -1135,11 +1184,43 @@ RunResult LinuxDevicePrivate::runInShell(const CommandLine &cmd, const QByteArra
{
QMutexLocker locker(&m_shellMutex);
DEBUG(cmd.toUserOutput());
const bool isSetup = setupShell();
if (checkDisconnectedWithWarning())
return {};
const bool isSetup = setupShell(q->sshParameters());
if (checkDisconnectedWithWarning())
return {};
QTC_ASSERT(isSetup, return {});
return m_handler->runInShell(cmd, data);
}
bool LinuxDevicePrivate::checkDisconnectedWithWarning()
{
if (!disconnected())
return false;
QMetaObject::invokeMethod(Core::ICore::infoBar(), [id = q->id(), name = q->displayName()] {
if (!Core::ICore::infoBar()->canInfoBeAdded(id))
return;
const QString warnStr
= Tr::tr("Device \"%1\" is currently marked as disconnected.").arg(name);
InfoBarEntry info(id, warnStr, InfoBarEntry::GlobalSuppression::Enabled);
info.setDetailsWidgetCreator([] {
const auto label = new QLabel(Tr::tr(
"The device was not available when trying to connect previously.<br>"
"No further connection attempts will be made until the device is manually reset "
"by running a successful connection test via the "
"<a href=\"dummy\">settings page</a>."));
label->setWordWrap(true);
QObject::connect(label, &QLabel::linkActivated, [] {
Core::ICore::showOptionsDialog(ProjectExplorer::Constants::DEVICE_SETTINGS_PAGE_ID);
});
return label;
});
Core::ICore::infoBar()->addInfo(info);
});
return true;
}
void LinuxDevicePrivate::attachToSharedConnection(SshConnectionHandle *connectionHandle,
const SshParameters &sshParameters)
{
@@ -1522,6 +1603,37 @@ void LinuxDevice::checkOsType()
d->checkOsType();
}
IDevice::DeviceState LinuxDevice::deviceState() const
{
if (isDisconnected())
return DeviceDisconnected;
return IDevice::deviceState();
}
QString LinuxDevice::deviceStateToString() const
{
if (isDisconnected())
return Tr::tr("Device is considered unconnected. Re-run device test to reset state.");
return IDevice::deviceStateToString();
}
bool LinuxDevice::isDisconnected() const
{
return d->disconnected();
}
void LinuxDevice::setDisconnected(bool disconnected)
{
d->setDisconnected(disconnected);
}
QFuture<bool> LinuxDevice::tryToConnect()
{
return Utils::asyncRun([this] {
QMutexLocker locker(&d->m_shellMutex);
return d->setupShell(sshParameters());
});
}
namespace Internal {
class LinuxDeviceFactory final : public IDeviceFactory

View File

@@ -20,6 +20,8 @@ public:
static Ptr create() { return Ptr(new LinuxDevice); }
IDevice::Ptr clone() const override;
ProjectExplorer::IDeviceWidget *createWidget() override;
bool canCreateProcessModel() const override { return true; }
@@ -42,6 +44,14 @@ public:
class LinuxDevicePrivate *connectionAccess() const;
void checkOsType() override;
DeviceState deviceState() const override;
QString deviceStateToString() const override;
bool isDisconnected() const;
void setDisconnected(bool disconnected);
QFuture<bool> tryToConnect();
protected:
LinuxDevice();

View File

@@ -3,6 +3,7 @@
#include "linuxdevicetester.h"
#include "linuxdevice.h"
#include "remotelinuxtr.h"
#include <projectexplorer/devicesupport/deviceusedportsgatherer.h>
@@ -17,6 +18,8 @@
#include <utils/qtcassert.h>
#include <utils/stringutils.h>
#include <QFutureWatcher>
using namespace ProjectExplorer;
using namespace Tasking;
using namespace Utils;
@@ -43,9 +46,13 @@ public:
const Storage<TransferStorage> &storage) const;
GroupItem transferTasks() const;
GroupItem commandTasks() const;
void runCommandTests();
bool isRunning() const { return m_connectionTest || m_taskTreeRunner.isRunning(); }
GenericLinuxDeviceTester *q = nullptr;
IDevice::Ptr m_device;
LinuxDevice::Ptr m_device;
QFutureWatcher<bool> *m_connectionTest = nullptr;
TaskTreeRunner m_taskTreeRunner;
QStringList m_extraCommands;
QList<GroupItem> m_extraTests;
@@ -276,6 +283,20 @@ GroupItem GenericLinuxDeviceTesterPrivate::commandTasks() const
return root;
}
void GenericLinuxDeviceTesterPrivate::runCommandTests()
{
const Group root {
echoTask("Hello"), // No quoting necessary
echoTask("Hello Remote World!"), // Checks quoting, too.
unameTask(),
gathererTask(),
transferTasks(),
m_extraTests,
commandTasks()
};
m_taskTreeRunner.start(root);
}
} // namespace Internal
using namespace Internal;
@@ -302,26 +323,40 @@ void GenericLinuxDeviceTester::setExtraTests(const QList<GroupItem> &extraTests)
void GenericLinuxDeviceTester::testDevice(const IDevice::Ptr &deviceConfiguration)
{
QTC_ASSERT(!d->m_taskTreeRunner.isRunning(), return);
QTC_ASSERT(!d->isRunning(), return);
d->m_device = deviceConfiguration;
emit progressMessage(Tr::tr("Connecting to device..."));
const Group root {
d->echoTask("Hello"), // No quoting necessary
d->echoTask("Hello Remote World!"), // Checks quoting, too.
d->unameTask(),
d->gathererTask(),
d->transferTasks(),
d->m_extraTests,
d->commandTasks()
};
d->m_taskTreeRunner.start(root);
d->m_device = std::static_pointer_cast<LinuxDevice>(deviceConfiguration);
d->m_connectionTest = new QFutureWatcher<bool>(this);
d->m_connectionTest->setFuture(d->m_device->tryToConnect());
connect(d->m_connectionTest, &QFutureWatcher<bool>::finished, this, [this] {
const bool success = d->m_connectionTest->result();
d->m_connectionTest->deleteLater();
d->m_connectionTest = nullptr;
if (success) {
emit progressMessage(Tr::tr("Connected. Now doing extended checks.\n"));
d->runCommandTests();
} else {
emit errorMessage(
Tr::tr("Basic connectivity test failed, device is considered unusable."));
emit finished(TestFailure);
}
});
}
void GenericLinuxDeviceTester::stopTest()
{
QTC_ASSERT(d->m_taskTreeRunner.isRunning(), return);
QTC_ASSERT(d->isRunning(), return);
if (d->m_connectionTest) {
d->m_connectionTest->disconnect();
d->m_connectionTest->cancel();
d->m_connectionTest = nullptr;
} else {
d->m_taskTreeRunner.reset();
}
emit finished(TestFailure);
}