Commit 18fa4865 authored by Fawzi Mohamed's avatar Fawzi Mohamed
Browse files

consoleprocess: support Terminal.app on mac



Distinguishes the process that starts the terminal from the stub
process, as on mac to support Terminal.app they are different.

Handle the stub not through the process that starts the terminal,
but through the local socket (on *nix).

Replace the blocking wait(...) in the main thread, with a nonblocking
wait in the signal handler when receiving a SIGCHLD, to leave the
main thread able to handle communication with creator.

This change allows the use of terminal emulator commands
that share a single instance or that fork.
So this is also the real fix for QTCREATORBUG-1633 on linux.

If creator crashes the stub and the debugged program live on.
This was done on purpose, it could be changed if considered better.

Task-number: QTCREATORBUG-6371
Task-number: QTCREATORBUG-1633

Change-Id: I4d4fb3a67b1987f4e46e2c603dcefe8c15152ad2
Reviewed-by: default avatarOswald Buddenhagen <oswald.buddenhagen@digia.com>
parent 7db3b660
#! /bin/bash
i=`pwd`
i=${i//\\/\\\\\\\\}
i=${i//\"/\\\\\\\"}
i=${i//\$/\\\\\\\$}
i=${i//\`/\\\\\\\`}
i=\\\"$i\\\"
# ugly escaping: for apple script \ and " need to be escaped, whereas %q takes care of all bash escaping
declare -a args
mydir=`pwd`
mydir=$(printf '%q' "$mydir")
mydir="${mydir//\\/\\\\}"
args[0]="cd ${mydir//\"/\\\"};"
for a in "$@" ; do
x=$(printf '%q ' "$a")
x="${x//\\/\\\\}"
args[${#args[@]}]="${x//\"/\\\"}"
done
mArgs=${args[@]:0}
osascript <<EOF
--Terminal opens a window by default when it is not running, so check
on applicationIsRunning(applicationName)
......@@ -14,7 +21,7 @@ osascript <<EOF
end applicationIsRunning
set terminalWasRunning to applicationIsRunning("Terminal")
set cdScript to "cd $i"
set cdScript to "$mArgs"
tell application "Terminal"
--do script will open a new window if none given, but terminal already opens one if not running
if terminalWasRunning then
......
......@@ -29,6 +29,8 @@
#include "consoleprocess_p.h"
#include <utils/hostosinfo.h>
#include <QSettings>
namespace Utils {
......
......@@ -62,16 +62,23 @@ public:
Environment environment() const;
bool start(const QString &program, const QString &args);
public slots:
void stop();
public:
void setMode(Mode m);
Mode mode() const;
bool isRunning() const; // This reflects the state of the console+stub
qint64 applicationPID() const;
void killProcess();
void killStub();
#ifdef Q_OS_WIN
qint64 applicationMainThreadID() const;
#else
void detachStub();
#endif
int exitCode() const;
......@@ -99,8 +106,8 @@ signals:
void processStopped();
// These reflect the state of the console+stub
void wrapperStarted();
void wrapperStopped();
void stubStarted();
void stubStopped();
private slots:
void stubConnectionAvailable();
......
......@@ -37,6 +37,10 @@
#include <QLocalSocket>
#include <QLocalServer>
QT_BEGIN_NAMESPACE
class QTimer;
QT_END_NAMESPACE
#ifdef Q_OS_WIN
# if QT_VERSION >= 0x050000
# include <QWinEventNotifier>
......@@ -51,6 +55,7 @@ namespace Utils {
struct ConsoleProcessPrivate {
ConsoleProcessPrivate();
static QString m_defaultConsoleProcess;
ConsoleProcess::Mode m_mode;
QString m_workingDir;
Environment m_environment;
......@@ -66,6 +71,9 @@ struct ConsoleProcessPrivate {
QProcess m_process;
QByteArray m_stubServerDir;
QSettings *m_settings;
bool m_stubConnected;
qint64 m_stubPid;
QTimer *m_stubConnectTimer;
#else
qint64 m_appMainThreadId;
PROCESS_INFORMATION *m_pid;
......
......@@ -37,6 +37,7 @@
#include <QCoreApplication>
#include <QDir>
#include <QSettings>
#include <QTimer>
#include <sys/stat.h>
#include <sys/types.h>
......@@ -51,7 +52,10 @@ ConsoleProcessPrivate::ConsoleProcessPrivate() :
m_appPid(0),
m_stubSocket(0),
m_tempFile(0),
m_settings(0)
m_settings(0),
m_stubConnected(false),
m_stubPid(0),
m_stubConnectTimer(0)
{
}
......@@ -61,8 +65,6 @@ ConsoleProcess::ConsoleProcess(QObject *parent) :
connect(&d->m_stubServer, SIGNAL(newConnection()), SLOT(stubConnectionAvailable()));
d->m_process.setProcessChannelMode(QProcess::ForwardedChannels);
connect(&d->m_process, SIGNAL(finished(int,QProcess::ExitStatus)),
SLOT(stubExited()));
}
void ConsoleProcess::setSettings(QSettings *settings)
......@@ -159,21 +161,54 @@ bool ConsoleProcess::start(const QString &program, const QString &args)
d->m_tempFile = 0;
return false;
}
d->m_stubConnectTimer = new QTimer(this);
connect(d->m_stubConnectTimer, SIGNAL(timeout()), SLOT(stop()));
d->m_stubConnectTimer->setSingleShot(true);
d->m_stubConnectTimer->start(10000);
d->m_executable = program;
emit wrapperStarted();
return true;
}
void ConsoleProcess::stop()
void ConsoleProcess::killProcess()
{
if (!isRunning())
return;
stubServerShutdown();
if (d->m_stubSocket && d->m_stubSocket->isWritable()) {
d->m_stubSocket->write("k", 1);
d->m_stubSocket->flush();
}
d->m_appPid = 0;
d->m_process.terminate();
if (!d->m_process.waitForFinished(1000))
d->m_process.kill();
d->m_process.waitForFinished();
}
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.kill();
d->m_process.waitForFinished();
}
}
}
bool ConsoleProcess::isRunning() const
......@@ -210,7 +245,8 @@ QString ConsoleProcess::stubServerListen()
void ConsoleProcess::stubServerShutdown()
{
delete d->m_stubSocket;
if (d->m_stubSocket)
d->m_stubSocket->deleteLater(); // we might be called from the disconnected signal of m_stubSocket
d->m_stubSocket = 0;
if (d->m_stubServer.isListening()) {
d->m_stubServer.close();
......@@ -220,8 +256,15 @@ void ConsoleProcess::stubServerShutdown()
void ConsoleProcess::stubConnectionAvailable()
{
if (d->m_stubConnectTimer) {
delete d->m_stubConnectTimer;
d->m_stubConnectTimer = 0;
}
d->m_stubConnected = true;
emit stubStarted();
d->m_stubSocket = d->m_stubServer.nextPendingConnection();
connect(d->m_stubSocket, SIGNAL(readyRead()), SLOT(readStubOutput()));
connect(d->m_stubSocket, SIGNAL(disconnected()), SLOT(stubExited()));
}
static QString errorMsg(int code)
......@@ -238,11 +281,12 @@ void ConsoleProcess::readStubOutput()
emit processError(msgCannotChangeToWorkDir(workingDirectory(), errorMsg(out.mid(10).toInt())));
} else if (out.startsWith("err:exec ")) {
emit processError(msgCannotExecute(d->m_executable, errorMsg(out.mid(9).toInt())));
} else if (out.startsWith("pid ")) {
// Will not need it any more
} else if (out.startsWith("spid ")) {
delete d->m_tempFile;
d->m_tempFile = 0;
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 ")) {
......@@ -257,6 +301,7 @@ void ConsoleProcess::readStubOutput()
emit processStopped();
} else {
emit processError(msgUnexpectedOutput(out));
d->m_stubPid = 0;
d->m_process.terminate();
break;
}
......@@ -269,6 +314,7 @@ void ConsoleProcess::stubExited()
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 = 0;
if (d->m_appPid) {
......@@ -277,7 +323,7 @@ void ConsoleProcess::stubExited()
d->m_appPid = 0;
emit processStopped(); // Maybe it actually did not, but keep state consistent
}
emit wrapperStopped();
emit stubStopped();
}
struct Terminal {
......@@ -293,15 +339,18 @@ static const Terminal knownTerminals[] =
{"rxvt", "-e"},
{"urxvt", "-e"},
{"xfce4-terminal", "-x"},
{"konsole", "--nofork -e"},
{"konsole", "-e"},
{"gnome-terminal", "-x"}
};
QString ConsoleProcess::defaultTerminalEmulator()
{
if (Utils::HostOsInfo::isMacHost())
if (Utils::HostOsInfo::isMacHost()) {
QString termCmd = QCoreApplication::applicationDirPath() + QLatin1String("/../Resources/scripts/openTerminal.command");
if (QFile(termCmd).exists())
return termCmd.replace(QLatin1Char(' '), QLatin1String("\\ "));
return QLatin1String("/usr/X11/bin/xterm");
}
const Environment env = Environment::systemEnvironment();
const int terminalCount = int(sizeof(knownTerminals) / sizeof(knownTerminals[0]));
for (int i = 0; i < terminalCount; ++i) {
......@@ -317,9 +366,6 @@ QString ConsoleProcess::defaultTerminalEmulator()
QStringList ConsoleProcess::availableTerminalEmulators()
{
if (Utils::HostOsInfo::isMacHost())
return QStringList(defaultTerminalEmulator());
QStringList result;
const Environment env = Environment::systemEnvironment();
const int terminalCount = int(sizeof(knownTerminals) / sizeof(knownTerminals[0]));
......@@ -331,6 +377,8 @@ QStringList ConsoleProcess::availableTerminalEmulators()
result.push_back(terminal);
}
}
if (!result.contains(defaultTerminalEmulator()))
result.append(defaultTerminalEmulator());
result.sort();
return result;
}
......
......@@ -151,16 +151,20 @@ bool ConsoleProcess::start(const QString &program, const QString &args)
d->processFinishedNotifier = new QWinEventNotifier(d->m_pid->hProcess, this);
connect(d->processFinishedNotifier, SIGNAL(activated(HANDLE)), SLOT(stubExited()));
emit wrapperStarted();
return true;
}
void ConsoleProcess::stop()
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);
......@@ -168,6 +172,12 @@ void ConsoleProcess::stop()
}
}
void ConsoleProcess::stop()
{
killProcess();
killStub();
}
bool ConsoleProcess::isRunning() const
{
return d->m_pid != 0;
......@@ -192,6 +202,7 @@ void ConsoleProcess::stubServerShutdown()
void ConsoleProcess::stubConnectionAvailable()
{
emit stubStarted();
d->m_stubSocket = d->m_stubServer.nextPendingConnection();
connect(d->m_stubSocket, SIGNAL(readyRead()), SLOT(readStubOutput()));
}
......@@ -281,7 +292,7 @@ void ConsoleProcess::stubExited()
d->m_appCode = -1;
emit processStopped();
}
emit wrapperStopped();
emit stubStopped();
}
QStringList ConsoleProcess::fixWinEnvironment(const QStringList &env)
......
......@@ -45,6 +45,7 @@
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <assert.h>
/* For OpenBSD */
#ifndef EPROTO
......@@ -55,6 +56,10 @@ extern char **environ;
static int qtcFd;
static char *sleepMsg;
static int chldPipe[2];
static int isDebug;
static volatile int isDetached;
static volatile int chldPid;
static void __attribute__((noreturn)) doExit(int code)
{
......@@ -71,10 +76,10 @@ static void sendMsg(const char *msg, int num)
char pidStr[64];
pidStrLen = sprintf(pidStr, msg, num);
if ((ioRet = write(qtcFd, pidStr, pidStrLen)) != pidStrLen) {
if (!isDetached && (ioRet = write(qtcFd, pidStr, pidStrLen)) != pidStrLen) {
fprintf(stderr, "Cannot write to creator comm socket: %s\n",
(ioRet < 0) ? strerror(errno) : "short write");
doExit(3);
isDetached = 2;
}
}
......@@ -88,16 +93,88 @@ enum {
ArgExe
};
/* Handle sigchld */
static void sigchldHandler(int sig)
{
int chldStatus;
/* Currently we have only one child, so we exit in case of error. */
int waitRes;
(void)sig;
for (;;) {
waitRes = waitpid(-1, &chldStatus, WNOHANG);
if (!waitRes)
break;
if (waitRes < 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 (isDebug) {
/* 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);
if (isDetached == 2 && isDebug) {
/* qtcreator was not informed and died while debugging, killing the child */
kill(chldPid, SIGKILL);
}
} else if (WIFEXITED(chldStatus)) {
int errNo;
/* 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;
/* fallthrough */
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);
doExit(3);
}
}
sendMsg("exit %d\n", WEXITSTATUS(chldStatus));
doExit(0);
} else {
sendMsg("crash %d\n", WTERMSIG(chldStatus));
doExit(0);
}
}
}
/* syntax: $0 {"run"|"debug"} <pid-socket> <continuation-msg> <workdir> <env-file> <exe> <args...> */
/* exit codes: 0 = ok, 1 = invocation error, 3 = internal error */
int main(int argc, char *argv[])
{
int errNo;
int chldPid;
int chldStatus;
int chldPipe[2];
int errNo, hadInvalidCommand = 0;
char **env = 0;
struct sockaddr_un sau;
struct sigaction act;
memset(&act, 0, sizeof(act));
if (argc < ArgEnv) {
fprintf(stderr, "This is an internal helper of Qt Creator. Do not run it manually.\n");
......@@ -118,6 +195,11 @@ int main(int argc, char *argv[])
doExit(1);
}
isDebug = !strcmp(argv[ArgAction], "debug");
isDetached = 0;
sendMsg("spid %ld\n", (long)getpid());
if (*argv[ArgDir] && chdir(argv[ArgDir])) {
/* Only expected error: no such file or direcotry */
sendMsg("err:chdir %d\n", errno);
......@@ -154,10 +236,26 @@ int main(int argc, char *argv[])
}
/* Ignore SIGTTOU. Without this, calling tcsetpgrp() from a background
* process group (in which we will be, once as child and once as parent)
* generates the mentioned signal and stops the concerned process. */
signal(SIGTTOU, SIG_IGN);
/*
* set up the signal handlers
*/
{
/* Ignore SIGTTOU. Without this, calling tcsetpgrp() from a background
* process group (in which we will be, once as child and once as parent)
* generates the mentioned signal and stops the concerned process. */
act.sa_handler = SIG_IGN;
if (sigaction(SIGTTOU, &act, 0)) {
perror("sigaction SIGTTOU");
doExit(3);
}
/* Handle SIGCHLD to keep track of what the child does without blocking */
act.sa_handler = sigchldHandler;
if (sigaction(SIGCHLD, &act, 0)) {
perror("sigaction SIGCHLD");
doExit(3);
}
}
/* Create execution result notification pipe. */
if (pipe(chldPipe)) {
......@@ -175,6 +273,10 @@ int main(int argc, char *argv[])
case 0:
close(qtcFd);
/* Remove the SIGCHLD handler from the child */
act.sa_handler = SIG_DFL;
sigaction(SIGCHLD, &act, 0);
/* 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. */
......@@ -200,57 +302,51 @@ int main(int argc, char *argv[])
_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);
char buffer[100];
int nbytes;
nbytes = read(qtcFd, buffer, 100);
if (nbytes <= 0) {
if (nbytes < 0 && errno == EINTR)
continue;
if (!isDetached) {
isDetached = 2;
if (nbytes == 0)
fprintf(stderr, "Lost connection to QtCreator, detaching from it.\n");
else
perror("Lost connection to QtCreator, detaching from it");
}
#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;
break;
} else {
int i;
for (i = 0; i < nbytes; ++i) {
switch (buffer[i]) {
case 'k':
if (chldPid > 0) {
kill(chldPid, SIGTERM);
sleep(1);
kill(chldPid, SIGKILL);
}
break;
case 'd':
isDetached = 1;
break;
case 's':
exit(0);
default:
if (!hadInvalidCommand) {
fprintf(stderr, "Ignoring invalid commands from QtCreator.\n");
hadInvalidCommand = 1;
}
}