// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "terminalinterface.h" #include "utilstr.h" #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(terminalInterfaceLog, "qtc.terminalinterface", QtWarningMsg) namespace Utils { static QString msgCommChannelFailed(const QString &error) { return Tr::tr("Cannot set up communication channel: %1").arg(error); } static QString msgCannotCreateTempFile(const QString &why) { return Tr::tr("Cannot create temporary file: %1").arg(why); } static QString msgCannotWriteTempFile() { return Tr::tr("Cannot write temporary file. Disk full?"); } static QString msgCannotCreateTempDir(const QString &dir, const QString &why) { return Tr::tr("Cannot create temporary directory \"%1\": %2").arg(dir, why); } static QString msgUnexpectedOutput(const QByteArray &what) { return Tr::tr("Unexpected output from helper program (%1).").arg(QString::fromLatin1(what)); } static QString msgCannotChangeToWorkDir(const FilePath &dir, const QString &why) { return Tr::tr("Cannot change to working directory \"%1\": %2").arg(dir.toUserOutput(), why); } static QString msgCannotExecute(const FilePath &p, const QString &why) { return Tr::tr("Cannot execute \"%1\": %2").arg(p.toUserOutput(), why); } static QString msgPromptToClose() { // Shown in a terminal which might have a different character set on Windows. return Tr::tr("Press to close this window..."); } class TerminalInterfacePrivate : public QObject { Q_OBJECT public: TerminalInterfacePrivate(TerminalInterface *p, bool waitOnExit) : q(p) , waitOnExit(waitOnExit) { connect(&stubServer, &QLocalServer::newConnection, q, &TerminalInterface::onNewStubConnection); } public: QLocalServer stubServer; QLocalSocket *stubSocket = nullptr; int stubProcessId = 0; int inferiorProcessId = 0; int inferiorThreadId = 0; std::unique_ptr envListFile; QTemporaryDir tempDir; std::unique_ptr stubConnectTimeoutTimer; ProcessResultData processResultData; TerminalInterface *q; StubCreator *stubCreator{nullptr}; const bool waitOnExit; bool didInferiorRun{false}; }; TerminalInterface::TerminalInterface(bool waitOnExit) : d(new TerminalInterfacePrivate(this, waitOnExit)) {} TerminalInterface::~TerminalInterface() { if (d->stubSocket && d->stubSocket->state() == QLocalSocket::ConnectedState) { if (d->inferiorProcessId) killInferiorProcess(); killStubProcess(); } if (d->stubCreator) d->stubCreator->deleteLater(); delete d; } void TerminalInterface::setStubCreator(StubCreator *creator) { d->stubCreator = creator; } int TerminalInterface::inferiorProcessId() const { return d->inferiorProcessId; } int TerminalInterface::inferiorThreadId() const { return d->inferiorThreadId; } static QString errnoToString(int code) { return QString::fromLocal8Bit(strerror(code)); } void TerminalInterface::onNewStubConnection() { d->stubConnectTimeoutTimer.reset(); d->stubSocket = d->stubServer.nextPendingConnection(); if (!d->stubSocket) return; connect(d->stubSocket, &QIODevice::readyRead, this, &TerminalInterface::onStubReadyRead); if (HostOsInfo::isAnyUnixHost()) connect(d->stubSocket, &QLocalSocket::disconnected, this, &TerminalInterface::onStubExited); } void TerminalInterface::onStubExited() { // The stub exit might get noticed before we read the pid for the kill on Windows // or the error status elsewhere. if (d->stubSocket && d->stubSocket->state() == QLocalSocket::ConnectedState) d->stubSocket->waitForDisconnected(); shutdownStubServer(); d->envListFile.reset(); if (d->inferiorProcessId) emitFinished(-1, QProcess::CrashExit); else if (!d->didInferiorRun) { emitError(QProcess::FailedToStart, Tr::tr("Failed to start terminal process. The stub exited before the inferior " "was started.")); } } void TerminalInterface::onStubReadyRead() { while (d->stubSocket && d->stubSocket->canReadLine()) { QByteArray out = d->stubSocket->readLine(); out.chop(1); // remove newline if (out.startsWith("err:chdir ")) { emitError(QProcess::FailedToStart, msgCannotChangeToWorkDir(m_setup.m_workingDirectory, errnoToString(out.mid(10).toInt()))); } else if (out.startsWith("err:exec ")) { emitError(QProcess::FailedToStart, msgCannotExecute(m_setup.m_commandLine.executable(), errnoToString(out.mid(9).toInt()))); } else if (out.startsWith("spid ")) { d->envListFile.reset(); d->envListFile = nullptr; } else if (out.startsWith("pid ")) { d->inferiorProcessId = out.mid(4).toInt(); d->didInferiorRun = true; emit started(d->inferiorProcessId, d->inferiorThreadId); } else if (out.startsWith("thread ")) { // Windows only d->inferiorThreadId = out.mid(7).toLongLong(); } else if (out.startsWith("exit ")) { emitFinished(out.mid(5).toInt(), QProcess::NormalExit); } else if (out.startsWith("crash ")) { emitFinished(out.mid(6).toInt(), QProcess::CrashExit); } else if (out.startsWith("ack ")) { qCDebug(terminalInterfaceLog) << "Received ack from stub: " << out; } else { emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); break; } } } Result<> TerminalInterface::startStubServer() { if (HostOsInfo::isWindowsHost()) { if (d->stubServer.listen(QString::fromLatin1("creator-%1-%2") .arg(QCoreApplication::applicationPid()) .arg(rand()))) return {}; return ResultError(d->stubServer.errorString()); } // We need to put the socket in a private directory, as some systems simply do not // check the file permissions of sockets. if (!QDir(d->tempDir.path()) .mkdir("socket")) { // QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner return ResultError(msgCannotCreateTempDir(d->tempDir.filePath("socket"), QString::fromLocal8Bit(strerror(errno)))); } if (!QFile::setPermissions(d->tempDir.filePath("socket"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) { return ResultError(Tr::tr("Cannot set permissions on temporary directory \"%1\": %2") .arg(d->tempDir.filePath("socket")) .arg(QString::fromLocal8Bit(strerror(errno)))); } const QString socketPath = d->tempDir.filePath("socket/stub-socket"); if (!d->stubServer.listen(socketPath)) { return ResultError( Tr::tr("Cannot create socket \"%1\": %2").arg(socketPath, d->stubServer.errorString())); } return {}; } void TerminalInterface::shutdownStubServer() { if (d->stubSocket) { // Read potentially remaining data onStubReadyRead(); // avoid getting queued readyRead signals d->stubSocket->disconnect(); // we might be called from the disconnected signal of stubSocket d->stubSocket->deleteLater(); } d->stubSocket = nullptr; if (d->stubServer.isListening()) d->stubServer.close(); } void TerminalInterface::emitError(QProcess::ProcessError error, const QString &errorString) { d->processResultData.m_error = error; d->processResultData.m_errorString = errorString; if (error == QProcess::FailedToStart) emit done(d->processResultData); } void TerminalInterface::emitFinished(int exitCode, QProcess::ExitStatus exitStatus) { d->inferiorProcessId = 0; d->inferiorThreadId = 0; d->processResultData.m_exitCode = exitCode; d->processResultData.m_exitStatus = exitStatus; emit done(d->processResultData); } bool TerminalInterface::isRunning() const { return d->stubSocket && d->stubSocket->isOpen(); } void TerminalInterface::cleanupAfterStartFailure(const QString &errorMessage) { shutdownStubServer(); emitError(QProcess::FailedToStart, errorMessage); d->envListFile.reset(); } void TerminalInterface::sendCommand(char c) { if (d->stubSocket && d->stubSocket->isWritable()) { d->stubSocket->write(&c, 1); d->stubSocket->flush(); } } void TerminalInterface::killInferiorProcess() { sendCommand('k'); if (d->stubSocket) { d->stubSocket->waitForReadyRead(); emitFinished(-1, QProcess::CrashExit); } } void TerminalInterface::killStubProcess() { if (!isRunning()) return; sendCommand('s'); if (d->stubSocket) d->stubSocket->waitForReadyRead(); shutdownStubServer(); } void TerminalInterface::start() { if (isRunning()) return; if (m_setup.m_terminalMode == TerminalMode::Detached) { Result result; QMetaObject::invokeMethod( d->stubCreator, [this, &result] { result = d->stubCreator->startStubProcess(m_setup); }, d->stubCreator->thread() == QThread::currentThread() ? Qt::DirectConnection : Qt::BlockingQueuedConnection); if (result) { emit started(*result, 0); emitFinished(0, QProcess::NormalExit); } else { emitError(QProcess::FailedToStart, result.error()); } return; } const Result<> result = startStubServer(); if (!result) { emitError(QProcess::FailedToStart, msgCommChannelFailed(result.error())); return; } Environment finalEnv = m_setup.m_environment; if (HostOsInfo::isMacHost()) finalEnv.set("TERM", "xterm-256color"); if (finalEnv.hasChanges()) { finalEnv = finalEnv.appliedToEnvironment(Environment::systemEnvironment()); d->envListFile = std::make_unique(this); if (!d->envListFile->open()) { cleanupAfterStartFailure(msgCannotCreateTempFile(d->envListFile->errorString())); return; } QTextStream stream(d->envListFile.get()); finalEnv.forEachEntry([&stream](const QString &key, const QString &value, bool enabled) { if (enabled) stream << key << '=' << value << '\0'; }); if (d->envListFile->error() != QFile::NoError) { cleanupAfterStartFailure(msgCannotWriteTempFile()); return; } } const FilePath stubPath = FilePath::fromUserInput(QCoreApplication::applicationDirPath()) .pathAppended(QLatin1String(RELATIVE_LIBEXEC_PATH)) .pathAppended((HostOsInfo::isWindowsHost() ? QLatin1String("qtcreator_process_stub.exe") : QLatin1String("qtcreator_process_stub"))); CommandLine cmd{stubPath, {"-s", d->stubServer.fullServerName()}}; if (!m_setup.m_workingDirectory.isEmpty()) cmd.addArgs({"-w", m_setup.m_workingDirectory.nativePath()}); if (m_setup.m_terminalMode == TerminalMode::Debug) cmd.addArg("-d"); if (terminalInterfaceLog().isDebugEnabled()) cmd.addArg("-v"); if (d->envListFile) cmd.addArgs({"-e", d->envListFile->fileName()}); cmd.addArgs({"--wait", d->waitOnExit ? msgPromptToClose() : ""}); cmd.addArgs({"--", m_setup.m_commandLine.executable().nativePath()}); cmd.addArgs(m_setup.m_commandLine.arguments(), CommandLine::Raw); QTC_ASSERT(d->stubCreator, return); ProcessSetupData stubSetupData; stubSetupData.m_commandLine = cmd; stubSetupData.m_environment = Environment::originalSystemEnvironment(); stubSetupData.m_extraData[TERMINAL_SHELL_NAME] = m_setup.m_extraData.value(TERMINAL_SHELL_NAME, m_setup.m_commandLine.executable().fileName()); if (m_setup.m_runAsRoot && !HostOsInfo::isWindowsHost()) { CommandLine rootCommand("sudo"); rootCommand.addCommandLineAsArgs(cmd); stubSetupData.m_commandLine = rootCommand; } else { stubSetupData.m_commandLine = cmd; } QMetaObject::invokeMethod( d->stubCreator, [stubSetupData, this] { d->stubCreator->startStubProcess(stubSetupData); }, d->stubCreator->thread() == QThread::currentThread() ? Qt::DirectConnection : Qt::BlockingQueuedConnection); d->stubConnectTimeoutTimer = std::make_unique(); connect(d->stubConnectTimeoutTimer.get(), &QTimer::timeout, this, [this] { killInferiorProcess(); killStubProcess(); emitFinished(-1, QProcess::ExitStatus::CrashExit); }); d->stubConnectTimeoutTimer->setSingleShot(true); d->stubConnectTimeoutTimer->start(10000); } qint64 TerminalInterface::write(const QByteArray &data) { Q_UNUSED(data) QTC_CHECK(false); return -1; } void TerminalInterface::sendControlSignal(ControlSignal controlSignal) { QTC_ASSERT(m_setup.m_terminalMode != TerminalMode::Detached, return); switch (controlSignal) { case ControlSignal::Terminate: case ControlSignal::Kill: killInferiorProcess(); break; case ControlSignal::Interrupt: sendCommand('i'); break; case ControlSignal::KickOff: sendCommand('c'); break; case ControlSignal::CloseWriteChannel: QTC_CHECK(false); break; } } } // namespace Utils #include "terminalinterface.moc"