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