Files
qt-creator/src/libs/utils/terminalprocess.cpp
Jarek Kobus 9d8106e54a TerminalProcess: Fix stopping the terminal
Fixes 30 seconds freeze when stopping the terminal
during debugging (when being interrupted on some
breakpoint).

Fixes also the freeze on closing the preferences
dialog after opening the remote shell via
"Open Remote Shell" and keeping it open.

Fixes: QTCREATORBUG-28365
Change-Id: I15dfd9cba02d03e0ba65878c5285ea8cc96d8aad
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
2022-10-31 10:29:09 +00:00

718 lines
24 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 <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 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 preferences. (%2)")
.arg(terminal.command).arg(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;
}
}
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 QtcProcess::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,
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