From 235761941f1018eac4f43df25c98e55d53524876 Mon Sep 17 00:00:00 2001 From: yossizap Date: Mon, 14 Oct 2019 10:00:44 +0000 Subject: [PATCH] Redirect stdout/stderr to Console Widget (#1821) * Added stdout/stderr redirection to the console --- src/Main.cpp | 44 ++++++++++++++++ src/widgets/ConsoleWidget.cpp | 96 +++++++++++++++++++++++++++++------ src/widgets/ConsoleWidget.h | 24 +++++++++ 3 files changed, 149 insertions(+), 15 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 0ed113af..11f5828e 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -121,6 +121,46 @@ static void migrateThemes() } } +/** + * @brief Attempt to connect to a parent console and configure outputs. + * + * @note Doesn't do anything if the exe wasn't executed from a console. + */ +#ifdef Q_OS_WIN +static void connectToConsole() +{ + if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + return; + } + + // Avoid reconfiguring stderr/stdout if one of them is already connected to a stream. + // This can happen when running with stdout/stderr redirected to a file. + if (0 > fileno(stdout)) { + // Overwrite FD 1 and 2 for the benefit of any code that uses the FDs + // directly. This is safe because the CRT allocates FDs 0, 1 and + // 2 at startup even if they don't have valid underlying Windows + // handles. This means we won't be overwriting an FD created by + // _open() after startup. + _close(1); + + if (freopen("CONOUT$", "a+", stdout)) { + // Avoid buffering stdout/stderr since IOLBF is replaced by IOFBF in Win32. + setvbuf(stdout, nullptr, _IONBF, 0); + } + } + if (0 > fileno(stderr)) { + _close(2); + + if (freopen("CONOUT$", "a+", stderr)) { + setvbuf(stderr, nullptr, _IONBF, 0); + } + } + + // Fix all cout, wcout, cin, wcin, cerr, wcerr, clog and wclog. + std::ios::sync_with_stdio(); +} +#endif + int main(int argc, char *argv[]) { @@ -133,6 +173,10 @@ int main(int argc, char *argv[]) initCrashHandler(); +#ifdef Q_OS_WIN + connectToConsole(); +#endif + qRegisterMetaType>(); qRegisterMetaType>(); diff --git a/src/widgets/ConsoleWidget.cpp b/src/widgets/ConsoleWidget.cpp index 9cf29669..972b4ec6 100644 --- a/src/widgets/ConsoleWidget.cpp +++ b/src/widgets/ConsoleWidget.cpp @@ -13,6 +13,20 @@ #include "common/Helpers.h" #include "common/SvgIconEngine.h" +#ifdef Q_OS_WIN +#include +#include +#define dup2 _dup2 +#define dup _dup +#define fileno _fileno +#define fdopen _fdopen +#define PIPE_SIZE 65536 // Match Linux size +#define PIPE_NAME "\\\\.\\pipe\\cutteroutput-%1" +#else +#include +#define PIPE_READ (0) +#define PIPE_WRITE (1) +#endif static const int invalidHistoryPos = -1; @@ -90,6 +104,8 @@ ConsoleWidget::ConsoleWidget(MainWindow *main, QAction *action) : connect(Config(), &Configuration::interfaceThemeChanged, this, &ConsoleWidget::setupFont); completer->popup()->installEventFilter(this); + + redirectOutput(); } ConsoleWidget::~ConsoleWidget() @@ -156,36 +172,26 @@ void ConsoleWidget::executeCommand(const QString &command) } ui->inputLineEdit->setEnabled(false); - const int originalLines = ui->outputTextEdit->blockCount(); - QTimer *timer = new QTimer(this); - timer->setInterval(500); - timer->setSingleShot(true); - connect(timer, &QTimer::timeout, [this]() { - ui->outputTextEdit->appendPlainText("Executing the command..."); - }); + QString cmd_line = "[" + RAddressString(Core()->getOffset()) + "]> " + command; + addOutput(cmd_line); - QString cmd_line = "
[" + RAddressString(Core()->getOffset()) + "]> " + command + "
"; RVA oldOffset = Core()->getOffset(); commandTask = QSharedPointer(new CommandTask(command, CommandTask::ColorMode::MODE_256, true)); connect(commandTask.data(), &CommandTask::finished, this, [this, cmd_line, - command, originalLines, oldOffset] (const QString & result) { + command, oldOffset] (const QString & result) { - if (originalLines < ui->outputTextEdit->blockCount()) { - removeLastLine(); - } - ui->outputTextEdit->appendHtml(cmd_line + result); + ui->outputTextEdit->appendHtml(result); scrollOutputToEnd(); historyAdd(command); commandTask.clear(); ui->inputLineEdit->setEnabled(true); ui->inputLineEdit->setFocus(); + if (oldOffset != Core()->getOffset()) { Core()->updateSeek(); } }); - connect(commandTask.data(), &CommandTask::finished, timer, &QTimer::stop); - timer->start(); Core()->getAsyncTaskManager()->start(commandTask); } @@ -324,3 +330,63 @@ void ConsoleWidget::invalidateHistoryPosition() { lastHistoryPosition = invalidHistoryPos; } + +void ConsoleWidget::processQueuedOutput() +{ + // Partial lines are ignored since carriage return is currently unsupported + while (pipeSocket->canReadLine()) { + QString output = QString(pipeSocket->readLine()); + + fprintf(origStderr, "%s", output.toStdString().c_str()); + + // Get the last segment that wasn't overwritten by carriage return + output = output.trimmed(); + output = output.remove(0, output.lastIndexOf('\r')).trimmed(); + ui->outputTextEdit->appendHtml(CutterCore::ansiEscapeToHtml(output)); + scrollOutputToEnd(); + } +} + +void ConsoleWidget::redirectOutput() +{ + // Make sure that we are running in a valid console with initialized output handles + if (0 > fileno(stderr) && 0 > fileno(stdout)) { + addOutput("Run cutter in a console to enable r2 output redirection into this widget."); + return; + } + + pipeSocket = new QLocalSocket(this); + + origStderr = fdopen(dup(fileno(stderr)), "a"); + origStdout = fdopen(dup(fileno(stdout)), "a"); +#ifdef Q_OS_WIN + QString pipeName = QString::fromLatin1(PIPE_NAME).arg(QUuid::createUuid().toString()); + + SECURITY_ATTRIBUTES attributes = {sizeof(SECURITY_ATTRIBUTES), 0, false}; + hWrite = CreateNamedPipeW((wchar_t*)pipeName.utf16(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_WAIT, 1, PIPE_SIZE, PIPE_SIZE, 0, &attributes); + + int writeFd = _open_osfhandle((intptr_t)hWrite, _O_WRONLY | _O_TEXT); + dup2(writeFd, fileno(stdout)); + dup2(writeFd, fileno(stderr)); + + pipeSocket->connectToServer(pipeName, QIODevice::ReadOnly); +#else + pipe(redirectPipeFds); + dup2(redirectPipeFds[PIPE_WRITE], fileno(stderr)); + dup2(redirectPipeFds[PIPE_WRITE], fileno(stdout)); + + // Attempt to force line buffering to avoid calling processQueuedOutput + // for partial lines + setlinebuf(stderr); + setlinebuf(stdout); + + // Configure the pipe to work in async mode + fcntl(redirectPipeFds[PIPE_READ], F_SETFL, O_ASYNC | O_NONBLOCK); + + pipeSocket->setSocketDescriptor(redirectPipeFds[PIPE_READ]); + pipeSocket->connectToServer(QIODevice::ReadOnly); +#endif + + connect(pipeSocket, SIGNAL(readyRead()), this, SLOT(processQueuedOutput())); +} diff --git a/src/widgets/ConsoleWidget.h b/src/widgets/ConsoleWidget.h index 8297fbe2..0b61f531 100644 --- a/src/widgets/ConsoleWidget.h +++ b/src/widgets/ConsoleWidget.h @@ -6,6 +6,8 @@ #include "common/CommandTask.h" #include +#include +#include #include @@ -62,6 +64,11 @@ private slots: void clear(); + /** + * @brief Passes redirected output from the pipe to the terminal and console + */ + void processQueuedOutput(); + private: void scrollOutputToEnd(); void historyAdd(const QString &input); @@ -70,6 +77,12 @@ private: void executeCommand(const QString &command); void setWrap(bool wrap); + /** + * @brief Redirects stderr and stdout to the output pipe which is handled by + * processQueuedOutput + */ + void redirectOutput(); + QSharedPointer commandTask; std::unique_ptr ui; @@ -84,6 +97,17 @@ private: QCompleter *completer; QShortcut *historyUpShortcut; QShortcut *historyDownShortcut; + FILE *origStderr; + FILE *origStdout; + QLocalSocket *pipeSocket; +#ifdef Q_OS_WIN + HANDLE hRead; + HANDLE hWrite; +#else + int redirectPipeFds[2]; + QVector *redirectionBuffer; + QSocketNotifier *outputNotifier; +#endif }; #endif // CONSOLEWIDGET_H