1 #include <QScrollBar>
2 #include <QMenu>
3 #include <QCompleter>
4 #include <QAction>
5 #include <QShortcut>
6 #include <QStringListModel>
7 #include <QTimer>
8 #include <QSettings>
9 #include <QDir>
10 #include <QUuid>
11 #include <iostream>
12 #include "core/Cutter.h"
13 #include "ConsoleWidget.h"
14 #include "ui_ConsoleWidget.h"
15 #include "common/Helpers.h"
16 #include "common/SvgIconEngine.h"
17 #include "WidgetShortcuts.h"
18 
19 #ifdef Q_OS_WIN
20 #include <io.h>
21 #define dup2 _dup2
22 #define dup _dup
23 #define fileno _fileno
24 #define fdopen _fdopen
25 #define PIPE_SIZE 65536 // Match Linux size
26 #define PIPE_NAME "\\\\.\\pipe\\cutteroutput-%1"
27 #else
28 #include <unistd.h>
29 #define PIPE_READ  (0)
30 #define PIPE_WRITE (1)
31 #define STDIN_PIPE_NAME "%1/cutter-stdin-%2"
32 #endif
33 
34 #define CONSOLE_R2_INPUT ("R2 Console")
35 #define CONSOLE_DEBUGEE_INPUT ("Debugee Input")
36 
37 static const int invalidHistoryPos = -1;
38 
39 static const char *consoleWrapSettingsKey = "console.wrap";
40 
ConsoleWidget(MainWindow * main)41 ConsoleWidget::ConsoleWidget(MainWindow *main) :
42     CutterDockWidget(main),
43     ui(new Ui::ConsoleWidget),
44     debugOutputEnabled(true),
45     maxHistoryEntries(100),
46     lastHistoryPosition(invalidHistoryPos),
47     completer(nullptr),
48     historyUpShortcut(nullptr),
49     historyDownShortcut(nullptr)
50 {
51     ui->setupUi(this);
52 
53     // Adjust console lineedit
54     ui->r2InputLineEdit->setTextMargins(10, 0, 0, 0);
55     ui->debugeeInputLineEdit->setTextMargins(10, 0, 0, 0);
56 
57     setupFont();
58 
59     // Adjust text margins of consoleOutputTextEdit
60     QTextDocument *console_docu = ui->outputTextEdit->document();
61     console_docu->setDocumentMargin(10);
62 
63     // Ctrl+` and ';' to toggle console widget
64     QAction *toggleConsole = toggleViewAction();
65     QList<QKeySequence> toggleShortcuts;
66     toggleShortcuts << widgetShortcuts["ConsoleWidget"] << widgetShortcuts["ConsoleWidgetAlternative"];
67     toggleConsole->setShortcuts(toggleShortcuts);
68     connect(toggleConsole, &QAction::triggered, this, [this, toggleConsole](){
69         if (toggleConsole->isChecked()) {
70             widgetToFocusOnRaise()->setFocus();
71         }
72     });
73 
74     QAction *actionClear = new QAction(tr("Clear Output"), this);
75     connect(actionClear, &QAction::triggered, ui->outputTextEdit, &QPlainTextEdit::clear);
76     addAction(actionClear);
77 
78     // Ctrl+l to clear the output
79     actionClear->setShortcut(Qt::CTRL + Qt::Key_L);
80     actionClear->setShortcutContext(Qt::WidgetWithChildrenShortcut);
81     actions.append(actionClear);
82 
83     actionWrapLines = new QAction(tr("Wrap Lines"), ui->outputTextEdit);
84     actionWrapLines->setCheckable(true);
85     setWrap(QSettings().value(consoleWrapSettingsKey, true).toBool());
86     connect(actionWrapLines, &QAction::triggered, this, [this] (bool checked) {
87         setWrap(checked);
88     });
89     actions.append(actionWrapLines);
90 
91     // Completion
92     completionActive = false;
93     completer = new QCompleter(&completionModel, this);
94     completer->setMaxVisibleItems(20);
95     completer->setCaseSensitivity(Qt::CaseInsensitive);
96     completer->setFilterMode(Qt::MatchStartsWith);
97     ui->r2InputLineEdit->setCompleter(completer);
98 
99     connect(ui->r2InputLineEdit, &QLineEdit::textEdited, this, &ConsoleWidget::updateCompletion);
100     updateCompletion();
101 
102     // Set console output context menu
103     ui->outputTextEdit->setContextMenuPolicy(Qt::CustomContextMenu);
104     connect(ui->outputTextEdit, &QWidget::customContextMenuRequested,
105             this, &ConsoleWidget::showCustomContextMenu);
106 
107     // Esc clears r2InputLineEdit and debugeeInputLineEdit (like OmniBar)
108     QShortcut *r2_clear_shortcut = new QShortcut(QKeySequence(Qt::Key_Escape), ui->r2InputLineEdit);
109     connect(r2_clear_shortcut, &QShortcut::activated, this, &ConsoleWidget::clear);
110     r2_clear_shortcut->setContext(Qt::WidgetShortcut);
111 
112     QShortcut *debugee_clear_shortcut = new QShortcut(QKeySequence(Qt::Key_Escape), ui->debugeeInputLineEdit);
113     connect(debugee_clear_shortcut, &QShortcut::activated, this, &ConsoleWidget::clear);
114     debugee_clear_shortcut->setContext(Qt::WidgetShortcut);
115 
116     // Up and down arrows show history
117     historyUpShortcut = new QShortcut(QKeySequence(Qt::Key_Up), ui->r2InputLineEdit);
118     connect(historyUpShortcut, &QShortcut::activated, this, &ConsoleWidget::historyPrev);
119     historyUpShortcut->setContext(Qt::WidgetShortcut);
120 
121     historyDownShortcut = new QShortcut(QKeySequence(Qt::Key_Down), ui->r2InputLineEdit);
122     connect(historyDownShortcut, &QShortcut::activated, this, &ConsoleWidget::historyNext);
123     historyDownShortcut->setContext(Qt::WidgetShortcut);
124 
125     QShortcut *completionShortcut = new QShortcut(QKeySequence(Qt::Key_Tab), ui->r2InputLineEdit);
126     connect(completionShortcut, &QShortcut::activated, this, &ConsoleWidget::triggerCompletion);
127 
128     connect(ui->r2InputLineEdit, &QLineEdit::editingFinished, this, &ConsoleWidget::disableCompletion);
129 
130     connect(Config(), &Configuration::fontsUpdated, this, &ConsoleWidget::setupFont);
131 
132     connect(ui->inputCombo,
133             static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
134             this, &ConsoleWidget::onIndexChange);
135 
136     connect(Core(), &CutterCore::debugTaskStateChanged, this, [ = ]() {
137         if (Core()->isRedirectableDebugee()) {
138             ui->inputCombo->setVisible(true);
139         } else {
140             ui->inputCombo->setVisible(false);
141             // Return to the r2 console
142             ui->inputCombo->setCurrentIndex(ui->inputCombo->findText(CONSOLE_R2_INPUT));
143         }
144     });
145 
146     completer->popup()->installEventFilter(this);
147 
148     if (Config()->getOutputRedirectionEnabled()) {
149         redirectOutput();
150     }
151 }
152 
~ConsoleWidget()153 ConsoleWidget::~ConsoleWidget()
154 {
155 #ifndef Q_OS_WIN
156     ::close(stdinFile);
157     remove(stdinFifoPath.toStdString().c_str());
158 #endif
159 }
160 
eventFilter(QObject * obj,QEvent * event)161 bool ConsoleWidget::eventFilter(QObject *obj, QEvent *event)
162 {
163     if(completer && obj == completer->popup() &&
164         // disable up/down shortcuts if completer is shown
165         (event->type() == QEvent::Type::Show || event->type() == QEvent::Type::Hide)) {
166         bool enabled = !completer->popup()->isVisible();
167         if (historyUpShortcut) {
168             historyUpShortcut->setEnabled(enabled);
169         }
170         if (historyDownShortcut) {
171             historyDownShortcut->setEnabled(enabled);
172         }
173     }
174     return false;
175 }
176 
widgetToFocusOnRaise()177 QWidget *ConsoleWidget::widgetToFocusOnRaise()
178 {
179     return ui->r2InputLineEdit;
180 }
181 
setupFont()182 void ConsoleWidget::setupFont()
183 {
184     ui->outputTextEdit->setFont(Config()->getFont());
185 }
186 
addOutput(const QString & msg)187 void ConsoleWidget::addOutput(const QString &msg)
188 {
189     ui->outputTextEdit->appendPlainText(msg);
190     scrollOutputToEnd();
191 }
192 
addDebugOutput(const QString & msg)193 void ConsoleWidget::addDebugOutput(const QString &msg)
194 {
195     if (debugOutputEnabled) {
196         ui->outputTextEdit->appendHtml("<font color=\"red\"> [DEBUG]:\t" + msg + "</font>");
197         scrollOutputToEnd();
198     }
199 }
200 
focusInputLineEdit()201 void ConsoleWidget::focusInputLineEdit()
202 {
203     ui->r2InputLineEdit->setFocus();
204 }
205 
removeLastLine()206 void ConsoleWidget::removeLastLine()
207 {
208     ui->outputTextEdit->setFocus();
209     QTextCursor cur = ui->outputTextEdit->textCursor();
210     ui->outputTextEdit->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor);
211     ui->outputTextEdit->moveCursor(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
212     ui->outputTextEdit->moveCursor(QTextCursor::End, QTextCursor::KeepAnchor);
213     ui->outputTextEdit->textCursor().removeSelectedText();
214     ui->outputTextEdit->textCursor().deletePreviousChar();
215     ui->outputTextEdit->setTextCursor(cur);
216 }
217 
executeCommand(const QString & command)218 void ConsoleWidget::executeCommand(const QString &command)
219 {
220     if (!commandTask.isNull()) {
221         return;
222     }
223     ui->r2InputLineEdit->setEnabled(false);
224 
225     QString cmd_line = "[" + RAddressString(Core()->getOffset()) + "]> " + command;
226     addOutput(cmd_line);
227 
228     RVA oldOffset = Core()->getOffset();
229     commandTask = QSharedPointer<CommandTask>(new CommandTask(command, CommandTask::ColorMode::MODE_256, true));
230     connect(commandTask.data(), &CommandTask::finished, this, [this, cmd_line,
231           command, oldOffset] (const QString & result) {
232 
233         ui->outputTextEdit->appendHtml(result);
234         scrollOutputToEnd();
235         historyAdd(command);
236         commandTask.clear();
237         ui->r2InputLineEdit->setEnabled(true);
238         ui->r2InputLineEdit->setFocus();
239 
240         if (oldOffset != Core()->getOffset()) {
241             Core()->updateSeek();
242         }
243     });
244 
245     Core()->getAsyncTaskManager()->start(commandTask);
246 }
247 
sendToStdin(const QString & input)248 void ConsoleWidget::sendToStdin(const QString &input)
249 {
250 #ifndef Q_OS_WIN
251     write(stdinFile, (input + "\n").toStdString().c_str(), input.size() + 1);
252     fsync(stdinFile);
253     addOutput("Sent input: '" + input + "'");
254 #else
255     // Stdin redirection isn't currently available in windows because console applications
256     // with stdin already get their own console window with stdin when they are launched
257     // that the user can type into.
258     addOutput("Unsupported feature");
259 #endif
260 }
261 
onIndexChange()262 void ConsoleWidget::onIndexChange()
263 {
264     QString console = ui->inputCombo->currentText();
265     if (console == CONSOLE_DEBUGEE_INPUT) {
266         ui->r2InputLineEdit->setVisible(false);
267         ui->debugeeInputLineEdit->setVisible(true);
268     } else if (console == CONSOLE_R2_INPUT) {
269         ui->r2InputLineEdit->setVisible(true);
270         ui->debugeeInputLineEdit->setVisible(false);
271     }
272 }
273 
setWrap(bool wrap)274 void ConsoleWidget::setWrap(bool wrap)
275 {
276     QSettings().setValue(consoleWrapSettingsKey, wrap);
277     actionWrapLines->setChecked(wrap);
278     ui->outputTextEdit->setLineWrapMode(wrap ? QPlainTextEdit::WidgetWidth: QPlainTextEdit::NoWrap);
279 }
280 
on_r2InputLineEdit_returnPressed()281 void ConsoleWidget::on_r2InputLineEdit_returnPressed()
282 {
283     QString input = ui->r2InputLineEdit->text();
284     if (input.isEmpty()) {
285         return;
286     }
287     executeCommand(input);
288     ui->r2InputLineEdit->clear();
289 }
290 
on_debugeeInputLineEdit_returnPressed()291 void ConsoleWidget::on_debugeeInputLineEdit_returnPressed()
292 {
293     QString input = ui->debugeeInputLineEdit->text();
294     if (input.isEmpty()) {
295         return;
296     }
297     sendToStdin(input);
298     ui->debugeeInputLineEdit->clear();
299 }
300 
on_execButton_clicked()301 void ConsoleWidget::on_execButton_clicked()
302 {
303     on_r2InputLineEdit_returnPressed();
304 }
305 
showCustomContextMenu(const QPoint & pt)306 void ConsoleWidget::showCustomContextMenu(const QPoint &pt)
307 {
308     actionWrapLines->setChecked(ui->outputTextEdit->lineWrapMode() == QPlainTextEdit::WidgetWidth);
309 
310     QMenu *menu = new QMenu(ui->outputTextEdit);
311     menu->addActions(actions);
312     menu->exec(ui->outputTextEdit->mapToGlobal(pt));
313     menu->deleteLater();
314 }
315 
historyNext()316 void ConsoleWidget::historyNext()
317 {
318     if (!history.isEmpty()) {
319         if (lastHistoryPosition > invalidHistoryPos) {
320             if (lastHistoryPosition >= history.size()) {
321                 lastHistoryPosition = history.size() - 1 ;
322             }
323 
324             --lastHistoryPosition;
325 
326             if (lastHistoryPosition >= 0) {
327                 ui->r2InputLineEdit->setText(history.at(lastHistoryPosition));
328             } else {
329                 ui->r2InputLineEdit->clear();
330             }
331 
332 
333         }
334     }
335 }
336 
historyPrev()337 void ConsoleWidget::historyPrev()
338 {
339     if (!history.isEmpty()) {
340         if (lastHistoryPosition >= history.size() - 1) {
341             lastHistoryPosition = history.size() - 2;
342         }
343 
344         ui->r2InputLineEdit->setText(history.at(++lastHistoryPosition));
345     }
346 }
347 
triggerCompletion()348 void ConsoleWidget::triggerCompletion()
349 {
350     if (completionActive) {
351         return;
352     }
353     completionActive = true;
354     updateCompletion();
355     completer->complete();
356 }
357 
disableCompletion()358 void ConsoleWidget::disableCompletion()
359 {
360     if (!completionActive) {
361         return;
362     }
363     completionActive = false;
364     updateCompletion();
365     completer->popup()->hide();
366 }
367 
updateCompletion()368 void ConsoleWidget::updateCompletion()
369 {
370     if (!completionActive) {
371         completionModel.setStringList({});
372         return;
373     }
374 
375     auto current = ui->r2InputLineEdit->text();
376     auto completions = Core()->autocomplete(current, R_LINE_PROMPT_DEFAULT);
377     int lastSpace = current.lastIndexOf(' ');
378     if (lastSpace >= 0) {
379         current = current.left(lastSpace + 1);
380         for (auto &s : completions) {
381             s = current + s;
382         }
383     }
384     completionModel.setStringList(completions);
385 }
386 
clear()387 void ConsoleWidget::clear()
388 {
389     disableCompletion();
390     ui->r2InputLineEdit->clear();
391     ui->debugeeInputLineEdit->clear();
392 
393     invalidateHistoryPosition();
394 
395     // Close the potential shown completer popup
396     ui->r2InputLineEdit->clearFocus();
397     ui->r2InputLineEdit->setFocus();
398 }
399 
scrollOutputToEnd()400 void ConsoleWidget::scrollOutputToEnd()
401 {
402     const int maxValue = ui->outputTextEdit->verticalScrollBar()->maximum();
403     ui->outputTextEdit->verticalScrollBar()->setValue(maxValue);
404 }
405 
historyAdd(const QString & input)406 void ConsoleWidget::historyAdd(const QString &input)
407 {
408     if (history.size() + 1 > maxHistoryEntries) {
409         history.removeLast();
410     }
411 
412     history.prepend(input);
413 
414     invalidateHistoryPosition();
415 }
invalidateHistoryPosition()416 void ConsoleWidget::invalidateHistoryPosition()
417 {
418     lastHistoryPosition = invalidHistoryPos;
419 }
420 
processQueuedOutput()421 void ConsoleWidget::processQueuedOutput()
422 {
423     // Partial lines are ignored since carriage return is currently unsupported
424     while (pipeSocket->canReadLine()) {
425         QString output = QString(pipeSocket->readLine());
426 
427         fprintf(origStderr, "%s", output.toStdString().c_str());
428 
429         // Get the last segment that wasn't overwritten by carriage return
430         output = output.trimmed();
431         output = output.remove(0, output.lastIndexOf('\r')).trimmed();
432         ui->outputTextEdit->appendHtml(CutterCore::ansiEscapeToHtml(output));
433         scrollOutputToEnd();
434     }
435 }
436 
redirectOutput()437 void ConsoleWidget::redirectOutput()
438 {
439     // Make sure that we are running in a valid console with initialized output handles
440     if (0 > fileno(stderr) && 0 > fileno(stdout)) {
441         addOutput("Run cutter in a console to enable r2 output redirection into this widget.");
442         return;
443     }
444 
445     pipeSocket = new QLocalSocket(this);
446 
447     origStdin = fdopen(dup(fileno(stderr)), "r");
448     origStderr = fdopen(dup(fileno(stderr)), "a");
449     origStdout = fdopen(dup(fileno(stdout)), "a");
450 #ifdef Q_OS_WIN
451     QString pipeName = QString::fromLatin1(PIPE_NAME).arg(QUuid::createUuid().toString());
452 
453     SECURITY_ATTRIBUTES attributes = {sizeof(SECURITY_ATTRIBUTES), 0, false};
454     hWrite = CreateNamedPipeW((wchar_t *)pipeName.utf16(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
455                               PIPE_TYPE_BYTE | PIPE_WAIT, 1, PIPE_SIZE, PIPE_SIZE, 0, &attributes);
456 
457     int writeFd = _open_osfhandle((intptr_t)hWrite, _O_WRONLY | _O_TEXT);
458     dup2(writeFd, fileno(stdout));
459     dup2(writeFd, fileno(stderr));
460 
461     pipeSocket->connectToServer(pipeName, QIODevice::ReadOnly);
462 #else
463     pipe(redirectPipeFds);
464     stdinFifoPath = QString(STDIN_PIPE_NAME).arg(QDir::tempPath(), QUuid::createUuid().toString());
465     mkfifo(stdinFifoPath.toStdString().c_str(), (mode_t) 0777);
466     stdinFile = open(stdinFifoPath.toStdString().c_str(), O_RDWR | O_ASYNC);
467 
468     dup2(stdinFile, fileno(stdin));
469     dup2(redirectPipeFds[PIPE_WRITE], fileno(stderr));
470     dup2(redirectPipeFds[PIPE_WRITE], fileno(stdout));
471 
472     // Attempt to force line buffering to avoid calling processQueuedOutput
473     // for partial lines
474     setlinebuf(stderr);
475     setlinebuf(stdout);
476 
477     // Configure the pipe to work in async mode
478     fcntl(redirectPipeFds[PIPE_READ], F_SETFL, O_ASYNC | O_NONBLOCK);
479 
480     pipeSocket->setSocketDescriptor(redirectPipeFds[PIPE_READ]);
481     pipeSocket->connectToServer(QIODevice::ReadOnly);
482 #endif
483 
484     connect(pipeSocket, &QIODevice::readyRead, this, &ConsoleWidget::processQueuedOutput);
485 }
486