diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 5cc79c31827..d898f228be4 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -31,7 +31,7 @@ add_qtc_library(Utils codegeneration.cpp codegeneration.h completinglineedit.cpp completinglineedit.h completingtextedit.cpp completingtextedit.h - consoleprocess.cpp consoleprocess.h consoleprocess_p.h + consoleprocess.cpp consoleprocess.h cpplanguage_details.h crumblepath.cpp crumblepath.h delegates.cpp delegates.h @@ -177,7 +177,6 @@ add_qtc_library(Utils extend_qtc_target(Utils CONDITION WIN32 SOURCES - consoleprocess_win.cpp process_ctrlc_stub.cpp touchbar/touchbar.cpp DEPENDS @@ -190,7 +189,6 @@ extend_qtc_target(Utils CONDITION WIN32 extend_qtc_target(Utils CONDITION APPLE SOURCES - consoleprocess_unix.cpp fileutils_mac.mm fileutils_mac.h processhandle_mac.mm theme/theme_mac.mm theme/theme_mac.h @@ -202,7 +200,6 @@ extend_qtc_target(Utils CONDITION APPLE extend_qtc_target(Utils CONDITION UNIX AND NOT APPLE SOURCES - consoleprocess_unix.cpp touchbar/touchbar.cpp ) diff --git a/src/libs/utils/consoleprocess.cpp b/src/libs/utils/consoleprocess.cpp index 335f9084c7e..9477361abde 100644 --- a/src/libs/utils/consoleprocess.cpp +++ b/src/libs/utils/consoleprocess.cpp @@ -23,10 +23,47 @@ ** ****************************************************************************/ -#include "consoleprocess_p.h" +#include "consoleprocess.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN + +# include +# include +# include + +#else + +# include +# include +# include +# include +# include + +#endif namespace Utils { +// TerminalCommand + TerminalCommand::TerminalCommand(const QString &command, const QString &openArgs, const QString &executeArgs) : command(command) , openArgs(openArgs) @@ -34,12 +71,810 @@ TerminalCommand::TerminalCommand(const QString &command, const QString &openArgs { } +// ConsoleProcessPrivate + +class ConsoleProcessPrivate +{ +public: + ConsoleProcessPrivate() = default; + + static QString m_defaultConsoleProcess; + ConsoleProcess::Mode m_mode = ConsoleProcess::Run; + QString m_workingDir; + Environment m_environment; + qint64 m_appPid = 0; + int m_appCode; + CommandLine m_commandLine; + QProcess::ExitStatus m_appStatus; + QLocalServer m_stubServer; + QLocalSocket *m_stubSocket = nullptr; + QTemporaryFile *m_tempFile = nullptr; + QProcess::ProcessError m_error = QProcess::UnknownError; + QString m_errorString; + bool m_abortOnMetaChars = true; + QSettings *m_settings = nullptr; + + // Used on Unix only + QProcess m_process; + bool m_stubConnected = false; + QTimer *m_stubConnectTimer = nullptr; + QByteArray m_stubServerDir; + qint64 m_stubPid = 0; + + // 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 +}; + + +// ConsoleProcess + +ConsoleProcess::ConsoleProcess(QObject *parent) : + QObject(parent), d(new ConsoleProcessPrivate) +{ + connect(&d->m_stubServer, &QLocalServer::newConnection, + this, &ConsoleProcess::stubConnectionAvailable); + + d->m_process.setProcessChannelMode(QProcess::ForwardedChannels); +} + ConsoleProcess::~ConsoleProcess() { stop(); delete d; } +void ConsoleProcess::setCommand(const CommandLine &command) +{ + d->m_commandLine = command; +} + +void ConsoleProcess::setSettings(QSettings *settings) +{ + d->m_settings = settings; +} + +Q_GLOBAL_STATIC_WITH_ARGS(const QVector, knownTerminals, ( +{ + {"x-terminal-emulator", "", "-e"}, + {"xterm", "", "-e"}, + {"aterm", "", "-e"}, + {"Eterm", "", "-e"}, + {"rxvt", "", "-e"}, + {"urxvt", "", "-e"}, + {"xfce4-terminal", "", "-x"}, + {"konsole", "--separate", "-e"}, + {"gnome-terminal", "", "--"} +})); + +TerminalCommand ConsoleProcess::defaultTerminalEmulator() +{ + static TerminalCommand defaultTerm; + + if (defaultTerm.command.isEmpty()) { + + if (HostOsInfo::isMacHost()) { + const QString termCmd = QCoreApplication::applicationDirPath() + + "/../Resources/scripts/openTerminal.py"; + if (QFileInfo::exists(termCmd)) + defaultTerm = {termCmd, "", ""}; + else + defaultTerm = {"/usr/X11/bin/xterm", "", "-e"}; + + } else if (HostOsInfo::isAnyUnixHost()) { + const Environment env = Environment::systemEnvironment(); + for (const TerminalCommand &term : *knownTerminals) { + const QString result = env.searchInPath(term.command).toString(); + if (!result.isEmpty()) + defaultTerm = {result, term.openArgs, term.executeArgs}; + else + defaultTerm = {"xterm", "", "-e"}; + } + } + } + + return defaultTerm; +} + +QVector ConsoleProcess::availableTerminalEmulators() +{ + QVector result; + + if (HostOsInfo::isAnyUnixHost()) { + const Environment env = Environment::systemEnvironment(); + for (const TerminalCommand &term : *knownTerminals) { + const QString command = env.searchInPath(term.command).toString(); + if (!command.isEmpty()) + result.push_back({command, term.openArgs, term.executeArgs}); + } + // sort and put default terminal on top + const TerminalCommand defaultTerm = defaultTerminalEmulator(); + result.removeAll(defaultTerm); + sort(result); + result.prepend(defaultTerm); + } + + return result; +} + +const char kTerminalVersion[] = "4.8"; +const char kTerminalVersionKey[] = "General/Terminal/SettingsVersion"; +const char kTerminalCommandKey[] = "General/Terminal/Command"; +const char kTerminalOpenOptionsKey[] = "General/Terminal/OpenOptions"; +const char kTerminalExecuteOptionsKey[] = "General/Terminal/ExecuteOptions"; + +TerminalCommand ConsoleProcess::terminalEmulator(const QSettings *settings) +{ + if (settings && HostOsInfo::isAnyUnixHost()) { + if (settings->value(kTerminalVersionKey).toString() == kTerminalVersion) { + if (settings->contains(kTerminalCommandKey)) + return {settings->value(kTerminalCommandKey).toString(), + settings->value(kTerminalOpenOptionsKey).toString(), + settings->value(kTerminalExecuteOptionsKey).toString()}; + } else { + // TODO remove reading of old settings some time after 4.8 + const QString value = settings->value("General/TerminalEmulator").toString().trimmed(); + if (!value.isEmpty()) { + // split off command and options + const QStringList splitCommand = QtcProcess::splitArgs(value); + if (QTC_GUARD(!splitCommand.isEmpty())) { + const QString command = splitCommand.first(); + const QStringList quotedArgs = Utils::transform(splitCommand.mid(1), + &QtcProcess::quoteArgUnix); + const QString options = quotedArgs.join(' '); + return {command, "", options}; + } + } + } + } + + return defaultTerminalEmulator(); +} + +void ConsoleProcess::setTerminalEmulator(QSettings *settings, const TerminalCommand &term) +{ + if (HostOsInfo::isAnyUnixHost()) { + settings->setValue(kTerminalVersionKey, kTerminalVersion); + if (term == defaultTerminalEmulator()) { + settings->remove(kTerminalCommandKey); + settings->remove(kTerminalOpenOptionsKey); + settings->remove(kTerminalExecuteOptionsKey); + } else { + settings->setValue(kTerminalCommandKey, term.command); + settings->setValue(kTerminalOpenOptionsKey, term.openArgs); + settings->setValue(kTerminalExecuteOptionsKey, term.executeArgs); + } + } +} + +static QString 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 QString quoteWinArgument(const QString &arg) +{ + if (!arg.length()) + return QString::fromLatin1("\"\""); + + QString ret(arg); + // Quotes are escaped and their preceding backslashes are doubled. + ret.replace(QRegExp(QLatin1String("(\\\\*)\"")), QLatin1String("\\1\\1\\\"")); + if (ret.contains(QRegExp(QLatin1String("\\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; +} + +// Quote a Windows command line correctly for the "CreateProcess" API +QString createWinCommandline(const QString &program, const QStringList &args) +{ + QString programName = quoteWinCommand(program); + foreach (const QString &arg, args) { + programName += QLatin1Char(' '); + programName += quoteWinArgument(arg); + } + return programName; +} + +QString createWinCommandline(const QString &program, const QString &args) +{ + QString programName = quoteWinCommand(program); + if (!args.isEmpty()) { + programName += QLatin1Char(' '); + programName += args; + } + return programName; +} + + +bool ConsoleProcess::startTerminalEmulator(QSettings *settings, const QString &workingDir, + const Environment &env) +{ +#ifdef Q_OS_WIN + Q_UNUSED(settings) + + STARTUPINFO si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + PROCESS_INFORMATION pinfo; + ZeroMemory(&pinfo, sizeof(pinfo)); + + QString cmdLine = createWinCommandline( + QString::fromLocal8Bit(qgetenv("COMSPEC")), QString()); + // cmdLine is assumed to be detached - + // https://blogs.msdn.microsoft.com/oldnewthing/20090601-00/?p=18083 + + QString totalEnvironment = env.toStringList().join('\0') + '\0'; + LPVOID envPtr = (env != Environment::systemEnvironment()) + ? (WCHAR *)(totalEnvironment.utf16()) : nullptr; + + bool success = CreateProcessW(0, (WCHAR *)cmdLine.utf16(), + 0, 0, FALSE, CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, + envPtr, workingDir.isEmpty() ? 0 : (WCHAR *)workingDir.utf16(), + &si, &pinfo); + + if (success) { + CloseHandle(pinfo.hThread); + CloseHandle(pinfo.hProcess); + } + + return success; +#else + const TerminalCommand term = terminalEmulator(settings); + QProcess process; + process.setProgram(term.command); + process.setArguments(QtcProcess::splitArgs(term.openArgs)); + process.setProcessEnvironment(env.toProcessEnvironment()); + process.setWorkingDirectory(workingDir); + + return process.startDetached(); +#endif +} + +void ConsoleProcess::setAbortOnMetaChars(bool abort) +{ + d->m_abortOnMetaChars = abort; +} + +qint64 ConsoleProcess::applicationMainThreadID() const +{ + if (HostOsInfo::isWindowsHost()) + return d->m_appMainThreadId; + return -1; +} + +bool ConsoleProcess::start() +{ + if (isRunning()) + return false; + + d->m_errorString.clear(); + d->m_error = QProcess::UnknownError; + +#ifdef Q_OS_WIN + + QString pcmd; + QString pargs; + if (d->m_mode != Run) { // The debugger engines already pre-process the arguments. + pcmd = d->m_commandLine.executable().toString(); + pargs = d->m_commandLine.arguments(); + } else { + QtcProcess::Arguments outArgs; + QtcProcess::prepareCommand(d->m_commandLine.executable().toString(), + d->m_commandLine.arguments(), + &pcmd, &outArgs, OsTypeWindows, + &d->m_environment, &d->m_workingDir); + pargs = outArgs.toWindowsArgs(); + } + + const QString err = stubServerListen(); + if (!err.isEmpty()) { + emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); + return false; + } + + QStringList env = d->m_environment.toStringList(); + if (!env.isEmpty()) { + d->m_tempFile = new QTemporaryFile(); + if (!d->m_tempFile->open()) { + stubServerShutdown(); + emitError(QProcess::FailedToStart, msgCannotCreateTempFile(d->m_tempFile->errorString())); + delete d->m_tempFile; + d->m_tempFile = nullptr; + return false; + } + QTextStream out(d->m_tempFile); + out.setCodec("UTF-16LE"); + out.setGenerateByteOrderMark(false); + + // 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(QRegExp(QLatin1String("^PATH="),Qt::CaseInsensitive)).isEmpty()) { + QByteArray path = qgetenv("PATH"); + if (!path.isEmpty()) + envStrings.prepend(QString::fromLatin1("PATH=%1").arg(QString::fromLocal8Bit(path))); + } + // add systemroot if needed + if (envStrings.filter(QRegExp(QLatin1String("^SystemRoot="),Qt::CaseInsensitive)).isEmpty()) { + QByteArray systemRoot = qgetenv("SystemRoot"); + if (!systemRoot.isEmpty()) + envStrings.prepend(QString::fromLatin1("SystemRoot=%1").arg(QString::fromLocal8Bit(systemRoot))); + } + return envStrings; + }(); + + for (const QString &var : fixedEnvironment) + out << var << QChar(0); + out << QChar(0); + out.flush(); + if (out.status() != QTextStream::Ok) { + stubServerShutdown(); + emitError(QProcess::FailedToStart, msgCannotWriteTempFile()); + delete d->m_tempFile; + d->m_tempFile = nullptr; + return false; + } + } + + 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 = QDir::toNativeSeparators(workingDirectory()); + if (!workDir.isEmpty() && !workDir.endsWith(QLatin1Char('\\'))) + workDir.append(QLatin1Char('\\')); + + QStringList stubArgs; + stubArgs << modeOption(d->m_mode) + << d->m_stubServer.fullServerName() + << workDir + << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) + << createWinCommandline(pcmd, pargs) + << msgPromptToClose(); + + const QString cmdLine = createWinCommandline( + 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; + delete d->m_tempFile; + d->m_tempFile = nullptr; + stubServerShutdown(); + emitError(QProcess::FailedToStart, tr("The process \"%1\" could not be started: %2").arg(cmdLine, winErrorMessage(GetLastError()))); + return false; + } + + d->processFinishedNotifier = new QWinEventNotifier(d->m_pid->hProcess, this); + connect(d->processFinishedNotifier, &QWinEventNotifier::activated, + this, &ConsoleProcess::stubExited); + +#else + + QtcProcess::SplitError perr; + QtcProcess::Arguments pargs = QtcProcess::prepareArgs(d->m_commandLine.arguments(), + &perr, + HostOsInfo::hostOs(), + &d->m_environment, + &d->m_workingDir, + d->m_abortOnMetaChars); + + QString pcmd; + if (perr == QtcProcess::SplitOk) { + pcmd = d->m_commandLine.executable().toString(); + } else { + if (perr != QtcProcess::FoundMeta) { + emitError(QProcess::FailedToStart, tr("Quoting error in command.")); + return false; + } + if (d->m_mode == Debug) { + // FIXME: QTCREATORBUG-2809 + emitError(QProcess::FailedToStart, tr("Debugging complex shell commands in a terminal" + " is currently not supported.")); + return false; + } + pcmd = QLatin1String("/bin/sh"); + pargs = QtcProcess::Arguments::createUnixArgs( + {"-c", (QtcProcess::quoteArg(d->m_commandLine.executable().toString()) + + ' ' + d->m_commandLine.arguments())}); + } + + QtcProcess::SplitError qerr; + const TerminalCommand terminal = terminalEmulator(d->m_settings); + const QtcProcess::Arguments terminalArgs = QtcProcess::prepareArgs(terminal.executeArgs, + &qerr, + HostOsInfo::hostOs(), + &d->m_environment, + &d->m_workingDir); + if (qerr != QtcProcess::SplitOk) { + emitError(QProcess::FailedToStart, qerr == QtcProcess::BadQuoting + ? tr("Quoting error in terminal command.") + : tr("Terminal command may not be a shell command.")); + return false; + } + + const QString err = stubServerListen(); + if (!err.isEmpty()) { + emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); + return false; + } + + d->m_environment.unset(QLatin1String("TERM")); + const QStringList env = d->m_environment.toStringList(); + if (!env.isEmpty()) { + d->m_tempFile = new QTemporaryFile(); + if (!d->m_tempFile->open()) { + stubServerShutdown(); + emitError(QProcess::FailedToStart, msgCannotCreateTempFile(d->m_tempFile->errorString())); + delete d->m_tempFile; + d->m_tempFile = nullptr; + return false; + } + QByteArray contents; + for (const QString &var : env) { + QByteArray l8b = var.toLocal8Bit(); + contents.append(l8b.constData(), l8b.size() + 1); + } + if (d->m_tempFile->write(contents) != contents.size() || !d->m_tempFile->flush()) { + stubServerShutdown(); + emitError(QProcess::FailedToStart, msgCannotWriteTempFile()); + delete d->m_tempFile; + d->m_tempFile = nullptr; + return false; + } + } + + const QString stubPath = QCoreApplication::applicationDirPath() + + QLatin1String("/" QTC_REL_TOOLS_PATH "/qtcreator_process_stub"); + const QStringList allArgs = terminalArgs.toUnixArgs() + << stubPath + << modeOption(d->m_mode) + << d->m_stubServer.fullServerName() + << msgPromptToClose() + << workingDirectory() + << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) + << QString::number(getpid()) + << pcmd + << pargs.toUnixArgs(); + + d->m_process.start(terminal.command, allArgs); + if (!d->m_process.waitForStarted()) { + stubServerShutdown(); + emitError(QProcess::UnknownError, tr("Cannot start the terminal emulator \"%1\", change the setting in the " + "Environment options.").arg(terminal.command)); + delete d->m_tempFile; + d->m_tempFile = nullptr; + return false; + } + d->m_stubConnectTimer = new QTimer(this); + connect(d->m_stubConnectTimer, &QTimer::timeout, this, &ConsoleProcess::stop); + d->m_stubConnectTimer->setSingleShot(true); + d->m_stubConnectTimer->start(10000); + +#endif + + return true; +} + +void ConsoleProcess::killProcess() +{ +#ifdef Q_OS_WIN + if (d->m_hInferior != NULL) { + TerminateProcess(d->m_hInferior, (unsigned)-1); + cleanupInferior(); + } +#else + if (d->m_stubSocket && d->m_stubSocket->isWritable()) { + d->m_stubSocket->write("k", 1); + d->m_stubSocket->flush(); + } + d->m_appPid = 0; +#endif +} + +void ConsoleProcess::killStub() +{ +#ifdef Q_OS_WIN + if (d->m_pid) { + TerminateProcess(d->m_pid->hProcess, (unsigned)-1); + WaitForSingleObject(d->m_pid->hProcess, INFINITE); + cleanupStub(); + } +#else + if (d->m_stubSocket && d->m_stubSocket->isWritable()) { + d->m_stubSocket->write("s", 1); + d->m_stubSocket->flush(); + } + stubServerShutdown(); + d->m_stubPid = 0; +#endif +} + +void ConsoleProcess::stop() +{ + killProcess(); + killStub(); + if (isRunning() && HostOsInfo::isAnyUnixHost()) { + d->m_process.terminate(); + if (!d->m_process.waitForFinished(1000) && d->m_process.state() == QProcess::Running) { + d->m_process.kill(); + d->m_process.waitForFinished(); + } + } +} + +bool ConsoleProcess::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 ConsoleProcess::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("Cannot create socket \"%1\": %2").arg(stubServer, d->m_stubServer.errorString()); + } + return QString(); +#endif +} + +void ConsoleProcess::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 ConsoleProcess::stubConnectionAvailable() +{ + d->m_stubConnected = true; + emit stubStarted(); + + 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, &ConsoleProcess::readStubOutput); + + if (HostOsInfo::isAnyUnixHost()) + connect(d->m_stubSocket, &QLocalSocket::disconnected, this, &ConsoleProcess::stubExited); +} + +static QString errorMsg(int code) +{ + return QString::fromLocal8Bit(strerror(code)); +} + +void ConsoleProcess::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(workingDirectory(), winErrorMessage(out.mid(10).toInt()))); + } else if (out.startsWith("err:exec ")) { + emitError(QProcess::FailedToStart, msgCannotExecute(d->m_commandLine.executable().toUserOutput(), winErrorMessage(out.mid(9).toInt()))); + } else if (out.startsWith("thread ")) { // Windows only + 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_appPid = out.mid(4).toLongLong(); + + d->m_hInferior = OpenProcess( + SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, + FALSE, d->m_appPid); + if (d->m_hInferior == NULL) { + emitError(QProcess::FailedToStart, 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("Cannot obtain exit status from inferior: %1") + .arg(winErrorMessage(GetLastError()))); + cleanupInferior(); + d->m_appStatus = QProcess::NormalExit; + d->m_appCode = chldStatus; + emit processStopped(d->m_appCode, d->m_appStatus); + }); + + emit processStarted(); + } 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(workingDirectory(), errorMsg(out.mid(10).toInt()))); + } else if (out.startsWith("err:exec ")) { + emitError(QProcess::FailedToStart, msgCannotExecute(d->m_commandLine.executable().toString(), errorMsg(out.mid(9).toInt()))); + } else if (out.startsWith("spid ")) { + delete d->m_tempFile; + d->m_tempFile = nullptr; + + d->m_stubPid = out.mid(4).toInt(); + } else if (out.startsWith("pid ")) { + d->m_appPid = out.mid(4).toInt(); + emit processStarted(); + } else if (out.startsWith("exit ")) { + d->m_appStatus = QProcess::NormalExit; + d->m_appCode = out.mid(5).toInt(); + d->m_appPid = 0; + emit processStopped(d->m_appCode, d->m_appStatus); + } else if (out.startsWith("crash ")) { + d->m_appStatus = QProcess::CrashExit; + d->m_appCode = out.mid(6).toInt(); + d->m_appPid = 0; + emit processStopped(d->m_appCode, d->m_appStatus); + } else { + emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); + d->m_stubPid = 0; + d->m_process.terminate(); + break; + } +#endif + } // while +} + +void ConsoleProcess::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(); + d->m_appStatus = QProcess::CrashExit; + d->m_appCode = -1; + emit processStopped(d->m_appCode, d->m_appStatus); + } +#else + stubServerShutdown(); + d->m_stubPid = 0; + delete d->m_tempFile; + d->m_tempFile = nullptr; + if (d->m_appPid) { + d->m_appStatus = QProcess::CrashExit; + d->m_appCode = -1; + d->m_appPid = 0; + emit processStopped(d->m_appCode, d->m_appStatus); // Maybe it actually did not, but keep state consistent + } +#endif + emit stubStopped(); +} + +void ConsoleProcess::detachStub() +{ + if (HostOsInfo::isAnyUnixHost()) { + if (d->m_stubSocket && d->m_stubSocket->isWritable()) { + d->m_stubSocket->write("d", 1); + d->m_stubSocket->flush(); + } + stubServerShutdown(); + d->m_stubPid = 0; + } +} + +void ConsoleProcess::cleanupInferior() +{ +#ifdef Q_OS_WIN + delete d->inferiorFinishedNotifier; + d->inferiorFinishedNotifier = nullptr; + CloseHandle(d->m_hInferior); + d->m_hInferior = NULL; + d->m_appPid = 0; +#endif +} + +void ConsoleProcess::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 ConsoleProcess::setMode(Mode m) { d->m_mode = m; @@ -174,4 +1009,4 @@ bool TerminalCommand::operator<(const TerminalCommand &other) const return command < other.command; } -} +} // Utils diff --git a/src/libs/utils/consoleprocess.h b/src/libs/utils/consoleprocess.h index 57e1da31454..4e6e4f12b6f 100644 --- a/src/libs/utils/consoleprocess.h +++ b/src/libs/utils/consoleprocess.h @@ -59,7 +59,8 @@ class QTCREATOR_UTILS_EXPORT ConsoleProcess : public QObject public: enum Mode { Run, Debug, Suspend }; - ConsoleProcess(QObject *parent = nullptr); + + explicit ConsoleProcess(QObject *parent = nullptr); ~ConsoleProcess() override; void setCommand(const Utils::CommandLine &command); @@ -75,8 +76,6 @@ public: QString errorString() const; bool start(); - -public slots: void stop(); public: @@ -90,36 +89,17 @@ public: void killStub(); qint64 applicationMainThreadID() const; -#ifndef Q_OS_WIN void detachStub(); -#endif int exitCode() const; QProcess::ExitStatus exitStatus() const; -#ifdef Q_OS_WIN - // Add PATH and SystemRoot environment variables in case they are missing - static QStringList fixWinEnvironment(const QStringList &env); - // Quote a Windows command line correctly for the "CreateProcess" API - static QString createWinCommandline(const QString &program, const QStringList &args); - static QString createWinCommandline(const QString &program, const QString &args); -#endif - -#ifndef Q_OS_WIN - void setSettings(QSettings *settings); + void setSettings(QSettings *); static TerminalCommand defaultTerminalEmulator(); static QVector availableTerminalEmulators(); static TerminalCommand terminalEmulator(const QSettings *settings); static void setTerminalEmulator(QSettings *settings, const TerminalCommand &term); -#else - void setSettings(QSettings *) {} - - static TerminalCommand defaultTerminalEmulator() { return TerminalCommand(); } - static QVector availableTerminalEmulators() { return {}; } - static TerminalCommand terminalEmulator(const QSettings *) { return TerminalCommand(); } - static void setTerminalEmulator(QSettings *, const TerminalCommand &) {} -#endif static bool startTerminalEmulator(QSettings *settings, const QString &workingDir, const Utils::Environment &env); @@ -127,6 +107,7 @@ public: signals: void error(QProcess::ProcessError error); void processError(const QString &errorString); + // These reflect the state of the actual client process void processStarted(); void processStopped(int, QProcess::ExitStatus); @@ -139,9 +120,6 @@ private: void stubConnectionAvailable(); void readStubOutput(); void stubExited(); -#ifdef Q_OS_WIN - void inferiorExited(); -#endif static QString modeOption(Mode m); static QString msgCommChannelFailed(const QString &error); @@ -156,14 +134,12 @@ private: void emitError(QProcess::ProcessError err, const QString &errorString); QString stubServerListen(); void stubServerShutdown(); -#ifdef Q_OS_WIN void cleanupStub(); void cleanupInferior(); -#endif - struct ConsoleProcessPrivate *d; + class ConsoleProcessPrivate *d; }; -} //namespace Utils +} // Utils Q_DECLARE_METATYPE(Utils::TerminalCommand) diff --git a/src/libs/utils/consoleprocess_p.h b/src/libs/utils/consoleprocess_p.h deleted file mode 100644 index 73169ad8dc6..00000000000 --- a/src/libs/utils/consoleprocess_p.h +++ /dev/null @@ -1,81 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -****************************************************************************/ - -#pragma once - -#include "consoleprocess.h" -#include "environment.h" - -#include - -#include -#include - -QT_BEGIN_NAMESPACE -class QTimer; -QT_END_NAMESPACE - -#ifdef Q_OS_WIN -# include -# include -#endif - -namespace Utils { - -struct ConsoleProcessPrivate { - ConsoleProcessPrivate(); - - static QString m_defaultConsoleProcess; - ConsoleProcess::Mode m_mode = ConsoleProcess::Run; - QString m_workingDir; - Environment m_environment; - qint64 m_appPid = 0; - int m_appCode; - CommandLine m_commandLine; - QProcess::ExitStatus m_appStatus; - QLocalServer m_stubServer; - QLocalSocket *m_stubSocket = nullptr; - QTemporaryFile *m_tempFile = nullptr; - QProcess::ProcessError m_error = QProcess::UnknownError; - QString m_errorString; - bool m_abortOnMetaChars = true; - -#ifdef Q_OS_UNIX - QProcess m_process; - QByteArray m_stubServerDir; - QSettings *m_settings = nullptr; - bool m_stubConnected = false; - qint64 m_stubPid = 0; - QTimer *m_stubConnectTimer = nullptr; -#else - qint64 m_appMainThreadId = 0; - PROCESS_INFORMATION *m_pid = nullptr; - HANDLE m_hInferior = NULL; - QWinEventNotifier *inferiorFinishedNotifier = nullptr; - QWinEventNotifier *processFinishedNotifier = nullptr; -#endif -}; - -} //namespace Utils diff --git a/src/libs/utils/consoleprocess_unix.cpp b/src/libs/utils/consoleprocess_unix.cpp deleted file mode 100644 index fa777f18596..00000000000 --- a/src/libs/utils/consoleprocess_unix.cpp +++ /dev/null @@ -1,463 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -****************************************************************************/ - -#include "consoleprocess_p.h" - -#include "qtcprocess.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace Utils { - -ConsoleProcessPrivate::ConsoleProcessPrivate() = default; - -ConsoleProcess::ConsoleProcess(QObject *parent) : - QObject(parent), d(new ConsoleProcessPrivate) -{ - connect(&d->m_stubServer, &QLocalServer::newConnection, - this, &ConsoleProcess::stubConnectionAvailable); - - d->m_process.setProcessChannelMode(QProcess::ForwardedChannels); -} - -qint64 ConsoleProcess::applicationMainThreadID() const -{ - return -1; -} - -void ConsoleProcess::setCommand(const Utils::CommandLine &command) -{ - d->m_commandLine = command; -} - -void ConsoleProcess::setAbortOnMetaChars(bool abort) -{ - d->m_abortOnMetaChars = abort; -} - -void ConsoleProcess::setSettings(QSettings *settings) -{ - d->m_settings = settings; -} - -bool ConsoleProcess::start() -{ - if (isRunning()) - return false; - - d->m_errorString.clear(); - d->m_error = QProcess::UnknownError; - - QtcProcess::SplitError perr; - QtcProcess::Arguments pargs = QtcProcess::prepareArgs(d->m_commandLine.arguments(), - &perr, - HostOsInfo::hostOs(), - &d->m_environment, - &d->m_workingDir, - d->m_abortOnMetaChars); - - QString pcmd; - if (perr == QtcProcess::SplitOk) { - pcmd = d->m_commandLine.executable().toString(); - } else { - if (perr != QtcProcess::FoundMeta) { - emitError(QProcess::FailedToStart, tr("Quoting error in command.")); - return false; - } - if (d->m_mode == Debug) { - // FIXME: QTCREATORBUG-2809 - emitError(QProcess::FailedToStart, tr("Debugging complex shell commands in a terminal" - " is currently not supported.")); - return false; - } - pcmd = QLatin1String("/bin/sh"); - pargs = QtcProcess::Arguments::createUnixArgs( - {"-c", (QtcProcess::quoteArg(d->m_commandLine.executable().toString()) - + ' ' + d->m_commandLine.arguments())}); - } - - QtcProcess::SplitError qerr; - const TerminalCommand terminal = terminalEmulator(d->m_settings); - const QtcProcess::Arguments terminalArgs = QtcProcess::prepareArgs(terminal.executeArgs, - &qerr, - HostOsInfo::hostOs(), - &d->m_environment, - &d->m_workingDir); - if (qerr != QtcProcess::SplitOk) { - emitError(QProcess::FailedToStart, qerr == QtcProcess::BadQuoting - ? tr("Quoting error in terminal command.") - : tr("Terminal command may not be a shell command.")); - return false; - } - - const QString err = stubServerListen(); - if (!err.isEmpty()) { - emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); - return false; - } - - d->m_environment.unset(QLatin1String("TERM")); - QStringList env = d->m_environment.toStringList(); - if (!env.isEmpty()) { - d->m_tempFile = new QTemporaryFile(); - if (!d->m_tempFile->open()) { - stubServerShutdown(); - emitError(QProcess::FailedToStart, msgCannotCreateTempFile(d->m_tempFile->errorString())); - delete d->m_tempFile; - d->m_tempFile = nullptr; - return false; - } - QByteArray contents; - foreach (const QString &var, env) { - QByteArray l8b = var.toLocal8Bit(); - contents.append(l8b.constData(), l8b.size() + 1); - } - if (d->m_tempFile->write(contents) != contents.size() || !d->m_tempFile->flush()) { - stubServerShutdown(); - emitError(QProcess::FailedToStart, msgCannotWriteTempFile()); - delete d->m_tempFile; - d->m_tempFile = nullptr; - return false; - } - } - - const QString stubPath = QCoreApplication::applicationDirPath() - + QLatin1String("/" QTC_REL_TOOLS_PATH "/qtcreator_process_stub"); - const QStringList allArgs = terminalArgs.toUnixArgs() - << stubPath - << modeOption(d->m_mode) - << d->m_stubServer.fullServerName() - << msgPromptToClose() - << workingDirectory() - << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) - << QString::number(getpid()) - << pcmd - << pargs.toUnixArgs(); - - d->m_process.start(terminal.command, allArgs); - if (!d->m_process.waitForStarted()) { - stubServerShutdown(); - emitError(QProcess::UnknownError, tr("Cannot start the terminal emulator \"%1\", change the setting in the " - "Environment options.").arg(terminal.command)); - delete d->m_tempFile; - d->m_tempFile = nullptr; - return false; - } - d->m_stubConnectTimer = new QTimer(this); - connect(d->m_stubConnectTimer, &QTimer::timeout, this, &ConsoleProcess::stop); - d->m_stubConnectTimer->setSingleShot(true); - d->m_stubConnectTimer->start(10000); - return true; -} - -void ConsoleProcess::killProcess() -{ - if (d->m_stubSocket && d->m_stubSocket->isWritable()) { - d->m_stubSocket->write("k", 1); - d->m_stubSocket->flush(); - } - d->m_appPid = 0; -} - -void ConsoleProcess::killStub() -{ - if (d->m_stubSocket && d->m_stubSocket->isWritable()) { - d->m_stubSocket->write("s", 1); - d->m_stubSocket->flush(); - } - stubServerShutdown(); - d->m_stubPid = 0; -} - -void ConsoleProcess::detachStub() -{ - if (d->m_stubSocket && d->m_stubSocket->isWritable()) { - d->m_stubSocket->write("d", 1); - d->m_stubSocket->flush(); - } - stubServerShutdown(); - d->m_stubPid = 0; -} - -void ConsoleProcess::stop() -{ - killProcess(); - killStub(); - if (isRunning()) { - d->m_process.terminate(); - if (!d->m_process.waitForFinished(1000) && d->m_process.state() == QProcess::Running) { - d->m_process.kill(); - d->m_process.waitForFinished(); - } - } -} - -bool ConsoleProcess::isRunning() const -{ - return d->m_process.state() != QProcess::NotRunning - || (d->m_stubSocket && d->m_stubSocket->isOpen()); -} - -QString ConsoleProcess::stubServerListen() -{ - // We need to put the socket in a private directory, as some systems simply do not - // check the file permissions of sockets. - QString stubFifoDir; - forever { - { - 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("Cannot create socket \"%1\": %2").arg(stubServer, d->m_stubServer.errorString()); - } - return QString(); -} - -void ConsoleProcess::stubServerShutdown() -{ - 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()); - } -} - -void ConsoleProcess::stubConnectionAvailable() -{ - if (d->m_stubConnectTimer) { - delete d->m_stubConnectTimer; - d->m_stubConnectTimer = nullptr; - } - d->m_stubConnected = true; - emit stubStarted(); - d->m_stubSocket = d->m_stubServer.nextPendingConnection(); - connect(d->m_stubSocket, &QIODevice::readyRead, this, &ConsoleProcess::readStubOutput); - connect(d->m_stubSocket, &QLocalSocket::disconnected, this, &ConsoleProcess::stubExited); -} - -static QString errorMsg(int code) -{ - return QString::fromLocal8Bit(strerror(code)); -} - -void ConsoleProcess::readStubOutput() -{ - while (d->m_stubSocket->canReadLine()) { - QByteArray out = d->m_stubSocket->readLine(); - out.chop(1); // \n - if (out.startsWith("err:chdir ")) { - emitError(QProcess::FailedToStart, msgCannotChangeToWorkDir(workingDirectory(), errorMsg(out.mid(10).toInt()))); - } else if (out.startsWith("err:exec ")) { - emitError(QProcess::FailedToStart, msgCannotExecute(d->m_commandLine.executable().toString(), errorMsg(out.mid(9).toInt()))); - } else if (out.startsWith("spid ")) { - delete d->m_tempFile; - d->m_tempFile = nullptr; - - d->m_stubPid = out.mid(4).toInt(); - } else if (out.startsWith("pid ")) { - d->m_appPid = out.mid(4).toInt(); - emit processStarted(); - } else if (out.startsWith("exit ")) { - d->m_appStatus = QProcess::NormalExit; - d->m_appCode = out.mid(5).toInt(); - d->m_appPid = 0; - emit processStopped(d->m_appCode, d->m_appStatus); - } else if (out.startsWith("crash ")) { - d->m_appStatus = QProcess::CrashExit; - d->m_appCode = out.mid(6).toInt(); - d->m_appPid = 0; - emit processStopped(d->m_appCode, d->m_appStatus); - } else { - emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); - d->m_stubPid = 0; - d->m_process.terminate(); - break; - } - } -} - -void ConsoleProcess::stubExited() -{ - // The stub exit might get noticed before we read the error status. - if (d->m_stubSocket && d->m_stubSocket->state() == QLocalSocket::ConnectedState) - d->m_stubSocket->waitForDisconnected(); - stubServerShutdown(); - d->m_stubPid = 0; - delete d->m_tempFile; - d->m_tempFile = nullptr; - if (d->m_appPid) { - d->m_appStatus = QProcess::CrashExit; - d->m_appCode = -1; - d->m_appPid = 0; - emit processStopped(d->m_appCode, d->m_appStatus); // Maybe it actually did not, but keep state consistent - } - emit stubStopped(); -} - -Q_GLOBAL_STATIC_WITH_ARGS(const QVector, knownTerminals, ( -{ - {"x-terminal-emulator", "", "-e"}, - {"xterm", "", "-e"}, - {"aterm", "", "-e"}, - {"Eterm", "", "-e"}, - {"rxvt", "", "-e"}, - {"urxvt", "", "-e"}, - {"xfce4-terminal", "", "-x"}, - {"konsole", "--separate", "-e"}, - {"gnome-terminal", "", "--"} -})); - -TerminalCommand ConsoleProcess::defaultTerminalEmulator() -{ - static TerminalCommand defaultTerm; - if (defaultTerm.command.isEmpty()) { - defaultTerm = []() -> TerminalCommand { - if (HostOsInfo::isMacHost()) { - const QString termCmd = QCoreApplication::applicationDirPath() - + "/../Resources/scripts/openTerminal.py"; - if (QFileInfo::exists(termCmd)) - return {termCmd, "", ""}; - return {"/usr/X11/bin/xterm", "", "-e"}; - } - const Environment env = Environment::systemEnvironment(); - for (const TerminalCommand &term : *knownTerminals) { - const QString result = env.searchInPath(term.command).toString(); - if (!result.isEmpty()) - return {result, term.openArgs, term.executeArgs}; - } - return {"xterm", "", "-e"}; - }(); - } - return defaultTerm; -} - -QVector ConsoleProcess::availableTerminalEmulators() -{ - QVector result; - const Environment env = Environment::systemEnvironment(); - for (const TerminalCommand &term : *knownTerminals) { - const QString command = env.searchInPath(term.command).toString(); - if (!command.isEmpty()) - result.push_back({command, term.openArgs, term.executeArgs}); - } - // sort and put default terminal on top - const TerminalCommand defaultTerm = defaultTerminalEmulator(); - result.removeAll(defaultTerm); - sort(result); - result.prepend(defaultTerm); - return result; -} - -const char kTerminalVersion[] = "4.8"; -const char kTerminalVersionKey[] = "General/Terminal/SettingsVersion"; -const char kTerminalCommandKey[] = "General/Terminal/Command"; -const char kTerminalOpenOptionsKey[] = "General/Terminal/OpenOptions"; -const char kTerminalExecuteOptionsKey[] = "General/Terminal/ExecuteOptions"; - -TerminalCommand ConsoleProcess::terminalEmulator(const QSettings *settings) -{ - if (settings) { - if (settings->value(kTerminalVersionKey).toString() == kTerminalVersion) { - if (settings->contains(kTerminalCommandKey)) - return {settings->value(kTerminalCommandKey).toString(), - settings->value(kTerminalOpenOptionsKey).toString(), - settings->value(kTerminalExecuteOptionsKey).toString()}; - } else { - // TODO remove reading of old settings some time after 4.8 - const QString value = settings->value("General/TerminalEmulator").toString().trimmed(); - if (!value.isEmpty()) { - // split off command and options - const QStringList splitCommand = QtcProcess::splitArgs(value); - if (QTC_GUARD(!splitCommand.isEmpty())) { - const QString command = splitCommand.first(); - const QStringList quotedArgs = Utils::transform(splitCommand.mid(1), - &QtcProcess::quoteArgUnix); - const QString options = quotedArgs.join(' '); - return {command, "", options}; - } - } - } - } - return defaultTerminalEmulator(); -} - -void ConsoleProcess::setTerminalEmulator(QSettings *settings, const TerminalCommand &term) -{ - settings->setValue(kTerminalVersionKey, kTerminalVersion); - if (term == defaultTerminalEmulator()) { - settings->remove(kTerminalCommandKey); - settings->remove(kTerminalOpenOptionsKey); - settings->remove(kTerminalExecuteOptionsKey); - } else { - settings->setValue(kTerminalCommandKey, term.command); - settings->setValue(kTerminalOpenOptionsKey, term.openArgs); - settings->setValue(kTerminalExecuteOptionsKey, term.executeArgs); - } -} - -bool ConsoleProcess::startTerminalEmulator(QSettings *settings, const QString &workingDir, - const Utils::Environment &env) -{ - const TerminalCommand term = terminalEmulator(settings); - QProcess process; - process.setProgram(term.command); - process.setArguments(QtcProcess::splitArgs(term.openArgs)); - process.setProcessEnvironment(env.toProcessEnvironment()); - process.setWorkingDirectory(workingDir); - - return process.startDetached(); -} - -} // namespace Utils diff --git a/src/libs/utils/consoleprocess_win.cpp b/src/libs/utils/consoleprocess_win.cpp deleted file mode 100644 index bb7b257f6f5..00000000000 --- a/src/libs/utils/consoleprocess_win.cpp +++ /dev/null @@ -1,409 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -****************************************************************************/ - -#include "consoleprocess_p.h" -#include "environment.h" -#include "qtcprocess.h" -#include "winutils.h" - -#include -#include -#include - -#include -#include - -namespace Utils { - -ConsoleProcessPrivate::ConsoleProcessPrivate() = default; - -ConsoleProcess::ConsoleProcess(QObject *parent) : - QObject(parent), d(new ConsoleProcessPrivate) -{ - connect(&d->m_stubServer, &QLocalServer::newConnection, - this, &ConsoleProcess::stubConnectionAvailable); -} - -void ConsoleProcess::setCommand(const Utils::CommandLine &command) -{ - d->m_commandLine = command; -} - -void ConsoleProcess::setAbortOnMetaChars(bool abort) -{ - d->m_abortOnMetaChars = abort; -} - -qint64 ConsoleProcess::applicationMainThreadID() const -{ - return d->m_appMainThreadId; -} - -bool ConsoleProcess::start() -{ - if (isRunning()) - return false; - - d->m_errorString.clear(); - d->m_error = QProcess::UnknownError; - - QString pcmd; - QString pargs; - if (d->m_mode != Run) { // The debugger engines already pre-process the arguments. - pcmd = d->m_commandLine.executable().toString(); - pargs = d->m_commandLine.arguments(); - } else { - QtcProcess::Arguments outArgs; - QtcProcess::prepareCommand(d->m_commandLine.executable().toString(), - d->m_commandLine.arguments(), - &pcmd, &outArgs, OsTypeWindows, - &d->m_environment, &d->m_workingDir); - pargs = outArgs.toWindowsArgs(); - } - - const QString err = stubServerListen(); - if (!err.isEmpty()) { - emitError(QProcess::FailedToStart, msgCommChannelFailed(err)); - return false; - } - - QStringList env = d->m_environment.toStringList(); - if (!env.isEmpty()) { - d->m_tempFile = new QTemporaryFile(); - if (!d->m_tempFile->open()) { - stubServerShutdown(); - emitError(QProcess::FailedToStart, msgCannotCreateTempFile(d->m_tempFile->errorString())); - delete d->m_tempFile; - d->m_tempFile = nullptr; - return false; - } - QTextStream out(d->m_tempFile); - out.setCodec("UTF-16LE"); - out.setGenerateByteOrderMark(false); - foreach (const QString &var, fixWinEnvironment(env)) - out << var << QChar(0); - out << QChar(0); - out.flush(); - if (out.status() != QTextStream::Ok) { - stubServerShutdown(); - emitError(QProcess::FailedToStart, msgCannotWriteTempFile()); - delete d->m_tempFile; - d->m_tempFile = nullptr; - return false; - } - } - - 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 = QDir::toNativeSeparators(workingDirectory()); - if (!workDir.isEmpty() && !workDir.endsWith(QLatin1Char('\\'))) - workDir.append(QLatin1Char('\\')); - - QStringList stubArgs; - stubArgs << modeOption(d->m_mode) - << d->m_stubServer.fullServerName() - << workDir - << (d->m_tempFile ? d->m_tempFile->fileName() : QString()) - << createWinCommandline(pcmd, pargs) - << msgPromptToClose(); - - const QString cmdLine = createWinCommandline( - 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; - delete d->m_tempFile; - d->m_tempFile = nullptr; - stubServerShutdown(); - emitError(QProcess::FailedToStart, tr("The process \"%1\" could not be started: %2").arg(cmdLine, winErrorMessage(GetLastError()))); - return false; - } - - d->processFinishedNotifier = new QWinEventNotifier(d->m_pid->hProcess, this); - connect(d->processFinishedNotifier, &QWinEventNotifier::activated, - this, &ConsoleProcess::stubExited); - return true; -} - - -void ConsoleProcess::killProcess() -{ - if (d->m_hInferior != NULL) { - TerminateProcess(d->m_hInferior, (unsigned)-1); - cleanupInferior(); - } -} - -void ConsoleProcess::killStub() -{ - if (d->m_pid) { - TerminateProcess(d->m_pid->hProcess, (unsigned)-1); - WaitForSingleObject(d->m_pid->hProcess, INFINITE); - cleanupStub(); - } -} - -void ConsoleProcess::stop() -{ - killProcess(); - killStub(); -} - -bool ConsoleProcess::isRunning() const -{ - return d->m_pid != nullptr; -} - -QString ConsoleProcess::stubServerListen() -{ - if (d->m_stubServer.listen(QString::fromLatin1("creator-%1-%2") - .arg(QCoreApplication::applicationPid()) - .arg(rand()))) - return QString(); - return d->m_stubServer.errorString(); -} - -void ConsoleProcess::stubServerShutdown() -{ - delete d->m_stubSocket; - d->m_stubSocket = nullptr; - if (d->m_stubServer.isListening()) - d->m_stubServer.close(); -} - -void ConsoleProcess::stubConnectionAvailable() -{ - emit stubStarted(); - d->m_stubSocket = d->m_stubServer.nextPendingConnection(); - connect(d->m_stubSocket, &QIODevice::readyRead, this, &ConsoleProcess::readStubOutput); -} - -void ConsoleProcess::readStubOutput() -{ - while (d->m_stubSocket->canReadLine()) { - QByteArray out = d->m_stubSocket->readLine(); - out.chop(2); // \r\n - if (out.startsWith("err:chdir ")) { - emitError(QProcess::FailedToStart, msgCannotChangeToWorkDir(workingDirectory(), winErrorMessage(out.mid(10).toInt()))); - } else if (out.startsWith("err:exec ")) { - emitError(QProcess::FailedToStart, msgCannotExecute(d->m_commandLine.executable().toUserOutput(), winErrorMessage(out.mid(9).toInt()))); - } else if (out.startsWith("thread ")) { // Windows only - 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_appPid = out.mid(4).toLongLong(); - - d->m_hInferior = OpenProcess( - SYNCHRONIZE | PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, - FALSE, d->m_appPid); - if (d->m_hInferior == NULL) { - emitError(QProcess::FailedToStart, 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, &ConsoleProcess::inferiorExited); - emit processStarted(); - } else { - emitError(QProcess::UnknownError, msgUnexpectedOutput(out)); - TerminateProcess(d->m_pid->hProcess, (unsigned)-1); - break; - } - } -} - -void ConsoleProcess::cleanupInferior() -{ - delete d->inferiorFinishedNotifier; - d->inferiorFinishedNotifier = nullptr; - CloseHandle(d->m_hInferior); - d->m_hInferior = NULL; - d->m_appPid = 0; -} - -void ConsoleProcess::inferiorExited() -{ - DWORD chldStatus; - - if (!GetExitCodeProcess(d->m_hInferior, &chldStatus)) - emitError(QProcess::UnknownError, tr("Cannot obtain exit status from inferior: %1") - .arg(winErrorMessage(GetLastError()))); - cleanupInferior(); - d->m_appStatus = QProcess::NormalExit; - d->m_appCode = chldStatus; - emit processStopped(d->m_appCode, d->m_appStatus); -} - -void ConsoleProcess::cleanupStub() -{ - 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; -} - -void ConsoleProcess::stubExited() -{ - // The stub exit might get noticed before we read the pid for the kill. - if (d->m_stubSocket && d->m_stubSocket->state() == QLocalSocket::ConnectedState) - d->m_stubSocket->waitForDisconnected(); - cleanupStub(); - if (d->m_hInferior != NULL) { - TerminateProcess(d->m_hInferior, (unsigned)-1); - cleanupInferior(); - d->m_appStatus = QProcess::CrashExit; - d->m_appCode = -1; - emit processStopped(d->m_appCode, d->m_appStatus); - } - emit stubStopped(); -} - -QStringList ConsoleProcess::fixWinEnvironment(const QStringList &env) -{ - QStringList envStrings = env; - // add PATH if necessary (for DLL loading) - if (envStrings.filter(QRegExp(QLatin1String("^PATH="),Qt::CaseInsensitive)).isEmpty()) { - QByteArray path = qgetenv("PATH"); - if (!path.isEmpty()) - envStrings.prepend(QString::fromLatin1("PATH=%1").arg(QString::fromLocal8Bit(path))); - } - // add systemroot if needed - if (envStrings.filter(QRegExp(QLatin1String("^SystemRoot="),Qt::CaseInsensitive)).isEmpty()) { - QByteArray systemRoot = qgetenv("SystemRoot"); - if (!systemRoot.isEmpty()) - envStrings.prepend(QString::fromLatin1("SystemRoot=%1").arg(QString::fromLocal8Bit(systemRoot))); - } - return envStrings; -} - -static QString 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 QString quoteWinArgument(const QString &arg) -{ - if (!arg.length()) - return QString::fromLatin1("\"\""); - - QString ret(arg); - // Quotes are escaped and their preceding backslashes are doubled. - ret.replace(QRegExp(QLatin1String("(\\\\*)\"")), QLatin1String("\\1\\1\\\"")); - if (ret.contains(QRegExp(QLatin1String("\\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; -} - -QString ConsoleProcess::createWinCommandline(const QString &program, const QStringList &args) -{ - QString programName = quoteWinCommand(program); - foreach (const QString &arg, args) { - programName += QLatin1Char(' '); - programName += quoteWinArgument(arg); - } - return programName; -} - -QString ConsoleProcess::createWinCommandline(const QString &program, const QString &args) -{ - QString programName = quoteWinCommand(program); - if (!args.isEmpty()) { - programName += QLatin1Char(' '); - programName += args; - } - return programName; -} - -bool ConsoleProcess::startTerminalEmulator(QSettings *, const QString &workingDir, - const Utils::Environment &env) -{ - STARTUPINFO si; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - - PROCESS_INFORMATION pinfo; - ZeroMemory(&pinfo, sizeof(pinfo)); - - QString cmdLine = createWinCommandline( - QString::fromLocal8Bit(qgetenv("COMSPEC")), QString()); - // cmdLine is assumed to be detached - - // https://blogs.msdn.microsoft.com/oldnewthing/20090601-00/?p=18083 - - QString totalEnvironment = env.toStringList().join('\0') + '\0'; - LPVOID envPtr = (env != Utils::Environment::systemEnvironment()) - ? (WCHAR *)(totalEnvironment.utf16()) : nullptr; - - bool success = CreateProcessW(0, (WCHAR *)cmdLine.utf16(), - 0, 0, FALSE, CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, - envPtr, workingDir.isEmpty() ? 0 : (WCHAR *)workingDir.utf16(), - &si, &pinfo); - - if (success) { - CloseHandle(pinfo.hThread); - CloseHandle(pinfo.hProcess); - } - - return success; -} - -} // namespace Utils diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index 36f3aa14a32..793658a1eca 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -132,9 +132,6 @@ SOURCES += \ $$PWD/jsontreeitem.cpp \ $$PWD/namevaluevalidator.cpp -win32:SOURCES += $$PWD/consoleprocess_win.cpp -else:SOURCES += $$PWD/consoleprocess_unix.cpp - HEADERS += \ $$PWD/environmentfwd.h \ $$PWD/genericconstants.h \ @@ -179,7 +176,6 @@ HEADERS += \ $$PWD/qtcolorbutton.h \ $$PWD/savedaction.h \ $$PWD/consoleprocess.h \ - $$PWD/consoleprocess_p.h \ $$PWD/synchronousprocess.h \ $$PWD/savefile.h \ $$PWD/fileutils.h \ diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index 456d45a2457..c3adefd0603 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -66,7 +66,6 @@ Project { "completingtextedit.h", "consoleprocess.cpp", "consoleprocess.h", - "consoleprocess_p.h", "cpplanguage_details.h", "crumblepath.cpp", "crumblepath.h", @@ -312,22 +311,6 @@ Project { ] } - Group { - name: "WindowsUtils" - condition: qbs.targetOS.contains("windows") - files: [ - "consoleprocess_win.cpp", - ] - } - - Group { - name: "ConsoleProcess_unix" - condition: qbs.targetOS.contains("unix") - files: [ - "consoleprocess_unix.cpp", - ] - } - Group { name: "FileUtils_macos" condition: qbs.targetOS.contains("macos")