diff options
Diffstat (limited to 'src/libs/utils/terminalprocess.cpp')
-rw-r--r-- | src/libs/utils/terminalprocess.cpp | 721 |
1 files changed, 0 insertions, 721 deletions
diff --git a/src/libs/utils/terminalprocess.cpp b/src/libs/utils/terminalprocess.cpp deleted file mode 100644 index bd0333c9d7..0000000000 --- a/src/libs/utils/terminalprocess.cpp +++ /dev/null @@ -1,721 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "terminalprocess_p.h" - -#include "commandline.h" -#include "environment.h" -#include "hostosinfo.h" -#include "qtcassert.h" -#include "qtcprocess.h" -#include "terminalcommand.h" -#include "utilstr.h" - -#include <QCoreApplication> -#include <QLocalServer> -#include <QLocalSocket> -#include <QRegularExpression> -#include <QTemporaryFile> -#include <QTextCodec> -#include <QTimer> -#include <QWinEventNotifier> - -#ifdef Q_OS_WIN - -#include "winutils.h" - -#include <cstring> -#include <stdlib.h> -#include <windows.h> - -#else - -#include <sys/stat.h> -#include <sys/types.h> -#include <errno.h> -#include <string.h> -#include <unistd.h> - -#endif - -namespace Utils { -namespace Internal { - -static QString modeOption(TerminalMode m) -{ - switch (m) { - case TerminalMode::Run: - return QLatin1String("run"); - case TerminalMode::Debug: - return QLatin1String("debug"); - case TerminalMode::Suspend: - return QLatin1String("suspend"); - case TerminalMode::Off: - QTC_CHECK(false); - break; - } - return {}; -} - -static QString msgCommChannelFailed(const QString &error) -{ - return Tr::tr("Cannot set up communication channel: %1").arg(error); -} - -static QString msgPromptToClose() -{ - // Shown in a terminal which might have a different character set on Windows. - return Tr::tr("Press <RETURN> to close this window..."); -} - -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.toString(), why); -} - -static QString msgCannotExecute(const QString & p, const QString &why) -{ - return Tr::tr("Cannot execute \"%1\": %2").arg(p, why); -} - -class TerminalProcessPrivate -{ -public: - TerminalProcessPrivate(QObject *parent) - : m_stubServer(parent) - , m_process(parent) {} - - qint64 m_processId = 0; - ProcessResultData m_result; - QLocalServer m_stubServer; - QLocalSocket *m_stubSocket = nullptr; - QTemporaryFile *m_tempFile = nullptr; - - // Used on Unix only - QtcProcess m_process; - QTimer *m_stubConnectTimer = nullptr; - QByteArray m_stubServerDir; - - // Used on Windows only - qint64 m_appMainThreadId = 0; - -#ifdef Q_OS_WIN - PROCESS_INFORMATION *m_pid = nullptr; - HANDLE m_hInferior = NULL; - QWinEventNotifier *inferiorFinishedNotifier = nullptr; - QWinEventNotifier *processFinishedNotifier = nullptr; -#endif -}; - -TerminalImpl::TerminalImpl() - : d(new TerminalProcessPrivate(this)) -{ - connect(&d->m_stubServer, &QLocalServer::newConnection, - this, &TerminalImpl::stubConnectionAvailable); - - d->m_process.setProcessChannelMode(QProcess::ForwardedChannels); -} - -TerminalImpl::~TerminalImpl() -{ - stopProcess(); - delete d; -} - -void TerminalImpl::start() -{ - if (isRunning()) - return; - - d->m_result = {}; - -#ifdef Q_OS_WIN - - QString pcmd; - QString pargs; - if (m_setup.m_terminalMode != TerminalMode::Run) { // The debugger engines already pre-process the arguments. - pcmd = m_setup.m_commandLine.executable().toString(); - pargs = m_setup.m_commandLine.arguments(); - } else { - ProcessArgs outArgs; - ProcessArgs::prepareCommand(m_setup.m_commandLine, &pcmd, &outArgs, - &m_setup.m_environment, &m_setup.m_workingDirectory); - pargs = outArgs.toWindowsArgs(); - } - - const QString err = stubServerListen(); - if (!err.isEmpty()) { - emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); - return; - } - - QStringList env = m_setup.m_environment.toStringList(); - if (!env.isEmpty()) { - d->m_tempFile = new QTemporaryFile(); - if (!d->m_tempFile->open()) { - cleanupAfterStartFailure(msgCannotCreateTempFile(d->m_tempFile->errorString())); - return; - } - QString outString; - QTextStream out(&outString); - // Add PATH and SystemRoot environment variables in case they are missing - const QStringList fixedEnvironment = [env] { - QStringList envStrings = env; - // add PATH if necessary (for DLL loading) - if (envStrings.filter(QRegularExpression("^PATH=.*", QRegularExpression::CaseInsensitiveOption)).isEmpty()) { - const QString path = qtcEnvironmentVariable("PATH"); - if (!path.isEmpty()) - envStrings.prepend(QString::fromLatin1("PATH=%1").arg(path)); - } - // add systemroot if needed - if (envStrings.filter(QRegularExpression("^SystemRoot=.*", QRegularExpression::CaseInsensitiveOption)).isEmpty()) { - const QString systemRoot = qtcEnvironmentVariable("SystemRoot"); - if (!systemRoot.isEmpty()) - envStrings.prepend(QString::fromLatin1("SystemRoot=%1").arg(systemRoot)); - } - return envStrings; - }(); - - for (const QString &var : fixedEnvironment) - out << var << QChar(0); - out << QChar(0); - const QTextCodec *textCodec = QTextCodec::codecForName("UTF-16LE"); - QTC_CHECK(textCodec); - const QByteArray outBytes = textCodec ? textCodec->fromUnicode(outString) : QByteArray(); - if (!textCodec || d->m_tempFile->write(outBytes) < 0) { - cleanupAfterStartFailure(msgCannotWriteTempFile()); - return; - } - d->m_tempFile->flush(); - } - - STARTUPINFO si; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - - d->m_pid = new PROCESS_INFORMATION; - ZeroMemory(d->m_pid, sizeof(PROCESS_INFORMATION)); - - QString workDir = m_setup.m_workingDirectory.toUserOutput(); - if (!workDir.isEmpty() && !workDir.endsWith(QLatin1Char('\\'))) - workDir.append(QLatin1Char('\\')); - - // Quote a Windows command line correctly for the "CreateProcess" API - static const auto quoteWinCommand = [](const QString &program) { - const QChar doubleQuote = QLatin1Char('"'); - - // add the program as the first arg ... it works better - QString programName = program; - programName.replace(QLatin1Char('/'), QLatin1Char('\\')); - if (!programName.startsWith(doubleQuote) && !programName.endsWith(doubleQuote) - && programName.contains(QLatin1Char(' '))) { - programName.prepend(doubleQuote); - programName.append(doubleQuote); - } - return programName; - }; - static const auto quoteWinArgument = [](const QString &arg) { - if (arg.isEmpty()) - return QString::fromLatin1("\"\""); - - QString ret(arg); - // Quotes are escaped and their preceding backslashes are doubled. - ret.replace(QRegularExpression("(\\\\*)\""), "\\1\\1\\\""); - if (ret.contains(QRegularExpression("\\s"))) { - // The argument must not end with a \ since this would be interpreted - // as escaping the quote -- rather put the \ behind the quote: e.g. - // rather use "foo"\ than "foo\" - int i = ret.length(); - while (i > 0 && ret.at(i - 1) == QLatin1Char('\\')) - --i; - ret.insert(i, QLatin1Char('"')); - ret.prepend(QLatin1Char('"')); - } - return ret; - }; - static const auto createWinCommandlineMultiArgs = [](const QString &program, const QStringList &args) { - QString programName = quoteWinCommand(program); - for (const QString &arg : args) { - programName += QLatin1Char(' '); - programName += quoteWinArgument(arg); - } - return programName; - }; - static const auto createWinCommandlineSingleArg = [](const QString &program, const QString &args) - { - QString programName = quoteWinCommand(program); - if (!args.isEmpty()) { - programName += QLatin1Char(' '); - programName += args; - } - return programName; - }; - - QStringList stubArgs; - stubArgs << modeOption(m_setup.m_terminalMode) - << d->m_stubServer.fullServerName() - << workDir - << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) - << createWinCommandlineSingleArg(pcmd, pargs) - << msgPromptToClose(); - - const QString cmdLine = createWinCommandlineMultiArgs( - QCoreApplication::applicationDirPath() + QLatin1String("/qtcreator_process_stub.exe"), stubArgs); - - bool success = CreateProcessW(0, (WCHAR*)cmdLine.utf16(), - 0, 0, FALSE, CREATE_NEW_CONSOLE, - 0, 0, - &si, d->m_pid); - - if (!success) { - delete d->m_pid; - d->m_pid = nullptr; - const QString msg = Tr::tr("The process \"%1\" could not be started: %2") - .arg(cmdLine, winErrorMessage(GetLastError())); - cleanupAfterStartFailure(msg); - return; - } - - d->processFinishedNotifier = new QWinEventNotifier(d->m_pid->hProcess, this); - connect(d->processFinishedNotifier, &QWinEventNotifier::activated, - this, &TerminalImpl::stubExited); - -#else - - ProcessArgs::SplitError perr; - ProcessArgs pargs = ProcessArgs::prepareArgs(m_setup.m_commandLine.arguments(), - &perr, - HostOsInfo::hostOs(), - &m_setup.m_environment, - &m_setup.m_workingDirectory, - m_setup.m_abortOnMetaChars); - - QString pcmd; - if (perr == ProcessArgs::SplitOk) { - pcmd = m_setup.m_commandLine.executable().toString(); - } else { - if (perr != ProcessArgs::FoundMeta) { - emitError(QProcess::FailedToStart, Tr::tr("Quoting error in command.")); - return; - } - if (m_setup.m_terminalMode == TerminalMode::Debug) { - // FIXME: QTCREATORBUG-2809 - emitError(QProcess::FailedToStart, - Tr::tr("Debugging complex shell commands in a terminal" - " is currently not supported.")); - return; - } - pcmd = qtcEnvironmentVariable("SHELL", "/bin/sh"); - pargs = ProcessArgs::createUnixArgs( - {"-c", (ProcessArgs::quoteArg(m_setup.m_commandLine.executable().toString()) - + ' ' + m_setup.m_commandLine.arguments())}); - } - - ProcessArgs::SplitError qerr; - const TerminalCommand terminal = TerminalCommand::terminalEmulator(); - const ProcessArgs terminalArgs = ProcessArgs::prepareArgs(terminal.executeArgs, - &qerr, - HostOsInfo::hostOs(), - &m_setup.m_environment, - &m_setup.m_workingDirectory); - if (qerr != ProcessArgs::SplitOk) { - emitError(QProcess::FailedToStart, - qerr == ProcessArgs::BadQuoting - ? Tr::tr("Quoting error in terminal command.") - : Tr::tr("Terminal command may not be a shell command.")); - return; - } - - const QString err = stubServerListen(); - if (!err.isEmpty()) { - emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); - return; - } - - m_setup.m_environment.unset(QLatin1String("TERM")); - - const QStringList env = m_setup.m_environment.toStringList(); - if (!env.isEmpty()) { - d->m_tempFile = new QTemporaryFile(this); - if (!d->m_tempFile->open()) { - cleanupAfterStartFailure(msgCannotCreateTempFile(d->m_tempFile->errorString())); - return; - } - QByteArray contents; - for (const QString &var : env) { - const QByteArray l8b = var.toLocal8Bit(); - contents.append(l8b.constData(), l8b.size() + 1); - } - if (d->m_tempFile->write(contents) != contents.size() || !d->m_tempFile->flush()) { - cleanupAfterStartFailure(msgCannotWriteTempFile()); - return; - } - } - - const QString stubPath = QCoreApplication::applicationDirPath() - + QLatin1String("/" RELATIVE_LIBEXEC_PATH "/qtcreator_process_stub"); - - QStringList allArgs = terminalArgs.toUnixArgs(); - - allArgs << stubPath - << modeOption(m_setup.m_terminalMode) - << d->m_stubServer.fullServerName() - << msgPromptToClose() - << m_setup.m_workingDirectory.path() - << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) - << QString::number(getpid()) - << pcmd - << pargs.toUnixArgs(); - - if (terminal.needsQuotes) - allArgs = QStringList { ProcessArgs::joinArgs(allArgs) }; - - d->m_process.setEnvironment(m_setup.m_environment); - d->m_process.setCommand({terminal.command, allArgs}); - d->m_process.setProcessImpl(m_setup.m_processImpl); - d->m_process.setReaperTimeout(m_setup.m_reaperTimeout); - - d->m_process.start(); - if (!d->m_process.waitForStarted()) { - const QString msg = Tr::tr("Cannot start the terminal emulator \"%1\", change the " - "setting in the Environment preferences. (%2)") - .arg(terminal.command.toUserOutput(), d->m_process.errorString()); - cleanupAfterStartFailure(msg); - return; - } - d->m_stubConnectTimer = new QTimer(this); - connect(d->m_stubConnectTimer, &QTimer::timeout, this, &TerminalImpl::stopProcess); - d->m_stubConnectTimer->setSingleShot(true); - d->m_stubConnectTimer->start(10000); - -#endif -} - -void TerminalImpl::cleanupAfterStartFailure(const QString &errorMessage) -{ - stubServerShutdown(); - emitError(QProcess::FailedToStart, errorMessage); - delete d->m_tempFile; - d->m_tempFile = nullptr; -} - -void TerminalImpl::sendControlSignal(ControlSignal controlSignal) -{ - switch (controlSignal) { - case ControlSignal::Terminate: - case ControlSignal::Kill: - killProcess(); - if (HostOsInfo::isWindowsHost()) - killStub(); - break; - case ControlSignal::Interrupt: - sendCommand('i'); - break; - case ControlSignal::KickOff: - sendCommand('c'); - break; - case ControlSignal::CloseWriteChannel: - QTC_CHECK(false); - break; - } -} - -void TerminalImpl::sendCommand(char c) -{ -#ifdef Q_OS_WIN - Q_UNUSED(c) -#else - if (d->m_stubSocket && d->m_stubSocket->isWritable()) { - d->m_stubSocket->write(&c, 1); - d->m_stubSocket->flush(); - } -#endif -} - -void TerminalImpl::killProcess() -{ -#ifdef Q_OS_WIN - if (d->m_hInferior != NULL) { - TerminateProcess(d->m_hInferior, (unsigned)-1); - cleanupInferior(); - } -#else - sendCommand('k'); -#endif - d->m_processId = 0; -} - -void TerminalImpl::killStub() -{ - if (!isRunning()) - return; - -#ifdef Q_OS_WIN - TerminateProcess(d->m_pid->hProcess, (unsigned)-1); - WaitForSingleObject(d->m_pid->hProcess, INFINITE); - cleanupStub(); - emitFinished(-1, QProcess::CrashExit); -#else - sendCommand('s'); - stubServerShutdown(); - d->m_process.stop(); - d->m_process.waitForFinished(); -#endif -} - -void TerminalImpl::stopProcess() -{ - killProcess(); - killStub(); -} - -bool TerminalImpl::isRunning() const -{ -#ifdef Q_OS_WIN - return d->m_pid != nullptr; -#else - return d->m_process.state() != QProcess::NotRunning - || (d->m_stubSocket && d->m_stubSocket->isOpen()); -#endif -} - -QString TerminalImpl::stubServerListen() -{ -#ifdef Q_OS_WIN - if (d->m_stubServer.listen(QString::fromLatin1("creator-%1-%2") - .arg(QCoreApplication::applicationPid()) - .arg(rand()))) - return QString(); - return d->m_stubServer.errorString(); -#else - // We need to put the socket in a private directory, as some systems simply do not - // check the file permissions of sockets. - QString stubFifoDir; - while (true) { - { - QTemporaryFile tf; - if (!tf.open()) - return msgCannotCreateTempFile(tf.errorString()); - stubFifoDir = tf.fileName(); - } - // By now the temp file was deleted again - d->m_stubServerDir = QFile::encodeName(stubFifoDir); - if (!::mkdir(d->m_stubServerDir.constData(), 0700)) - break; - if (errno != EEXIST) - return msgCannotCreateTempDir(stubFifoDir, QString::fromLocal8Bit(strerror(errno))); - } - const QString stubServer = stubFifoDir + QLatin1String("/stub-socket"); - if (!d->m_stubServer.listen(stubServer)) { - ::rmdir(d->m_stubServerDir.constData()); - return Tr::tr("Cannot create socket \"%1\": %2") - .arg(stubServer, d->m_stubServer.errorString()); - } - return {}; -#endif -} - -void TerminalImpl::stubServerShutdown() -{ -#ifdef Q_OS_WIN - delete d->m_stubSocket; - d->m_stubSocket = nullptr; - if (d->m_stubServer.isListening()) - d->m_stubServer.close(); -#else - if (d->m_stubSocket) { - readStubOutput(); // we could get the shutdown signal before emptying the buffer - d->m_stubSocket->disconnect(); // avoid getting queued readyRead signals - d->m_stubSocket->deleteLater(); // we might be called from the disconnected signal of m_stubSocket - } - d->m_stubSocket = nullptr; - if (d->m_stubServer.isListening()) { - d->m_stubServer.close(); - ::rmdir(d->m_stubServerDir.constData()); - } -#endif -} - -void TerminalImpl::stubConnectionAvailable() -{ - if (d->m_stubConnectTimer) { - delete d->m_stubConnectTimer; - d->m_stubConnectTimer = nullptr; - } - - d->m_stubSocket = d->m_stubServer.nextPendingConnection(); - connect(d->m_stubSocket, &QIODevice::readyRead, this, &TerminalImpl::readStubOutput); - - if (HostOsInfo::isAnyUnixHost()) - connect(d->m_stubSocket, &QLocalSocket::disconnected, this, &TerminalImpl::stubExited); -} - -static QString errorMsg(int code) -{ - return QString::fromLocal8Bit(strerror(code)); -} - -void TerminalImpl::readStubOutput() -{ - while (d->m_stubSocket->canReadLine()) { - QByteArray out = d->m_stubSocket->readLine(); -#ifdef Q_OS_WIN - out.chop(2); // \r\n - if (out.startsWith("err:chdir ")) { - emitError(QProcess::FailedToStart, - msgCannotChangeToWorkDir(m_setup.m_workingDirectory, winErrorMessage(out.mid(10).toInt()))); - } else if (out.startsWith("err:exec ")) { - emitError(QProcess::FailedToStart, - msgCannotExecute(m_setup.m_commandLine.executable().toUserOutput(), winErrorMessage(out.mid(9).toInt()))); - } else if (out.startsWith("thread ")) { // Windows only - // TODO: ensure that it comes before "pid " comes - d->m_appMainThreadId = out.mid(7).toLongLong(); - } else if (out.startsWith("pid ")) { - // Will not need it any more - delete d->m_tempFile; - d->m_tempFile = nullptr; - d->m_processId = out.mid(4).toLongLong(); - - d->m_hInferior = OpenProcess( - SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, - FALSE, d->m_processId); - if (d->m_hInferior == NULL) { - emitError(QProcess::FailedToStart, - Tr::tr("Cannot obtain a handle to the inferior: %1") - .arg(winErrorMessage(GetLastError()))); - // Uhm, and now what? - continue; - } - d->inferiorFinishedNotifier = new QWinEventNotifier(d->m_hInferior, this); - connect(d->inferiorFinishedNotifier, &QWinEventNotifier::activated, this, [this] { - DWORD chldStatus; - - if (!GetExitCodeProcess(d->m_hInferior, &chldStatus)) - emitError(QProcess::UnknownError, - Tr::tr("Cannot obtain exit status from inferior: %1") - .arg(winErrorMessage(GetLastError()))); - cleanupInferior(); - emitFinished(chldStatus, QProcess::NormalExit); - }); - - emit started(d->m_processId, d->m_appMainThreadId); - } else { - emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); - TerminateProcess(d->m_pid->hProcess, (unsigned)-1); - break; - } -#else - out.chop(1); // \n - if (out.startsWith("err:chdir ")) { - emitError(QProcess::FailedToStart, - msgCannotChangeToWorkDir(m_setup.m_workingDirectory, errorMsg(out.mid(10).toInt()))); - } else if (out.startsWith("err:exec ")) { - emitError(QProcess::FailedToStart, - msgCannotExecute(m_setup.m_commandLine.executable().toString(), errorMsg(out.mid(9).toInt()))); - } else if (out.startsWith("spid ")) { - delete d->m_tempFile; - d->m_tempFile = nullptr; - } else if (out.startsWith("pid ")) { - d->m_processId = out.mid(4).toInt(); - emit started(d->m_processId); - } 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 { - emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); - d->m_process.terminate(); - break; - } -#endif - } // while -} - -void TerminalImpl::stubExited() -{ - // The stub exit might get noticed before we read the pid for the kill on Windows - // or the error status elsewhere. - if (d->m_stubSocket && d->m_stubSocket->state() == QLocalSocket::ConnectedState) - d->m_stubSocket->waitForDisconnected(); - -#ifdef Q_OS_WIN - cleanupStub(); - if (d->m_hInferior != NULL) { - TerminateProcess(d->m_hInferior, (unsigned)-1); - cleanupInferior(); - emitFinished(-1, QProcess::CrashExit); - } -#else - stubServerShutdown(); - delete d->m_tempFile; - d->m_tempFile = nullptr; - if (d->m_processId) - emitFinished(-1, QProcess::CrashExit); -#endif -} - -void TerminalImpl::cleanupInferior() -{ -#ifdef Q_OS_WIN - delete d->inferiorFinishedNotifier; - d->inferiorFinishedNotifier = nullptr; - CloseHandle(d->m_hInferior); - d->m_hInferior = NULL; -#endif -} - -void TerminalImpl::cleanupStub() -{ -#ifdef Q_OS_WIN - stubServerShutdown(); - delete d->processFinishedNotifier; - d->processFinishedNotifier = nullptr; - CloseHandle(d->m_pid->hThread); - CloseHandle(d->m_pid->hProcess); - delete d->m_pid; - d->m_pid = nullptr; - delete d->m_tempFile; - d->m_tempFile = nullptr; -#endif -} - -void TerminalImpl::emitError(QProcess::ProcessError error, const QString &errorString) -{ - d->m_result.m_error = error; - d->m_result.m_errorString = errorString; - if (error == QProcess::FailedToStart) - emit done(d->m_result); -} - -void TerminalImpl::emitFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - d->m_processId = 0; - d->m_result.m_exitCode = exitCode; - d->m_result.m_exitStatus = exitStatus; - emit done(d->m_result); -} - - -} // Internal -} // Utils |