File indexing completed on 2024-05-05 04:39:44

0001 /*
0002     SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "dockerruntime.h"
0008 #include "dockerpreferencessettings.h"
0009 #include "debug_docker.h"
0010 
0011 #include <interfaces/icore.h>
0012 #include <interfaces/iprojectcontroller.h>
0013 #include <interfaces/iproject.h>
0014 #include <project/projectmodel.h>
0015 #include <project/interfaces/ibuildsystemmanager.h>
0016 
0017 #include <QJsonArray>
0018 #include <QJsonObject>
0019 #include <QJsonDocument>
0020 
0021 #include <KLocalizedString>
0022 #include <KProcess>
0023 #include <KActionCollection>
0024 #include <KShell>
0025 #include <KUser>
0026 #include <QProcess>
0027 #include <QDir>
0028 #include <outputview/outputexecutejob.h>
0029 
0030 using namespace KDevelop;
0031 
0032 DockerPreferencesSettings* DockerRuntime::s_settings = nullptr;
0033 
0034 DockerRuntime::DockerRuntime(const QString &tag)
0035     : KDevelop::IRuntime()
0036     , m_tag(tag)
0037 {
0038     setObjectName(tag);
0039 }
0040 
0041 void DockerRuntime::inspectContainer()
0042 {
0043     auto* process = new QProcess(this);
0044     connect(process, QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished),
0045             this, [process, this](int code, QProcess::ExitStatus status){
0046         process->deleteLater();
0047         qCDebug(DOCKER) << "inspect container" << code << status;
0048         if (code || status) {
0049             qCWarning(DOCKER) << "Could not figure out the container" << m_container;
0050             return;
0051         }
0052         const QJsonArray arr = QJsonDocument::fromJson(process->readAll()).array();
0053         const QJsonObject obj = arr.constBegin()->toObject();
0054 
0055         const QJsonObject objGraphDriverData = obj.value(QLatin1String("GraphDriver")).toObject().value(QLatin1String("Data")).toObject();
0056         m_mergedDir = Path(objGraphDriverData.value(QLatin1String("MergedDir")).toString());
0057         qCDebug(DOCKER) << "mergeddir:" << m_container << m_mergedDir;
0058 
0059         const auto& entries = obj[QLatin1String("Config")].toObject()[QLatin1String("Env")].toArray();
0060         for (const auto& entry : entries) {
0061             const auto content = entry.toString().split(QLatin1Char('='));
0062             if (content.count() != 2)
0063                 continue;
0064             m_envs.insert(content[0].toLocal8Bit(), content[1].toLocal8Bit());
0065         }
0066         qCDebug(DOCKER) << "envs:" << m_container << m_envs;
0067     });
0068     process->start(QStringLiteral("docker"), {QStringLiteral("container"), QStringLiteral("inspect"), m_container});
0069     process->waitForFinished();
0070     qDebug() << "inspecting" << QStringList{QStringLiteral("container"), QStringLiteral("inspect"), m_container} << process->exitCode();
0071 }
0072 
0073 DockerRuntime::~DockerRuntime()
0074 {
0075 }
0076 
0077 QByteArray DockerRuntime::getenv(const QByteArray& varname) const
0078 {
0079     return m_envs.value(varname);
0080 }
0081 
0082 void DockerRuntime::setEnabled(bool enable)
0083 {
0084     if (enable) {
0085         m_userMergedDir = KDevelop::Path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/docker-") + QString(m_tag).replace(QLatin1Char('/'), QLatin1Char('_')));
0086         QDir().mkpath(m_userMergedDir.toLocalFile());
0087 
0088         QProcess pCreateContainer;
0089         pCreateContainer.start(QStringLiteral("docker"), {QStringLiteral("run"), QStringLiteral("-d"), m_tag, QStringLiteral("tail"), QStringLiteral("-f"), QStringLiteral("/dev/null")});
0090         pCreateContainer.waitForFinished();
0091         if (pCreateContainer.exitCode()) {
0092             qCWarning(DOCKER) << "could not create the container" << pCreateContainer.readAll();
0093         }
0094         m_container = QString::fromUtf8(pCreateContainer.readAll().trimmed());
0095 
0096         inspectContainer();
0097 
0098         const QStringList cmd = {QStringLiteral("pkexec"), QStringLiteral("bindfs"), QLatin1String("--map=root/")+KUser().loginName(), m_mergedDir.toLocalFile(), m_userMergedDir.toLocalFile() };
0099         QProcess p;
0100         p.start(cmd.first(), cmd.mid(1));
0101         p.waitForFinished();
0102         if (p.exitCode()) {
0103             qCDebug(DOCKER) << "bindfs returned" << cmd << p.exitCode() << p.readAll();
0104         }
0105     } else {
0106         int codeContainer = QProcess::execute(QStringLiteral("docker"), {QStringLiteral("kill"), m_container});
0107         qCDebug(DOCKER) << "docker kill returned" << codeContainer;
0108 
0109         int code = QProcess::execute(QStringLiteral("pkexec"), {QStringLiteral("umount"), m_userMergedDir.toLocalFile()});
0110         qCDebug(DOCKER) << "umount returned" << code;
0111 
0112         m_container.clear();
0113     }
0114 }
0115 
0116 static QString ensureEndsSlash(const QString &string)
0117 {
0118     return string.endsWith(QLatin1Char('/')) ? string : (string + QLatin1Char('/'));
0119 }
0120 
0121 static QStringList projectVolumes()
0122 {
0123     QStringList ret;
0124     const QString dir = ensureEndsSlash(DockerRuntime::s_settings->projectsVolume());
0125     const QString buildDir = ensureEndsSlash(DockerRuntime::s_settings->buildDirsVolume());
0126 
0127     const auto& projects = ICore::self()->projectController()->projects();
0128     for (IProject* project : projects) {
0129         const Path path = project->path();
0130         if (path.isLocalFile()) {
0131             ret << QStringLiteral("--volume") << QStringLiteral("%1:%2").arg(path.toLocalFile(), dir + project->name());
0132         }
0133 
0134         const auto ibsm = project->buildSystemManager();
0135         if (ibsm) {
0136             ret << QStringLiteral("--volume") << ibsm->buildDirectory(project->projectItem()).toLocalFile() + QLatin1Char(':') +  buildDir + project->name();
0137         }
0138     }
0139     return ret;
0140 }
0141 
0142 QStringList DockerRuntime::workingDirArgs(QProcess* process) const
0143 {
0144     const auto wd = process->workingDirectory();
0145     return wd.isEmpty() ? QStringList{} : QStringList{QStringLiteral("-w"), pathInRuntime(KDevelop::Path(wd)).toLocalFile()};
0146 }
0147 
0148 void DockerRuntime::startProcess(QProcess* process) const
0149 {
0150     auto program = process->program();
0151     if (program.contains(QLatin1Char('/')))
0152         program = pathInRuntime(Path(program)).toLocalFile();
0153 
0154     const QStringList args = QStringList{QStringLiteral("run"), QStringLiteral("--rm")} << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << program << process->arguments();
0155     process->setProgram(QStringLiteral("docker"));
0156     process->setArguments(args);
0157 
0158     qCDebug(DOCKER) << "starting qprocess" << process->program() << process->arguments();
0159     process->start();
0160 }
0161 
0162 void DockerRuntime::startProcess(KProcess* process) const
0163 {
0164     auto program = process->program();
0165     if (program[0].contains(QLatin1Char('/')))
0166         program[0] = pathInRuntime(Path(program[0])).toLocalFile();
0167     process->setProgram(QStringList{QStringLiteral("docker"), QStringLiteral("run"), QStringLiteral("--rm")} << workingDirArgs(process) << KShell::splitArgs(s_settings->extraArguments()) << projectVolumes() << m_tag << program);
0168 
0169     qCDebug(DOCKER) << "starting kprocess" << process->program().join(QLatin1Char(' '));
0170     process->start();
0171 }
0172 
0173 static Path projectRelPath(const KDevelop::Path & projectsDir, const KDevelop::Path& runtimePath, bool sourceDir)
0174 {
0175     const auto relPath = projectsDir.relativePath(runtimePath);
0176     const int index = relPath.indexOf(QLatin1Char('/'));
0177     auto project = ICore::self()->projectController()->findProjectByName(relPath.left(index));
0178 
0179     if (!project) {
0180         qCWarning(DOCKER) << "No project for" << relPath;
0181     } else {
0182         const auto repPathProject = index < 0 ? QString() : relPath.mid(index+1);
0183         const auto rootPath = sourceDir ? project->path() : project->buildSystemManager()->buildDirectory(project->projectItem());
0184         return Path(rootPath, repPathProject);
0185     }
0186     return {};
0187 }
0188 
0189 KDevelop::Path DockerRuntime::pathInHost(const KDevelop::Path& runtimePath) const
0190 {
0191     Path ret;
0192     const Path projectsDir(DockerRuntime::s_settings->projectsVolume());
0193     if (runtimePath==projectsDir || projectsDir.isParentOf(runtimePath)) {
0194         ret = projectRelPath(projectsDir, runtimePath, true);
0195     } else {
0196         const Path buildDirs(DockerRuntime::s_settings->buildDirsVolume());
0197         if (runtimePath==buildDirs || buildDirs.isParentOf(runtimePath)) {
0198             ret = projectRelPath(buildDirs, runtimePath, false);
0199         } else
0200             ret = KDevelop::Path(m_userMergedDir, KDevelop::Path(QStringLiteral("/")).relativePath(runtimePath));
0201     }
0202     qCDebug(DOCKER) << "pathInHost" << ret << runtimePath;
0203     return ret;
0204 }
0205 
0206 KDevelop::Path DockerRuntime::pathInRuntime(const KDevelop::Path& localPath) const
0207 {
0208     if (m_userMergedDir==localPath || m_userMergedDir.isParentOf(localPath)) {
0209         KDevelop::Path ret(KDevelop::Path(QStringLiteral("/")), m_userMergedDir.relativePath(localPath));
0210         qCDebug(DOCKER) << "docker runtime pathInRuntime..." << ret << localPath;
0211         return ret;
0212     } else if (auto project = ICore::self()->projectController()->findProjectForUrl(localPath.toUrl())) {
0213         const Path projectsDir(DockerRuntime::s_settings->projectsVolume());
0214         const QString relpath = project->path().relativePath(localPath);
0215         const KDevelop::Path ret(projectsDir, project->name() + QLatin1Char('/') + relpath);
0216         qCDebug(DOCKER) << "docker user pathInRuntime..." << ret << localPath;
0217         return ret;
0218     } else {
0219         const auto projects = ICore::self()->projectController()->projects();
0220         for (auto project : projects) {
0221             auto ibsm = project->buildSystemManager();
0222             if (ibsm) {
0223                 const auto builddir = ibsm->buildDirectory(project->projectItem());
0224                 if (builddir != localPath && !builddir.isParentOf(localPath))
0225                     continue;
0226 
0227                 const Path builddirs(DockerRuntime::s_settings->buildDirsVolume());
0228                 const QString relpath = builddir.relativePath(localPath);
0229                 const KDevelop::Path ret(builddirs, project->name() + QLatin1Char('/') + relpath);
0230                 qCDebug(DOCKER) << "docker build pathInRuntime..." << ret << localPath;
0231                 return ret;
0232             }
0233         }
0234         qCWarning(DOCKER) << "only project files are accessible on the docker runtime" << localPath;
0235     }
0236     qCDebug(DOCKER) << "bypass..." << localPath;
0237     return localPath;
0238 }
0239 
0240 QString DockerRuntime::findExecutable(const QString& executableName) const
0241 {
0242     QStringList rtPaths;
0243 
0244     auto envPaths = getenv(QByteArrayLiteral("PATH")).split(':');
0245     std::transform(envPaths.begin(), envPaths.end(), std::back_inserter(rtPaths),
0246                     [this](QByteArray p) {
0247                         return pathInHost(Path(QString::fromLocal8Bit(p))).toLocalFile();
0248                     });
0249 
0250     return QStandardPaths::findExecutable(executableName, rtPaths);
0251 }
0252 
0253 #include "moc_dockerruntime.cpp"