Commit bbaf7893 authored by Oswald Buddenhagen's avatar Oswald Buddenhagen

"debug in terminal" feature.

includes complete refactoring of ConsoleProcess.
parent d35e1906
......@@ -6,4 +6,5 @@ SUBDIRS = \
aggregation \
extensionsystem \
utils \
utils/process_stub.pro \
cplusplus
......@@ -37,9 +37,12 @@
#include <QtCore/QStringList>
#include <QtCore/QProcess>
#include <QtNetwork/QLocalServer>
#ifdef Q_OS_WIN
#include <windows.h>
class QWinEventNotifier;
class QTemporaryFile;
#endif
namespace Core {
......@@ -50,41 +53,68 @@ class QWORKBENCH_UTILS_EXPORT ConsoleProcess : public QObject, public AbstractPr
Q_OBJECT
public:
ConsoleProcess(QObject *parent);
ConsoleProcess(QObject *parent = 0);
~ConsoleProcess();
bool start(const QString &program, const QStringList &args);
void stop();
bool isRunning() const;
qint64 applicationPID() const;
int exitCode() const;
void setDebug(bool on) { m_debug = on; }
bool isDebug() const { return m_debug; }
bool isRunning() const; // This reflects the state of the console+stub
qint64 applicationPID() const { return m_appPid; }
int exitCode() const { return m_appCode; } // This will be the signal number if exitStatus == CrashExit
QProcess::ExitStatus exitStatus() const { return m_appStatus; }
#ifdef Q_OS_WIN
// These are public for WinGuiProcess. Should be in AbstractProcess, but it has no .cpp so far.
static QString createCommandline(const QString &program, const QStringList &args);
static QStringList fixEnvironment(const QStringList &env);
#endif
signals:
void processError(const QString &error);
// These reflect the state of the actual client process
void processStarted();
void processStopped();
private:
bool m_isRunning;
#ifdef Q_OS_WIN
public:
static QString createCommandline(const QString &program,
const QStringList &args);
static QByteArray createEnvironment(const QStringList &env);
// These reflect the state of the console+stub
void wrapperStarted();
void wrapperStopped();
private slots:
void processDied();
void stubConnectionAvailable();
void readStubOutput();
void stubExited();
#ifdef Q_OS_WIN
void inferiorExited();
#endif
private:
QString stubServerListen();
void stubServerShutdown();
#ifdef Q_OS_WIN
void cleanupStub();
void cleanupInferior();
#endif
bool m_debug;
qint64 m_appPid;
int m_appCode;
QString m_executable;
QProcess::ExitStatus m_appStatus;
QLocalServer m_stubServer;
QLocalSocket *m_stubSocket;
#ifdef Q_OS_WIN
PROCESS_INFORMATION *m_pid;
HANDLE m_hInferior;
QWinEventNotifier *inferiorFinishedNotifier;
QWinEventNotifier *processFinishedNotifier;
QTemporaryFile *m_tempFile;
#else
private:
QProcess m_process;
private slots:
void processFinished(int, QProcess::ExitStatus);
QByteArray m_stubServerDir;
#endif
};
......
......@@ -29,57 +29,78 @@
#include "consoleprocess.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QLocalSocket>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
using namespace Core::Utils;
ConsoleProcess::ConsoleProcess(QObject *parent)
: QObject(parent)
{
m_isRunning = false;
}
m_debug = false;
m_appPid = 0;
m_stubSocket = 0;
ConsoleProcess::~ConsoleProcess()
{
connect(&m_stubServer, SIGNAL(newConnection()), SLOT(stubConnectionAvailable()));
m_process.setProcessChannelMode(QProcess::ForwardedChannels);
connect(&m_process, SIGNAL(finished(int, QProcess::ExitStatus)),
SLOT(stubExited()));
}
static QString shellEscape(const QString &in)
ConsoleProcess::~ConsoleProcess()
{
QString out = in;
out.replace('\'', "'\''");
out.prepend('\'');
out.append('\'');
return out;
stop();
}
bool ConsoleProcess::start(const QString &program, const QStringList &args)
{
if (m_process.state() != QProcess::NotRunning)
if (isRunning())
return false;
QString shellArgs;
shellArgs += QLatin1String("cd ");
shellArgs += shellEscape(workingDirectory());
shellArgs += QLatin1Char(';');
shellArgs += shellEscape(program);
foreach (const QString &arg, args) {
shellArgs += QLatin1Char(' ');
shellArgs += shellEscape(arg);
}
shellArgs += QLatin1String("; echo; echo \"Press enter to close this window\"; read DUMMY");
m_process.setEnvironment(environment());
QString err = stubServerListen();
if (!err.isEmpty()) {
emit processError(tr("Cannot set up comm channel: %1").arg(err));
return false;
}
connect(&m_process, SIGNAL(finished(int, QProcess::ExitStatus)),
this, SLOT(processFinished(int, QProcess::ExitStatus)));
QStringList xtermArgs;
xtermArgs << "-e" << (QCoreApplication::applicationDirPath() + "/qtcreator_process_stub")
<< (m_debug ? "debug" : "exec")
<< m_stubServer.fullServerName()
<< tr("Press <RETURN> to close this window...")
<< workingDirectory() << environment() << ""
<< program << args;
m_process.start(QLatin1String("xterm"), QStringList() << QLatin1String("-e") << "/bin/sh" << "-c" << shellArgs);
if (!m_process.waitForStarted())
m_process.start(QLatin1String("xterm"), xtermArgs);
if (!m_process.waitForStarted()) {
stubServerShutdown();
emit processError(tr("Cannot start console emulator xterm."));
return false;
emit processStarted();
}
m_executable = program;
emit wrapperStarted();
return true;
}
void ConsoleProcess::processFinished(int, QProcess::ExitStatus)
void ConsoleProcess::stop()
{
emit processStopped();
if (!isRunning())
return;
stubServerShutdown();
m_appPid = 0;
m_process.terminate();
if (!m_process.waitForFinished(1000))
m_process.kill();
m_process.waitForFinished();
}
bool ConsoleProcess::isRunning() const
......@@ -87,19 +108,97 @@ bool ConsoleProcess::isRunning() const
return m_process.state() != QProcess::NotRunning;
}
void ConsoleProcess::stop()
QString ConsoleProcess::stubServerListen()
{
m_process.terminate();
m_process.waitForFinished();
// 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 tr("Cannot create temporary file: %2").arg(tf.errorString());
stubFifoDir = QFile::encodeName(tf.fileName());
}
// By now the temp file was deleted again
m_stubServerDir = QFile::encodeName(stubFifoDir);
if (!::mkdir(m_stubServerDir.constData(), 0700))
break;
if (errno != EEXIST)
return tr("Cannot create temporary directory %1: %2").arg(stubFifoDir, strerror(errno));
}
QString stubServer = stubFifoDir + "/stub-socket";
if (!m_stubServer.listen(stubServer)) {
::rmdir(m_stubServerDir.constData());
return tr("Cannot create socket %1: %2").arg(stubServer, m_stubServer.errorString());
}
return QString();
}
void ConsoleProcess::stubServerShutdown()
{
delete m_stubSocket;
m_stubSocket = 0;
if (m_stubServer.isListening()) {
m_stubServer.close();
::rmdir(m_stubServerDir.constData());
}
}
void ConsoleProcess::stubConnectionAvailable()
{
m_stubSocket = m_stubServer.nextPendingConnection();
connect(m_stubSocket, SIGNAL(readyRead()), SLOT(readStubOutput()));
}
qint64 ConsoleProcess::applicationPID() const
static QString errorMsg(int code)
{
return m_process.pid();
return QString::fromLocal8Bit(strerror(code));
}
int ConsoleProcess::exitCode() const
void ConsoleProcess::readStubOutput()
{
return m_process.exitCode();
while (m_stubSocket->canReadLine()) {
QByteArray out = m_stubSocket->readLine();
out.chop(1); // \n
if (out.startsWith("err:chdir ")) {
emit processError(tr("Cannot change to working directory %1: %2")
.arg(workingDirectory(), errorMsg(out.mid(10).toInt())));
} else if (out.startsWith("err:exec ")) {
emit processError(tr("Cannot execute %1: %2")
.arg(m_executable, errorMsg(out.mid(9).toInt())));
} else if (out.startsWith("pid ")) {
m_appPid = out.mid(4).toInt();
emit processStarted();
} else if (out.startsWith("exit ")) {
m_appStatus = QProcess::NormalExit;
m_appCode = out.mid(5).toInt();
m_appPid = 0;
emit processStopped();
} else if (out.startsWith("crash ")) {
m_appStatus = QProcess::CrashExit;
m_appCode = out.mid(6).toInt();
m_appPid = 0;
emit processStopped();
} else {
emit processError(tr("Unexpected output from helper program."));
m_process.terminate();
break;
}
}
}
void ConsoleProcess::stubExited()
{
// The stub exit might get noticed before we read the error status.
if (m_stubSocket && m_stubSocket->state() == QLocalSocket::ConnectedState)
m_stubSocket->waitForDisconnected();
stubServerShutdown();
if (m_appPid) {
m_appStatus = QProcess::CrashExit;
m_appCode = -1;
m_appPid = 0;
emit processStopped(); // Maybe it actually did not, but keep state consistent
}
emit wrapperStopped();
}
This diff is collapsed.
TEMPLATE = app
TARGET = qtcreator_process_stub
DESTDIR = ../../../bin
CONFIG += warn_on console use_c_linker
CONFIG -= qt app_bundle
build_all:!build_pass {
CONFIG -= build_all
CONFIG += release
}
unix {
SOURCES += process_stub_unix.c
} else {
SOURCES += process_stub_win.c
}
/***************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Qt Software Information (qt-info@nokia.com)
**
**
** Non-Open Source Usage
**
** Licensees may use this file in accordance with the Qt Beta Version
** License Agreement, Agreement version 2.2 provided with the Software or,
** alternatively, in accordance with the terms contained in a written
** agreement between you and Nokia.
**
** GNU General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU General
** Public License versions 2.0 or 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the packaging
** of this file. Please review the following information to ensure GNU
** General Public Licensing requirements will be met:
**
** http://www.fsf.org/licensing/licenses/info/GPLv2.html and
** http://www.gnu.org/copyleft/gpl.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt GPL Exception
** version 1.3, included in the file GPL_EXCEPTION.txt in this package.
**
***************************************************************************/
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
extern char **environ;
static int qtcFd;
static char *sleepMsg;
static void __attribute__((noreturn)) doExit(int code)
{
tcsetpgrp(0, getpid());
puts(sleepMsg);
fgets(sleepMsg, 2, stdin); /* Minimal size to make it wait */
exit(code);
}
static void sendMsg(const char *msg, int num)
{
int pidStrLen;
int ioRet;
char pidStr[64];
pidStrLen = sprintf(pidStr, msg, num);
if ((ioRet = write(qtcFd, pidStr, pidStrLen)) != pidStrLen) {
fprintf(stderr, "Cannot write to creator comm socket: %s\n",
(ioRet < 0) ? strerror(errno) : "short write");
doExit(3);
}
}
enum {
ArgCmd = 0,
ArgAction,
ArgSocket,
ArgMsg,
ArgDir,
ArgEnv
};
/* syntax: $0 {"run"|"debug"} <pid-socket> <continuation-msg> <workdir> <env...> "" <exe> <args...> */
/* exit codes: 0 = ok, 1 = invocation error, 3 = internal error */
int main(int argc, char *argv[])
{
int envIdx = ArgEnv;
int errNo;
int chldPid;
int chldStatus;
int chldPipe[2];
struct sockaddr_un sau;
if (argc < ArgEnv) {
fprintf(stderr, "This is an internal helper of Qt Creator. Do not run it manually.\n");
return 1;
}
sleepMsg = argv[ArgMsg];
/* Connect to the master, i.e. Creator. */
if ((qtcFd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("Cannot create creator comm socket");
doExit(3);
}
sau.sun_family = AF_UNIX;
strcpy(sau.sun_path, argv[ArgSocket]);
if (connect(qtcFd, (struct sockaddr *)&sau, sizeof(sau))) {
fprintf(stderr, "Cannot connect creator comm socket %s: %s\n", sau.sun_path, strerror(errno));
doExit(1);
}
if (*argv[ArgDir] && chdir(argv[ArgDir])) {
/* Only expected error: no such file or direcotry */
sendMsg("err:chdir %d\n", errno);
return 1;
}
/* Create execution result notification pipe. */
if (pipe(chldPipe)) {
perror("Cannot create status pipe");
doExit(3);
}
/* The debugged program is not supposed to inherit these handles. But we cannot
* close the writing end before calling exec(). Just handle both ends the same way ... */
fcntl(chldPipe[0], F_SETFD, FD_CLOEXEC);
fcntl(chldPipe[1], F_SETFD, FD_CLOEXEC);
switch ((chldPid = fork())) {
case -1:
perror("Cannot fork child process failed");
doExit(3);
case 0:
close(qtcFd);
/* Put the process into an own process group and make it the foregroud
* group on this terminal, so it will receive ctrl-c events, etc.
* This is the main reason for *all* this stub magic in the first place. */
/* If one of these calls fails, the world is about to end anyway, so
* don't bother checking the return values. */
setpgid(0, 0);
tcsetpgrp(0, getpid());
/* Get a SIGTRAP after exec() has loaded the new program. */
#ifdef __linux__
ptrace(PTRACE_TRACEME);
#else
ptrace(PT_TRACE_ME, 0, 0, 0);
#endif
for (envIdx = ArgEnv; *argv[envIdx]; ++envIdx) ;
if (envIdx != ArgEnv) {
argv[envIdx] = 0;
environ = argv + ArgEnv;
}
++envIdx;
execvp(argv[envIdx], argv + envIdx);
/* Only expected error: no such file or direcotry, i.e. executable not found */
errNo = errno;
write(chldPipe[1], &errNo, sizeof(errNo)); /* Only realistic error case is SIGPIPE */
_exit(0);
default:
for (;;) {
if (wait(&chldStatus) < 0) {
perror("Cannot obtain exit status of child process");
doExit(3);
}
if (WIFSTOPPED(chldStatus)) {
/* The child stopped. This can be only the result of ptrace(TRACE_ME). */
/* We won't need the notification pipe any more, as we know that
* the exec() succeeded. */
close(chldPipe[0]);
close(chldPipe[1]);
chldPipe[0] = -1;
/* If we are not debugging, just skip the "handover enabler".
* This is suboptimal, as it makes us ignore setuid/-gid bits. */
if (!strcmp(argv[ArgAction], "debug")) {
/* Stop the child after we detach from it, so we can hand it over to gdb.
* If the signal delivery is not queued, things will go awry. It works on
* Linux and MacOSX ... */
kill(chldPid, SIGSTOP);
}
#ifdef __linux__
ptrace(PTRACE_DETACH, chldPid, 0, 0);
#else
ptrace(PT_DETACH, chldPid, 0, 0);
#endif
sendMsg("pid %d\n", chldPid);
} else if (WIFEXITED(chldStatus)) {
/* The child exited normally. */
if (chldPipe[0] >= 0) {
/* The child exited before being stopped by ptrace(). That can only
* mean that the exec() failed. */
switch (read(chldPipe[0], &errNo, sizeof(errNo))) {
default:
/* Read of unknown length. Should never happen ... */
errno = EPROTO;
case -1:
/* Read failed. Should never happen, either ... */
perror("Cannot read status from child process");
doExit(3);
case sizeof(errNo):
/* Child telling us the errno from exec(). */
sendMsg("err:exec %d\n", errNo);
return 3;
}
}
sendMsg("exit %d\n", WEXITSTATUS(chldStatus));
doExit(0);
} else {
sendMsg("crash %d\n", WTERMSIG(chldStatus));
doExit(0);
}
}
break;
}
}
/***************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Qt Software Information (qt-info@nokia.com)
**
**
** Non-Open Source Usage
**
** Licensees may use this file in accordance with the Qt Beta Version
** License Agreement, Agreement version 2.2 provided with the Software or,
** alternatively, in accordance with the terms contained in a written
** agreement between you and Nokia.
**
** GNU General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU General
** Public License versions 2.0 or 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the packaging
** of this file. Please review the following information to ensure GNU
** General Public Licensing requirements will be met:
**
** http://www.fsf.org/licensing/licenses/info/GPLv2.html and
** http://www.gnu.org/copyleft/gpl.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt GPL Exception
** version 1.3, included in the file GPL_EXCEPTION.txt in this package.
**
***************************************************************************/
#define _WIN32_WINNT 0x0501 /* WinXP, needed for DebugActiveProcessStop() */
#include <windows.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <direct.h>
static FILE *qtcFd;
static wchar_t *sleepMsg;
/* Print some "press enter" message, wait for that, exit. */
static void doExit(int code)
{
char buf[2];
_putws(sleepMsg);
fgets(buf, 2, stdin); /* Minimal size to make it wait */
exit(code);
}
/* Print an error message for unexpected Windows system errors, wait, exit. */
static void systemError(const char *str)
{
fprintf(stderr, str, GetLastError());
doExit(3);
}
/* Send a message to the master. */
static void sendMsg(const char *msg, int num)
{
int pidStrLen;
char pidStr[64];
pidStrLen = sprintf(pidStr, msg, num);
if (fwrite(pidStr, pidStrLen, 1, qtcFd) != 1 || fflush(qtcFd)) {
fprintf(stderr, "Cannot write to creator comm socket: %s\n",
strerror(errno));
doExit(3);
}
}
/* Ignore the first ctrl-c/break within a second. */
static BOOL WINAPI ctrlHandler(DWORD dwCtrlType)
{
static ULARGE_INTEGER lastTime;
ULARGE_INTEGER thisTime;
SYSTEMTIME sysTime;
FILETIME fileTime;
if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT) {
GetSystemTime(&sysTime);
SystemTimeToFileTime(&sysTime, &fileTime);
thisTime.LowPart = fileTime.dwLowDateTime;
thisTime.HighPart = fileTime.dwHighDateTime;
if (lastTime.QuadPart + 10000000 < thisTime.QuadPart) {
lastTime.QuadPart = thisTime.QuadPart;
return TRUE;
}
}
return FALSE;
}
enum {
ArgCmd = 0,
ArgAction,
ArgSocket,
ArgDir,
ArgEnv,
ArgCmdLine,
ArgMsg,
ArgCount
};
/* syntax: $0 {"run"|"debug"} <pid-socket> <workdir> <env-file> <cmdline> <continuation-msg> */
/* exit codes: 0 = ok, 1 = invocation error, 3 = internal error */
int main()
{
int argc;
wchar_t **argv;
wchar_t *env = 0;
STARTUPINFOW si;
PROCESS_INFORMATION pi;
DEBUG_EVENT dbev;
argv = CommandLineToArgvW(GetCommandLine(), &argc);
if (argc != ArgCount) {
fprintf(stderr, "This is an internal helper of Qt Creator. Do not run it manually.\n");