Files
qt-creator/src/libs/utils/terminalprocess.cpp
Eike Ziller e77c90469b Utils: Use qtcEnvironmentVariable* instead of qEnvironmentVariable*
And instead of qgetenv.
Takes Qt Creator's setting at "Environment > System > Environment" into
account, which makes it easier on some platforms to set them (e.g.
macOS), can be configured differently in different settings paths, and
potentially can be changed at runtime (depending on usage).

Change-Id: I50e457bab2d3495e5c69676fe1a0257a5fea3e52
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: hjk <hjk@qt.io>
2022-09-01 13:03:05 +00:00

715 lines
23 KiB
C++

// 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 "terminalprocess_p.h"
#include "commandline.h"
#include "environment.h"
#include "hostosinfo.h"
#include "qtcassert.h"
#include "qtcprocess.h"
#include "terminalcommand.h"
#include "winutils.h"
#include <QCoreApplication>
#include <QLocalServer>
#include <QLocalSocket>
#include <QRegularExpression>
#include <QTemporaryFile>
#include <QTextCodec>
#include <QTimer>
#include <QWinEventNotifier>
#ifdef Q_OS_WIN
# include <windows.h>
# include <stdlib.h>
# include <cstring>
#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 QtcProcess::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 QtcProcess::tr("Press <RETURN> to close this window...");
}
static QString msgCannotCreateTempFile(const QString &why)
{
return QtcProcess::tr("Cannot create temporary file: %1").arg(why);
}
static QString msgCannotWriteTempFile()
{
return QtcProcess::tr("Cannot write temporary file. Disk full?");
}
static QString msgCannotCreateTempDir(const QString & dir, const QString &why)
{
return QtcProcess::tr("Cannot create temporary directory \"%1\": %2").arg(dir, why);
}
static QString msgUnexpectedOutput(const QByteArray &what)
{
return QtcProcess::tr("Unexpected output from helper program (%1).")
.arg(QString::fromLatin1(what));
}
static QString msgCannotChangeToWorkDir(const FilePath &dir, const QString &why)
{
return QtcProcess::tr("Cannot change to working directory \"%1\": %2").arg(dir.toString(), why);
}
static QString msgCannotExecute(const QString & p, const QString &why)
{
return QtcProcess::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 = QtcProcess::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, QtcProcess::tr("Quoting error in command."));
return;
}
if (m_setup.m_terminalMode == TerminalMode::Debug) {
// FIXME: QTCREATORBUG-2809
emitError(QProcess::FailedToStart,
QtcProcess::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
? QtcProcess::tr("Quoting error in terminal command.")
: QtcProcess::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({FilePath::fromString(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
= QtcProcess::tr("Cannot start the terminal emulator \"%1\", change the setting in the "
"Environment options.")
.arg(terminal.command);
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:
stopProcess();
break;
case ControlSignal::Interrupt:
sendCommand('i');
break;
case ControlSignal::KickOff:
sendCommand('c');
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();
#else
sendCommand('s');
stubServerShutdown();
d->m_process.waitForFinished();
#endif
emitFinished(-1, QProcess::CrashExit);
}
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 QtcProcess::tr("Cannot create socket \"%1\": %2")
.arg(stubServer, d->m_stubServer.errorString());
}
return QString();
#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,
QtcProcess::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,
QtcProcess::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