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"