/**************************************************************************** ** ** 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 "linuxdevice.h" #include "genericlinuxdeviceconfigurationwidget.h" #include "genericlinuxdeviceconfigurationwizard.h" #include "linuxdeviceprocess.h" #include "linuxdevicetester.h" #include "publickeydeploymentdialog.h" #include "remotelinux_constants.h" #include "remotelinuxsignaloperation.h" #include "remotelinuxenvironmentreader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace QSsh; using namespace Utils; namespace RemoteLinux { const char Delimiter0[] = "x--"; 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) { return s.replace(QLatin1Char('\0'), QLatin1String("")); } class LinuxDeviceProcessList : public SshDeviceProcessList { public: LinuxDeviceProcessList(const IDevice::ConstPtr &device, QObject *parent) : SshDeviceProcessList(device, parent) { } private: QString listProcessesCommandLine() const override { return QString::fromLatin1( "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;" "printf '%1''%2';" "done").arg(QLatin1String(Delimiter0)).arg(QLatin1String(Delimiter1)); } QList buildProcessList(const QString &listProcessesReply) const override { QList processes; const QStringList lines = listProcessesReply.split(QString::fromLatin1(Delimiter0) + QString::fromLatin1(Delimiter1), Qt::SkipEmptyParts); 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, int(elements.count()), qPrintable(visualizeNull(line))); continue; } bool ok; const int pid = elements.first().mid(6).toInt(&ok); 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(']'); } DeviceProcessItem process; process.pid = pid; process.cmdLine = command; process.exe = elements.at(3); processes.append(process); } Utils::sort(processes); return processes; } }; class LinuxPortsGatheringMethod : public PortsGatheringMethod { Runnable runnable(QAbstractSocket::NetworkLayerProtocol protocol) const override { // 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 Runnable runnable; runnable.command.setExecutable("sed"); runnable.command.setArguments("-e 's/.*: [[:xdigit:]]*:\\([[:xdigit:]]\\{4\\}\\).*/\\1/g' /proc/net/tcp*"); return runnable; } QList usedPorts(const QByteArray &output) const override { QList ports; QList portStrings = output.split('\n'); foreach (const QByteArray &portString, portStrings) { if (portString.size() != 4) continue; bool ok; const Utils::Port port(portString.toInt(&ok, 16)); 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; } }; // ShellThreadHandler class ShellThreadHandler : public QObject { public: ~ShellThreadHandler() { if (m_shell) delete m_shell; } bool startFailed(const SshConnectionParameters ¶meters) { delete m_shell; m_shell = nullptr; DEBUG("Failed to connect to " << parameters.host()); return false; } bool start(const SshConnectionParameters ¶meters) { // TODO: start here shared ssh connection if needed (take it from settings) // connect to it // wait for connected m_shell = new SshRemoteProcess("/bin/sh", parameters.connectionOptions(SshSettings::sshFilePath()) << parameters.host(), ProcessMode::Writer); m_shell->start(); const bool startOK = m_shell->waitForStarted(); if (!startOK) return startFailed(parameters); m_shell->write("echo\n"); const bool readOK = m_shell->waitForReadyRead(); if (!readOK) return startFailed(parameters); const QByteArray output = m_shell->readAllStandardOutput(); if (output != "\n") return startFailed(parameters); return true; } 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 QString &cmd) { QTC_ASSERT(m_shell, return {}); static int val = 0; const QByteArray delim = QString::number(++val, 16).toUtf8(); DEBUG("RUN2 " << cmd); m_shell->readAllStandardOutput(); // clean possible left-overs const QByteArray marker = "___QTC___" + delim + "_OUTPUT_MARKER___"; DEBUG(" CMD: " << cmd.toUtf8() + "\necho " + marker + "\n"); m_shell->write(cmd.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 << "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 QString &cmd); QString outputForRunInShell(const CommandLine &cmd); LinuxDevice *q = nullptr; QThread m_shellThread; ShellThreadHandler *m_handler = nullptr; mutable QMutex m_shellMutex; }; // LinuxDevice LinuxDevice::LinuxDevice() : d(new LinuxDevicePrivate(this)) { setDisplayType(tr("Generic Linux")); setDefaultDisplayName(tr("Generic Linux Device")); setOsType(OsTypeLinux); addDeviceAction({tr("Deploy Public Key..."), [](const IDevice::Ptr &device, QWidget *parent) { if (auto d = PublicKeyDeploymentDialog::createDialog(device, parent)) { d->exec(); delete d; } }}); setOpenTerminal([this](const Environment &env, const FilePath &workingDir) { DeviceProcess * const proc = createProcess(nullptr); QObject::connect(proc, &DeviceProcess::finished, [proc] { if (!proc->errorString().isEmpty()) { Core::MessageManager::writeDisrupting( tr("Error running remote shell: %1").arg(proc->errorString())); } proc->deleteLater(); }); QObject::connect(proc, &DeviceProcess::error, [proc] { Core::MessageManager::writeDisrupting(tr("Error starting remote shell.")); 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) runnable.command.setExecutable("/bin/sh"); proc->setRunInTerminal(true); proc->start(runnable); }); if (Utils::HostOsInfo::isAnyUnixHost()) { addDeviceAction({tr("Open Remote Shell"), [](const IDevice::Ptr &device, QWidget *) { device->openTerminal(Environment(), FilePath()); }}); } } LinuxDevice::~LinuxDevice() { delete d; } IDeviceWidget *LinuxDevice::createWidget() { return new GenericLinuxDeviceConfigurationWidget(sharedFromThis()); } DeviceProcess *LinuxDevice::createProcess(QObject *parent) const { return new LinuxDeviceProcess(sharedFromThis(), parent); } bool LinuxDevice::canAutoDetectPorts() const { return true; } PortsGatheringMethod::Ptr LinuxDevice::portsGatheringMethod() const { return LinuxPortsGatheringMethod::Ptr(new LinuxPortsGatheringMethod); } DeviceProcessList *LinuxDevice::createProcessListModel(QObject *parent) const { return new LinuxDeviceProcessList(sharedFromThis(), parent); } DeviceTester *LinuxDevice::createDeviceTester() const { return new GenericLinuxDeviceTester; } DeviceProcessSignalOperation::Ptr LinuxDevice::signalOperation() const { return DeviceProcessSignalOperation::Ptr(new RemoteLinuxSignalOperation(sshParameters())); } 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())); } QString LinuxDevice::userAtHost() const { if (sshParameters().userName().isEmpty()) return sshParameters().host(); return sshParameters().userName() + '@' + sshParameters().host(); } 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; } 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; } QString LinuxDevicePrivate::outputForRunInShell(const QString &cmd) { QMutexLocker locker(&m_shellMutex); DEBUG(cmd); 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; } QString LinuxDevicePrivate::outputForRunInShell(const CommandLine &cmd) { return outputForRunInShell(cmd.toUserOutput()); } 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/") ? 3 : 2; 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(); } 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; } 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<runInShell({"chmod", {QString::number(flags, 16), filePath.path()}}); } static void filterEntriesHelper(const FilePath &base, const std::function &callBack, const QStringList &entries, const FileFilter &filter) { const QList nameRegexps = transform(filter.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(filter.fileFilters == QDir::NoFilter); QTC_CHECK(filter.iteratorFlags == QDirIterator::NoIteratorFlags); for (const QString &entry : entries) { if (!nameMatches(entry)) continue; if (!callBack(base.pathAppended(entry))) break; } } void LinuxDevice::iterateDirectory(const FilePath &filePath, const std::function &callBack, const FileFilter &filter) 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, filter); } 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 { // Factory LinuxDeviceFactory::LinuxDeviceFactory() : IDeviceFactory(Constants::GenericLinuxOsType) { setDisplayName(LinuxDevice::tr("Generic Linux Device")); setIcon(QIcon()); setConstructionFunction(&LinuxDevice::create); setCanCreate(true); setCreator([] { GenericLinuxDeviceConfigurationWizard wizard(Core::ICore::dialogParent()); if (wizard.exec() != QDialog::Accepted) return IDevice::Ptr(); return wizard.device(); }); } } // namespace Internal } // namespace RemoteLinux