1/*
2 This file is part of Konsole
3
4 Copyright 2006-2008 by Robert Knight <robertknight@gmail.com>
5 Copyright 1997,1998 by Lars Doelle <lars.doelle@on-line.de>
6 Copyright 2009 by Thomas Dreibholz <dreibh@iem.uni-due.de>
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 02110-1301 USA.
22*/
23
24// Own
25#include "Session.h"
26
27// Standard
28#include <stdlib.h>
29#include <signal.h>
30#include <unistd.h>
31
32// Qt
33#include <QApplication>
34#include <QtGui/QColor>
35#include <QtCore/QDir>
36#include <QtCore/QFile>
37#include <QtCore/QStringList>
38#include <QtDBus/QtDBus>
39
40// KDE
41#include <KDebug>
42#include <KLocalizedString>
43#include <KNotification>
44#include <KRun>
45#include <KShell>
46#include <KProcess>
47#include <KStandardDirs>
48#include <KConfigGroup>
49
50// Konsole
51#include <sessionadaptor.h>
52
53#include "ProcessInfo.h"
54#include "Pty.h"
55#include "TerminalDisplay.h"
56#include "ShellCommand.h"
57#include "Vt102Emulation.h"
58#include "ZModemDialog.h"
59#include "History.h"
60
61using namespace Konsole;
62
63int Session::lastSessionId = 0;
64
65// HACK This is copied out of QUuid::createUuid with reseeding forced.
66// Required because color schemes repeatedly seed the RNG...
67// ...with a constant.
68QUuid createUuid()
69{
70 static const int intbits = sizeof(int) * 8;
71 static int randbits = 0;
72 if (!randbits) {
73 int max = RAND_MAX;
74 do {
75 ++randbits;
76 } while ((max = max >> 1));
77 }
78
79 qsrand(uint(QDateTime::currentDateTime().toTime_t()));
80 qrand(); // Skip first
81
82 QUuid result;
83 uint* data = &(result.data1);
84 int chunks = 16 / sizeof(uint);
85 while (chunks--) {
86 uint randNumber = 0;
87 for (int filled = 0; filled < intbits; filled += randbits)
88 randNumber |= qrand() << filled;
89 *(data + chunks) = randNumber;
90 }
91
92 result.data4[0] = (result.data4[0] & 0x3F) | 0x80; // UV_DCE
93 result.data3 = (result.data3 & 0x0FFF) | 0x4000; // UV_Random
94
95 return result;
96}
97
98Session::Session(QObject* parent) :
99 QObject(parent)
100 , _shellProcess(0)
101 , _emulation(0)
102 , _monitorActivity(false)
103 , _monitorSilence(false)
104 , _notifiedActivity(false)
105 , _silenceSeconds(10)
106 , _autoClose(true)
107 , _closePerUserRequest(false)
108 , _addToUtmp(true)
109 , _flowControlEnabled(true)
110 , _sessionId(0)
111 , _sessionProcessInfo(0)
112 , _foregroundProcessInfo(0)
113 , _foregroundPid(0)
114 , _zmodemBusy(false)
115 , _zmodemProc(0)
116 , _zmodemProgress(0)
117 , _hasDarkBackground(false)
118{
119 _uniqueIdentifier = createUuid();
120
121 //prepare DBus communication
122 new SessionAdaptor(this);
123 _sessionId = ++lastSessionId;
124 QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/") + QString::number(_sessionId), this);
125
126 //create emulation backend
127 _emulation = new Vt102Emulation();
128
129 connect(_emulation, SIGNAL(titleChanged(int,QString)),
130 this, SLOT(setUserTitle(int,QString)));
131 connect(_emulation, SIGNAL(stateSet(int)),
132 this, SLOT(activityStateSet(int)));
133 connect(_emulation, SIGNAL(zmodemDetected()),
134 this, SLOT(fireZModemDetected()));
135 connect(_emulation, SIGNAL(changeTabTextColorRequest(int)),
136 this, SIGNAL(changeTabTextColorRequest(int)));
137 connect(_emulation, SIGNAL(profileChangeCommandReceived(QString)),
138 this, SIGNAL(profileChangeCommandReceived(QString)));
139 connect(_emulation, SIGNAL(flowControlKeyPressed(bool)),
140 this, SLOT(updateFlowControlState(bool)));
141 connect(_emulation, SIGNAL(primaryScreenInUse(bool)),
142 this, SLOT(onPrimaryScreenInUse(bool)));
143 connect(_emulation, SIGNAL(selectionChanged(QString)),
144 this, SIGNAL(selectionChanged(QString)));
145 connect(_emulation, SIGNAL(imageResizeRequest(QSize)),
146 this, SIGNAL(resizeRequest(QSize)));
147
148 //create new teletype for I/O with shell process
149 openTeletype(-1);
150
151 //setup timer for monitoring session activity & silence
152 _silenceTimer = new QTimer(this);
153 _silenceTimer->setSingleShot(true);
154 connect(_silenceTimer, SIGNAL(timeout()), this, SLOT(silenceTimerDone()));
155
156 _activityTimer = new QTimer(this);
157 _activityTimer->setSingleShot(true);
158 connect(_activityTimer, SIGNAL(timeout()), this, SLOT(activityTimerDone()));
159}
160
161Session::~Session()
162{
163 delete _foregroundProcessInfo;
164 delete _sessionProcessInfo;
165 delete _emulation;
166 delete _shellProcess;
167 delete _zmodemProc;
168}
169
170void Session::openTeletype(int fd)
171{
172 if (isRunning()) {
173 kWarning() << "Attempted to open teletype in a running session.";
174 return;
175 }
176
177 delete _shellProcess;
178
179 if (fd < 0)
180 _shellProcess = new Pty();
181 else
182 _shellProcess = new Pty(fd);
183
184 _shellProcess->setUtf8Mode(_emulation->utf8());
185
186 // connect the I/O between emulator and pty process
187 connect(_shellProcess, SIGNAL(receivedData(const char*,int)),
188 this, SLOT(onReceiveBlock(const char*,int)));
189 connect(_emulation, SIGNAL(sendData(const char*,int)),
190 _shellProcess, SLOT(sendData(const char*,int)));
191
192 // UTF8 mode
193 connect(_emulation, SIGNAL(useUtf8Request(bool)),
194 _shellProcess, SLOT(setUtf8Mode(bool)));
195
196 // get notified when the pty process is finished
197 connect(_shellProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
198 this, SLOT(done(int,QProcess::ExitStatus)));
199
200 // emulator size
201 connect(_emulation, SIGNAL(imageSizeChanged(int,int)),
202 this, SLOT(updateWindowSize(int,int)));
203 connect(_emulation, SIGNAL(imageSizeInitialized()),
204 this, SLOT(run()));
205}
206
207WId Session::windowId() const
208{
209 // Returns a window ID for this session which is used
210 // to set the WINDOWID environment variable in the shell
211 // process.
212 //
213 // Sessions can have multiple views or no views, which means
214 // that a single ID is not always going to be accurate.
215 //
216 // If there are no views, the window ID is just 0. If
217 // there are multiple views, then the window ID for the
218 // top-level window which contains the first view is
219 // returned
220
221 if (_views.count() == 0) {
222 return 0;
223 } else {
224 QWidget* window = _views.first();
225
226 Q_ASSERT(window);
227
228 while (window->parentWidget() != 0)
229 window = window->parentWidget();
230
231 return window->winId();
232 }
233}
234
235void Session::setDarkBackground(bool darkBackground)
236{
237 _hasDarkBackground = darkBackground;
238}
239
240bool Session::isRunning() const
241{
242 return _shellProcess && (_shellProcess->state() == QProcess::Running);
243}
244
245void Session::setCodec(QTextCodec* codec)
246{
247 emulation()->setCodec(codec);
248}
249
250bool Session::setCodec(QByteArray name)
251{
252 QTextCodec* codec = QTextCodec::codecForName(name);
253
254 if (codec) {
255 setCodec(codec);
256 return true;
257 } else {
258 return false;
259 }
260}
261
262QByteArray Session::codec()
263{
264 return _emulation->codec()->name();
265}
266
267void Session::setProgram(const QString& program)
268{
269 _program = ShellCommand::expand(program);
270}
271
272void Session::setArguments(const QStringList& arguments)
273{
274 _arguments = ShellCommand::expand(arguments);
275}
276
277void Session::setInitialWorkingDirectory(const QString& dir)
278{
279 _initialWorkingDir = KShell::tildeExpand(ShellCommand::expand(dir));
280}
281
282QString Session::currentWorkingDirectory()
283{
284 // only returned cached value
285 if (_currentWorkingDir.isEmpty())
286 updateWorkingDirectory();
287
288 return _currentWorkingDir;
289}
290ProcessInfo* Session::updateWorkingDirectory()
291{
292 ProcessInfo* process = getProcessInfo();
293
294 const QString currentDir = process->validCurrentDir();
295 if (currentDir != _currentWorkingDir) {
296 _currentWorkingDir = currentDir;
297 emit currentDirectoryChanged(_currentWorkingDir);
298 }
299
300 return process;
301}
302
303QList<TerminalDisplay*> Session::views() const
304{
305 return _views;
306}
307
308void Session::addView(TerminalDisplay* widget)
309{
310 Q_ASSERT(!_views.contains(widget));
311
312 _views.append(widget);
313
314 // connect emulation - view signals and slots
315 connect(widget, SIGNAL(keyPressedSignal(QKeyEvent*)),
316 _emulation, SLOT(sendKeyEvent(QKeyEvent*)));
317 connect(widget, SIGNAL(mouseSignal(int,int,int,int)),
318 _emulation, SLOT(sendMouseEvent(int,int,int,int)));
319 connect(widget, SIGNAL(sendStringToEmu(const char*)),
320 _emulation, SLOT(sendString(const char*)));
321
322 // allow emulation to notify view when the foreground process
323 // indicates whether or not it is interested in mouse signals
324 connect(_emulation, SIGNAL(programUsesMouseChanged(bool)),
325 widget, SLOT(setUsesMouse(bool)));
326
327 widget->setUsesMouse(_emulation->programUsesMouse());
328
329 connect(_emulation, SIGNAL(programBracketedPasteModeChanged(bool)),
330 widget, SLOT(setBracketedPasteMode(bool)));
331
332 widget->setBracketedPasteMode(_emulation->programBracketedPasteMode());
333
334 widget->setScreenWindow(_emulation->createWindow());
335
336 //connect view signals and slots
337 connect(widget, SIGNAL(changedContentSizeSignal(int,int)),
338 this, SLOT(onViewSizeChange(int,int)));
339
340 connect(widget, SIGNAL(destroyed(QObject*)),
341 this, SLOT(viewDestroyed(QObject*)));
342}
343
344void Session::viewDestroyed(QObject* view)
345{
346 // the received QObject has already been destroyed, so using
347 // qobject_cast<> does not work here
348 TerminalDisplay* display = static_cast<TerminalDisplay*>(view);
349
350 Q_ASSERT(_views.contains(display));
351
352 removeView(display);
353}
354
355void Session::removeView(TerminalDisplay* widget)
356{
357 _views.removeAll(widget);
358
359 disconnect(widget, 0, this, 0);
360
361 // disconnect
362 // - key presses signals from widget
363 // - mouse activity signals from widget
364 // - string sending signals from widget
365 //
366 // ... and any other signals connected in addView()
367 disconnect(widget, 0, _emulation, 0);
368
369 // disconnect state change signals emitted by emulation
370 disconnect(_emulation, 0, widget, 0);
371
372 // close the session automatically when the last view is removed
373 if (_views.count() == 0) {
374 close();
375 }
376}
377
378// Upon a KPty error, there is no description on what that error was...
379// Check to see if the given program is executable.
380QString Session::checkProgram(const QString& program)
381{
382 QString exec = program;
383
384 if (exec.isEmpty())
385 return QString();
386
387 QFileInfo info(exec);
388 if (info.isAbsolute() && info.exists() && info.isExecutable()) {
389 return exec;
390 }
391
392 exec = KRun::binaryName(exec, false);
393 exec = KShell::tildeExpand(exec);
394 QString pexec = KStandardDirs::findExe(exec);
395 if (pexec.isEmpty()) {
396 kError() << i18n("Could not find binary: ") << exec;
397 return QString();
398 }
399
400 return exec;
401}
402
403void Session::terminalWarning(const QString& message)
404{
405 static const QByteArray warningText = i18nc("@info:shell Alert the user with red color text", "Warning: ").toLocal8Bit();
406 QByteArray messageText = message.toLocal8Bit();
407
408 static const char redPenOn[] = "\033[1m\033[31m";
409 static const char redPenOff[] = "\033[0m";
410
411 _emulation->receiveData(redPenOn, qstrlen(redPenOn));
412 _emulation->receiveData("\n\r\n\r", 4);
413 _emulation->receiveData(warningText.constData(), qstrlen(warningText.constData()));
414 _emulation->receiveData(messageText.constData(), qstrlen(messageText.constData()));
415 _emulation->receiveData("\n\r\n\r", 4);
416 _emulation->receiveData(redPenOff, qstrlen(redPenOff));
417}
418
419QString Session::shellSessionId() const
420{
421 QString friendlyUuid(_uniqueIdentifier.toString());
422 friendlyUuid.remove('-').remove('{').remove('}');
423
424 return friendlyUuid;
425}
426
427void Session::run()
428{
429 // extra safeguard for potential bug.
430 if (isRunning()) {
431 kWarning() << "Attempted to re-run an already running session.";
432 return;
433 }
434
435 //check that everything is in place to run the session
436 if (_program.isEmpty()) {
437 kWarning() << "Program to run not set.";
438 }
439 if (_arguments.isEmpty()) {
440 kWarning() << "No command line arguments specified.";
441 }
442 if (_uniqueIdentifier.isNull()) {
443 _uniqueIdentifier = createUuid();
444 }
445
446 const int CHOICE_COUNT = 3;
447 // if '_program' is empty , fall back to default shell. If that is not set
448 // then fall back to /bin/sh
449#ifndef _WIN32
450 QString programs[CHOICE_COUNT] = {_program, qgetenv("SHELL"), "/bin/sh"};
451#else
452 // on windows, fall back to %COMSPEC% or to cmd.exe
453 QString programs[CHOICE_COUNT] = {_program, qgetenv("COMSPEC"), "cmd.exe"};
454#endif
455 QString exec;
456 int choice = 0;
457 while (choice < CHOICE_COUNT) {
458 exec = checkProgram(programs[choice]);
459 if (exec.isEmpty())
460 choice++;
461 else
462 break;
463 }
464
465 // if a program was specified via setProgram(), but it couldn't be found, print a warning
466 if (choice != 0 && choice < CHOICE_COUNT && !_program.isEmpty()) {
467 terminalWarning(i18n("Could not find '%1', starting '%2' instead. Please check your profile settings.", _program, exec));
468 // if none of the choices are available, print a warning
469 } else if (choice == CHOICE_COUNT) {
470 terminalWarning(i18n("Could not find an interactive shell to start."));
471 return;
472 }
473
474 // if no arguments are specified, fall back to program name
475 QStringList arguments = _arguments.join(QChar(' ')).isEmpty() ?
476 QStringList() << exec :
477 _arguments;
478
479 if (!_initialWorkingDir.isEmpty()) {
480 _shellProcess->setInitialWorkingDirectory(_initialWorkingDir);
481 } else {
482 _shellProcess->setInitialWorkingDirectory(QDir::currentPath());
483 }
484
485 _shellProcess->setFlowControlEnabled(_flowControlEnabled);
486 _shellProcess->setEraseChar(_emulation->eraseChar());
487 _shellProcess->setUseUtmp(_addToUtmp);
488
489 // this is not strictly accurate use of the COLORFGBG variable. This does not
490 // tell the terminal exactly which colors are being used, but instead approximates
491 // the color scheme as "black on white" or "white on black" depending on whether
492 // the background color is deemed dark or not
493 const QString backgroundColorHint = _hasDarkBackground ? "COLORFGBG=15;0" : "COLORFGBG=0;15";
494 addEnvironmentEntry(backgroundColorHint);
495
496 addEnvironmentEntry(QString("SHELL_SESSION_ID=%1").arg(shellSessionId()));
497
498 addEnvironmentEntry(QString("WINDOWID=%1").arg(QString::number(windowId())));
499
500 const QString dbusService = QDBusConnection::sessionBus().baseService();
501 addEnvironmentEntry(QString("KONSOLE_DBUS_SERVICE=%1").arg(dbusService));
502
503 const QString dbusObject = QString("/Sessions/%1").arg(QString::number(_sessionId));
504 addEnvironmentEntry(QString("KONSOLE_DBUS_SESSION=%1").arg(dbusObject));
505
506 int result = _shellProcess->start(exec, arguments, _environment);
507 if (result < 0) {
508 terminalWarning(i18n("Could not start program '%1' with arguments '%2'.", exec, arguments.join(" ")));
509 terminalWarning(_shellProcess->errorString());
510 return;
511 }
512
513 _shellProcess->setWriteable(false); // We are reachable via kwrited.
514
515 emit started();
516}
517
518void Session::setUserTitle(int what, const QString& caption)
519{
520 //set to true if anything is actually changed (eg. old _nameTitle != new _nameTitle )
521 bool modified = false;
522
523 if ((what == IconNameAndWindowTitle) || (what == WindowTitle)) {
524 if (_userTitle != caption) {
525 _userTitle = caption;
526 modified = true;
527 }
528 }
529
530 if ((what == IconNameAndWindowTitle) || (what == IconName)) {
531 if (_iconText != caption) {
532 _iconText = caption;
533 modified = true;
534 }
535 }
536
537 if (what == TextColor || what == BackgroundColor) {
538 QString colorString = caption.section(';', 0, 0);
539 QColor color = QColor(colorString);
540 if (color.isValid()) {
541 if (what == TextColor)
542 emit changeForegroundColorRequest(color);
543 else
544 emit changeBackgroundColorRequest(color);
545 }
546 }
547
548 if (what == SessionName) {
549 if (_localTabTitleFormat != caption) {
550 _localTabTitleFormat = caption;
551 setTitle(Session::DisplayedTitleRole, caption);
552 modified = true;
553 }
554 }
555
556 /* I don't believe this has ever worked in KDE 4.x
557 if (what == 31) {
558 QString cwd = caption;
559 cwd = cwd.replace(QRegExp("^~"), QDir::homePath());
560 emit openUrlRequest(cwd);
561 }*/
562
563 /* The below use of 32 works but appears to non-standard.
564 It is from a commit from 2004 c20973eca8776f9b4f15bee5fdcb5a3205aa69de
565 */
566 // change icon via \033]32;Icon\007
567 if (what == SessionIcon) {
568 if (_iconName != caption) {
569 _iconName = caption;
570
571 modified = true;
572 }
573 }
574
575 if (what == ProfileChange) {
576 emit profileChangeCommandReceived(caption);
577 return;
578 }
579
580 if (modified)
581 emit titleChanged();
582}
583
584QString Session::userTitle() const
585{
586 return _userTitle;
587}
588void Session::setTabTitleFormat(TabTitleContext context , const QString& format)
589{
590 if (context == LocalTabTitle)
591 _localTabTitleFormat = format;
592 else if (context == RemoteTabTitle)
593 _remoteTabTitleFormat = format;
594}
595QString Session::tabTitleFormat(TabTitleContext context) const
596{
597 if (context == LocalTabTitle)
598 return _localTabTitleFormat;
599 else if (context == RemoteTabTitle)
600 return _remoteTabTitleFormat;
601
602 return QString();
603}
604
605void Session::silenceTimerDone()
606{
607 //FIXME: The idea here is that the notification popup will appear to tell the user than output from
608 //the terminal has stopped and the popup will disappear when the user activates the session.
609 //
610 //This breaks with the addition of multiple views of a session. The popup should disappear
611 //when any of the views of the session becomes active
612
613 //FIXME: Make message text for this notification and the activity notification more descriptive.
614 if (_monitorSilence) {
615 KNotification::event("Silence", i18n("Silence in session '%1'", _nameTitle), QPixmap(),
616 QApplication::activeWindow(),
617 KNotification::CloseWhenWidgetActivated);
618 emit stateChanged(NOTIFYSILENCE);
619 } else {
620 emit stateChanged(NOTIFYNORMAL);
621 }
622}
623
624void Session::activityTimerDone()
625{
626 _notifiedActivity = false;
627}
628
629void Session::updateFlowControlState(bool suspended)
630{
631 if (suspended) {
632 if (flowControlEnabled()) {
633 foreach(TerminalDisplay * display, _views) {
634 if (display->flowControlWarningEnabled())
635 display->outputSuspended(true);
636 }
637 }
638 } else {
639 foreach(TerminalDisplay * display, _views) {
640 display->outputSuspended(false);
641 }
642 }
643}
644
645void Session::onPrimaryScreenInUse(bool use)
646{
647 emit primaryScreenInUse(use);
648}
649
650void Session::activityStateSet(int state)
651{
652 // TODO: should this hardcoded interval be user configurable?
653 const int activityMaskInSeconds = 15;
654
655 if (state == NOTIFYBELL) {
656 emit bellRequest(i18n("Bell in session '%1'", _nameTitle));
657 } else if (state == NOTIFYACTIVITY) {
658 if (_monitorActivity && !_notifiedActivity) {
659 KNotification::event("Activity", i18n("Activity in session '%1'", _nameTitle), QPixmap(),
660 QApplication::activeWindow(),
661 KNotification::CloseWhenWidgetActivated);
662
663 // mask activity notification for a while to avoid flooding
664 _notifiedActivity = true;
665 _activityTimer->start(activityMaskInSeconds * 1000);
666 }
667
668 // reset the counter for monitoring continuous silence since there is activity
669 if (_monitorSilence) {
670 _silenceTimer->start(_silenceSeconds * 1000);
671 }
672 }
673
674 if (state == NOTIFYACTIVITY && !_monitorActivity)
675 state = NOTIFYNORMAL;
676 if (state == NOTIFYSILENCE && !_monitorSilence)
677 state = NOTIFYNORMAL;
678
679 emit stateChanged(state);
680}
681
682void Session::onViewSizeChange(int /*height*/, int /*width*/)
683{
684 updateTerminalSize();
685}
686
687void Session::updateTerminalSize()
688{
689 int minLines = -1;
690 int minColumns = -1;
691
692 // minimum number of lines and columns that views require for
693 // their size to be taken into consideration ( to avoid problems
694 // with new view widgets which haven't yet been set to their correct size )
695 const int VIEW_LINES_THRESHOLD = 2;
696 const int VIEW_COLUMNS_THRESHOLD = 2;
697
698 //select largest number of lines and columns that will fit in all visible views
699 foreach(TerminalDisplay* view, _views) {
700 if (view->isHidden() == false &&
701 view->lines() >= VIEW_LINES_THRESHOLD &&
702 view->columns() >= VIEW_COLUMNS_THRESHOLD) {
703 minLines = (minLines == -1) ? view->lines() : qMin(minLines , view->lines());
704 minColumns = (minColumns == -1) ? view->columns() : qMin(minColumns , view->columns());
705 view->processFilters();
706 }
707 }
708
709 // backend emulation must have a _terminal of at least 1 column x 1 line in size
710 if (minLines > 0 && minColumns > 0) {
711 _emulation->setImageSize(minLines , minColumns);
712 }
713}
714void Session::updateWindowSize(int lines, int columns)
715{
716 Q_ASSERT(lines > 0 && columns > 0);
717 _shellProcess->setWindowSize(columns, lines);
718}
719void Session::refresh()
720{
721 // attempt to get the shell process to redraw the display
722 //
723 // this requires the program running in the shell
724 // to cooperate by sending an update in response to
725 // a window size change
726 //
727 // the window size is changed twice, first made slightly larger and then
728 // resized back to its normal size so that there is actually a change
729 // in the window size (some shells do nothing if the
730 // new and old sizes are the same)
731 //
732 // if there is a more 'correct' way to do this, please
733 // send an email with method or patches to konsole-devel@kde.org
734
735 const QSize existingSize = _shellProcess->windowSize();
736 _shellProcess->setWindowSize(existingSize.width() + 1, existingSize.height());
737 usleep(500); // introduce small delay to avoid changing size too quickly
738 _shellProcess->setWindowSize(existingSize.width(), existingSize.height());
739}
740
741void Session::sendSignal(int signal)
742{
743 const ProcessInfo* process = getProcessInfo();
744 bool ok = false;
745 int pid;
746 pid = process->foregroundPid(&ok);
747
748 if (ok) {
749 ::kill(pid, signal);
750 }
751}
752
753bool Session::kill(int signal)
754{
755 if (_shellProcess->pid() <= 0)
756 return false;
757
758 int result = ::kill(_shellProcess->pid(), signal);
759
760 if (result == 0) {
761 if (_shellProcess->waitForFinished(1000))
762 return true;
763 else
764 return false;
765 } else {
766 return false;
767 }
768}
769
770void Session::close()
771{
772 if (isRunning()) {
773 if (!closeInNormalWay())
774 closeInForceWay();
775 } else {
776 // terminal process has finished, just close the session
777 QTimer::singleShot(1, this, SIGNAL(finished()));
778 }
779}
780
781bool Session::closeInNormalWay()
782{
783 _autoClose = true;
784 _closePerUserRequest = true;
785
786 // for the possible case where following events happen in sequence:
787 //
788 // 1). the terminal process crashes
789 // 2). the tab stays open and displays warning message
790 // 3). the user closes the tab explicitly
791 //
792 if (!isRunning()) {
793 emit finished();
794 return true;
795 }
796
797 if (kill(SIGHUP)) {
798 return true;
799 } else {
800 kWarning() << "Process " << _shellProcess->pid() << " did not die with SIGHUP";
801 _shellProcess->closePty();
802 return (_shellProcess->waitForFinished(1000));
803 }
804}
805
806bool Session::closeInForceWay()
807{
808 _autoClose = true;
809 _closePerUserRequest = true;
810
811 if (kill(SIGKILL)) {
812 return true;
813 } else {
814 kWarning() << "Process " << _shellProcess->pid() << " did not die with SIGKILL";
815 return false;
816 }
817}
818
819void Session::sendText(const QString& text) const
820{
821 _emulation->sendText(text);
822}
823
824void Session::runCommand(const QString& command) const
825{
826 _emulation->sendText(command + '\n');
827}
828
829void Session::sendMouseEvent(int buttons, int column, int line, int eventType)
830{
831 _emulation->sendMouseEvent(buttons, column, line, eventType);
832}
833
834void Session::done(int exitCode, QProcess::ExitStatus exitStatus)
835{
836 // This slot should be triggered only one time
837 disconnect(_shellProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
838 this, SLOT(done(int,QProcess::ExitStatus)));
839
840 if (!_autoClose) {
841 _userTitle = i18nc("@info:shell This session is done", "Finished");
842 emit titleChanged();
843 return;
844 }
845
846 if (_closePerUserRequest) {
847 emit finished();
848 return;
849 }
850
851 QString message;
852
853 if (exitCode != 0) {
854 if (exitStatus != QProcess::NormalExit)
855 message = i18n("Program '%1' crashed.", _program);
856 else
857 message = i18n("Program '%1' exited with status %2.", _program, exitCode);
858
859 //FIXME: See comments in Session::silenceTimerDone()
860 KNotification::event("Finished", message , QPixmap(),
861 QApplication::activeWindow(),
862 KNotification::CloseWhenWidgetActivated);
863 }
864
865 if (exitStatus != QProcess::NormalExit) {
866 // this seeming duplicated line is for the case when exitCode is 0
867 message = i18n("Program '%1' crashed.", _program);
868 terminalWarning(message);
869 } else {
870 emit finished();
871 }
872}
873
874Emulation* Session::emulation() const
875{
876 return _emulation;
877}
878
879QString Session::keyBindings() const
880{
881 return _emulation->keyBindings();
882}
883
884QStringList Session::environment() const
885{
886 return _environment;
887}
888
889void Session::setEnvironment(const QStringList& environment)
890{
891 _environment = environment;
892}
893
894void Session::addEnvironmentEntry(const QString& entry)
895{
896 _environment << entry;
897}
898
899int Session::sessionId() const
900{
901 return _sessionId;
902}
903
904void Session::setKeyBindings(const QString& name)
905{
906 _emulation->setKeyBindings(name);
907}
908
909void Session::setTitle(TitleRole role , const QString& newTitle)
910{
911 if (title(role) != newTitle) {
912 if (role == NameRole)
913 _nameTitle = newTitle;
914 else if (role == DisplayedTitleRole)
915 _displayTitle = newTitle;
916
917 emit titleChanged();
918 }
919}
920
921QString Session::title(TitleRole role) const
922{
923 if (role == NameRole)
924 return _nameTitle;
925 else if (role == DisplayedTitleRole)
926 return _displayTitle;
927 else
928 return QString();
929}
930
931ProcessInfo* Session::getProcessInfo()
932{
933 ProcessInfo* process = 0;
934
935 if (isForegroundProcessActive()) {
936 process = _foregroundProcessInfo;
937 } else {
938 updateSessionProcessInfo();
939 process = _sessionProcessInfo;
940 }
941
942 return process;
943}
944
945void Session::updateSessionProcessInfo()
946{
947 Q_ASSERT(_shellProcess);
948
949 bool ok;
950 // The checking for pid changing looks stupid, but it is needed
951 // at the moment to workaround the problem that processId() might
952 // return 0
953 if (!_sessionProcessInfo ||
954 (processId() != 0 && processId() != _sessionProcessInfo->pid(&ok))) {
955 delete _sessionProcessInfo;
956 _sessionProcessInfo = ProcessInfo::newInstance(processId());
957 _sessionProcessInfo->setUserHomeDir();
958 }
959 _sessionProcessInfo->update();
960}
961
962bool Session::updateForegroundProcessInfo()
963{
964 Q_ASSERT(_shellProcess);
965
966 const int foregroundPid = _shellProcess->foregroundProcessGroup();
967 if (foregroundPid != _foregroundPid) {
968 delete _foregroundProcessInfo;
969 _foregroundProcessInfo = ProcessInfo::newInstance(foregroundPid);
970 _foregroundPid = foregroundPid;
971 }
972
973 if (_foregroundProcessInfo) {
974 _foregroundProcessInfo->update();
975 return _foregroundProcessInfo->isValid();
976 } else {
977 return false;
978 }
979}
980
981bool Session::isRemote()
982{
983 ProcessInfo* process = getProcessInfo();
984
985 bool ok = false;
986 return (process->name(&ok) == "ssh" && ok);
987}
988
989QString Session::getDynamicTitle()
990{
991 // update current directory from process
992 ProcessInfo* process = updateWorkingDirectory();
993
994 // format tab titles using process info
995 bool ok = false;
996 QString title;
997 if (process->name(&ok) == "ssh" && ok) {
998 SSHProcessInfo sshInfo(*process);
999 title = sshInfo.format(tabTitleFormat(Session::RemoteTabTitle));
1000 } else {
1001 title = process->format(tabTitleFormat(Session::LocalTabTitle));
1002 }
1003
1004 return title;
1005}
1006
1007KUrl Session::getUrl()
1008{
1009 QString path;
1010
1011 updateSessionProcessInfo();
1012 if (_sessionProcessInfo->isValid()) {
1013 bool ok = false;
1014
1015 // check if foreground process is bookmark-able
1016 if (isForegroundProcessActive()) {
1017 // for remote connections, save the user and host
1018 // bright ideas to get the directory at the other end are welcome :)
1019 if (_foregroundProcessInfo->name(&ok) == "ssh" && ok) {
1020 SSHProcessInfo sshInfo(*_foregroundProcessInfo);
1021
1022 path = "ssh://" + sshInfo.userName() + '@' + sshInfo.host();
1023
1024 QString port = sshInfo.port();
1025 if (!port.isEmpty() && port != "22") {
1026 path.append(':' + port);
1027 }
1028 } else {
1029 path = _foregroundProcessInfo->currentDir(&ok);
1030 if (!ok)
1031 path.clear();
1032 }
1033 } else { // otherwise use the current working directory of the shell process
1034 path = _sessionProcessInfo->currentDir(&ok);
1035 if (!ok)
1036 path.clear();
1037 }
1038 }
1039
1040 return KUrl(path);
1041}
1042
1043void Session::setIconName(const QString& iconName)
1044{
1045 if (iconName != _iconName) {
1046 _iconName = iconName;
1047 emit titleChanged();
1048 }
1049}
1050
1051void Session::setIconText(const QString& iconText)
1052{
1053 _iconText = iconText;
1054}
1055
1056QString Session::iconName() const
1057{
1058 return _iconName;
1059}
1060
1061QString Session::iconText() const
1062{
1063 return _iconText;
1064}
1065
1066void Session::setHistoryType(const HistoryType& hType)
1067{
1068 _emulation->setHistory(hType);
1069}
1070
1071const HistoryType& Session::historyType() const
1072{
1073 return _emulation->history();
1074}
1075
1076void Session::clearHistory()
1077{
1078 _emulation->clearHistory();
1079}
1080
1081QStringList Session::arguments() const
1082{
1083 return _arguments;
1084}
1085
1086QString Session::program() const
1087{
1088 return _program;
1089}
1090
1091bool Session::isMonitorActivity() const
1092{
1093 return _monitorActivity;
1094}
1095bool Session::isMonitorSilence() const
1096{
1097 return _monitorSilence;
1098}
1099
1100void Session::setMonitorActivity(bool monitor)
1101{
1102 if (_monitorActivity == monitor)
1103 return;
1104
1105 _monitorActivity = monitor;
1106 _notifiedActivity = false;
1107
1108 // This timer is meaningful only after activity has been notified
1109 _activityTimer->stop();
1110
1111 activityStateSet(NOTIFYNORMAL);
1112}
1113
1114void Session::setMonitorSilence(bool monitor)
1115{
1116 if (_monitorSilence == monitor)
1117 return;
1118
1119 _monitorSilence = monitor;
1120 if (_monitorSilence) {
1121 _silenceTimer->start(_silenceSeconds * 1000);
1122 } else {
1123 _silenceTimer->stop();
1124 }
1125
1126 activityStateSet(NOTIFYNORMAL);
1127}
1128
1129void Session::setMonitorSilenceSeconds(int seconds)
1130{
1131 _silenceSeconds = seconds;
1132 if (_monitorSilence) {
1133 _silenceTimer->start(_silenceSeconds * 1000);
1134 }
1135}
1136
1137void Session::setAddToUtmp(bool add)
1138{
1139 _addToUtmp = add;
1140}
1141
1142void Session::setAutoClose(bool close)
1143{
1144 _autoClose = close;
1145}
1146
1147bool Session::autoClose() const
1148{
1149 return _autoClose;
1150}
1151
1152void Session::setFlowControlEnabled(bool enabled)
1153{
1154 _flowControlEnabled = enabled;
1155
1156 if (_shellProcess)
1157 _shellProcess->setFlowControlEnabled(_flowControlEnabled);
1158
1159 emit flowControlEnabledChanged(enabled);
1160}
1161bool Session::flowControlEnabled() const
1162{
1163 if (_shellProcess)
1164 return _shellProcess->flowControlEnabled();
1165 else
1166 return _flowControlEnabled;
1167}
1168void Session::fireZModemDetected()
1169{
1170 if (!_zmodemBusy) {
1171 QTimer::singleShot(10, this, SIGNAL(zmodemDetected()));
1172 _zmodemBusy = true;
1173 }
1174}
1175
1176void Session::cancelZModem()
1177{
1178 _shellProcess->sendData("\030\030\030\030", 4); // Abort
1179 _zmodemBusy = false;
1180}
1181
1182void Session::startZModem(const QString& zmodem, const QString& dir, const QStringList& list)
1183{
1184 _zmodemBusy = true;
1185 _zmodemProc = new KProcess();
1186 _zmodemProc->setOutputChannelMode(KProcess::SeparateChannels);
1187
1188 *_zmodemProc << zmodem << "-v" << list;
1189
1190 if (!dir.isEmpty())
1191 _zmodemProc->setWorkingDirectory(dir);
1192
1193 connect(_zmodemProc, SIGNAL(readyReadStandardOutput()),
1194 this, SLOT(zmodemReadAndSendBlock()));
1195 connect(_zmodemProc, SIGNAL(readyReadStandardError()),
1196 this, SLOT(zmodemReadStatus()));
1197 connect(_zmodemProc, SIGNAL(finished(int,QProcess::ExitStatus)),
1198 this, SLOT(zmodemFinished()));
1199
1200 _zmodemProc->start();
1201
1202 disconnect(_shellProcess, SIGNAL(receivedData(const char*,int)),
1203 this, SLOT(onReceiveBlock(const char*,int)));
1204 connect(_shellProcess, SIGNAL(receivedData(const char*,int)),
1205 this, SLOT(zmodemReceiveBlock(const char*,int)));
1206
1207 _zmodemProgress = new ZModemDialog(QApplication::activeWindow(), false,
1208 i18n("ZModem Progress"));
1209
1210 connect(_zmodemProgress, SIGNAL(user1Clicked()),
1211 this, SLOT(zmodemFinished()));
1212
1213 _zmodemProgress->show();
1214}
1215
1216void Session::zmodemReadAndSendBlock()
1217{
1218 _zmodemProc->setReadChannel(QProcess::StandardOutput);
1219 QByteArray data = _zmodemProc->readAll();
1220
1221 if (data.count() == 0)
1222 return;
1223
1224 _shellProcess->sendData(data.constData(), data.count());
1225}
1226
1227void Session::zmodemReadStatus()
1228{
1229 _zmodemProc->setReadChannel(QProcess::StandardError);
1230 QByteArray msg = _zmodemProc->readAll();
1231 while (!msg.isEmpty()) {
1232 int i = msg.indexOf('\015');
1233 int j = msg.indexOf('\012');
1234 QByteArray txt;
1235 if ((i != -1) && ((j == -1) || (i < j))) {
1236 msg = msg.mid(i + 1);
1237 } else if (j != -1) {
1238 txt = msg.left(j);
1239 msg = msg.mid(j + 1);
1240 } else {
1241 txt = msg;
1242 msg.truncate(0);
1243 }
1244 if (!txt.isEmpty())
1245 _zmodemProgress->addProgressText(QString::fromLocal8Bit(txt));
1246 }
1247}
1248
1249void Session::zmodemReceiveBlock(const char* data, int len)
1250{
1251 QByteArray bytes(data, len);
1252
1253 _zmodemProc->write(bytes);
1254}
1255
1256void Session::zmodemFinished()
1257{
1258 /* zmodemFinished() is called by QProcess's finished() and
1259 ZModemDialog's user1Clicked(). Therefore, an invocation by
1260 user1Clicked() will recursively invoke this function again
1261 when the KProcess is deleted! */
1262 if (_zmodemProc) {
1263 KProcess* process = _zmodemProc;
1264 _zmodemProc = 0; // Set _zmodemProc to 0 avoid recursive invocations!
1265 _zmodemBusy = false;
1266 delete process; // Now, the KProcess may be disposed safely.
1267
1268 disconnect(_shellProcess, SIGNAL(receivedData(const char*,int)),
1269 this , SLOT(zmodemReceiveBlock(const char*,int)));
1270 connect(_shellProcess, SIGNAL(receivedData(const char*,int)),
1271 this, SLOT(onReceiveBlock(const char*,int)));
1272
1273 _shellProcess->sendData("\030\030\030\030", 4); // Abort
1274 _shellProcess->sendData("\001\013\n", 3); // Try to get prompt back
1275 _zmodemProgress->transferDone();
1276 }
1277}
1278
1279void Session::onReceiveBlock(const char* buf, int len)
1280{
1281 _emulation->receiveData(buf, len);
1282}
1283
1284QSize Session::size()
1285{
1286 return _emulation->imageSize();
1287}
1288
1289void Session::setSize(const QSize& size)
1290{
1291 if ((size.width() <= 1) || (size.height() <= 1))
1292 return;
1293
1294 emit resizeRequest(size);
1295}
1296
1297QSize Session::preferredSize() const
1298{
1299 return _preferredSize;
1300}
1301
1302void Session::setPreferredSize(const QSize& size)
1303{
1304 _preferredSize = size;
1305}
1306
1307int Session::processId() const
1308{
1309 return _shellProcess->pid();
1310}
1311
1312void Session::setTitle(int role , const QString& title)
1313{
1314 switch (role) {
1315 case 0:
1316 this->setTitle(Session::NameRole, title);
1317 break;
1318 case 1:
1319 this->setTitle(Session::DisplayedTitleRole, title);
1320
1321 // without these, that title will be overridden by the expansion of
1322 // title format shortly after, which will confuses users.
1323 _localTabTitleFormat = title;
1324 _remoteTabTitleFormat = title;
1325
1326 break;
1327 }
1328}
1329
1330QString Session::title(int role) const
1331{
1332 switch (role) {
1333 case 0:
1334 return this->title(Session::NameRole);
1335 case 1:
1336 return this->title(Session::DisplayedTitleRole);
1337 default:
1338 return QString();
1339 }
1340}
1341
1342void Session::setTabTitleFormat(int context , const QString& format)
1343{
1344 switch (context) {
1345 case 0:
1346 this->setTabTitleFormat(Session::LocalTabTitle, format);
1347 break;
1348 case 1:
1349 this->setTabTitleFormat(Session::RemoteTabTitle, format);
1350 break;
1351 }
1352}
1353
1354QString Session::tabTitleFormat(int context) const
1355{
1356 switch (context) {
1357 case 0:
1358 return this->tabTitleFormat(Session::LocalTabTitle);
1359 case 1:
1360 return this->tabTitleFormat(Session::RemoteTabTitle);
1361 default:
1362 return QString();
1363 }
1364}
1365
1366void Session::setHistorySize(int lines)
1367{
1368 if (lines < 0) {
1369 setHistoryType(HistoryTypeFile());
1370 } else if (lines == 0) {
1371 setHistoryType(HistoryTypeNone());
1372 } else {
1373 setHistoryType(CompactHistoryType(lines));
1374 }
1375}
1376
1377int Session::historySize() const
1378{
1379 const HistoryType& currentHistory = historyType();
1380
1381 if (currentHistory.isEnabled()) {
1382 if (currentHistory.isUnlimited()) {
1383 return -1;
1384 } else {
1385 return currentHistory.maximumLineCount();
1386 }
1387 } else {
1388 return 0;
1389 }
1390}
1391
1392int Session::foregroundProcessId()
1393{
1394 int pid;
1395
1396 bool ok = false;
1397 pid = getProcessInfo()->pid(&ok);
1398 if (!ok)
1399 pid = -1;
1400
1401 return pid;
1402}
1403
1404bool Session::isForegroundProcessActive()
1405{
1406 // foreground process info is always updated after this
1407 return updateForegroundProcessInfo() && (processId() != _foregroundPid);
1408}
1409
1410QString Session::foregroundProcessName()
1411{
1412 QString name;
1413
1414 if (updateForegroundProcessInfo()) {
1415 bool ok = false;
1416 name = _foregroundProcessInfo->name(&ok);
1417 if (!ok)
1418 name.clear();
1419 }
1420
1421 return name;
1422}
1423
1424void Session::saveSession(KConfigGroup& group)
1425{
1426 group.writePathEntry("WorkingDir", currentWorkingDirectory());
1427 group.writeEntry("LocalTab", tabTitleFormat(LocalTabTitle));
1428 group.writeEntry("RemoteTab", tabTitleFormat(RemoteTabTitle));
1429 group.writeEntry("SessionGuid", _uniqueIdentifier.toString());
1430 group.writeEntry("Encoding", QString(codec()));
1431}
1432
1433void Session::restoreSession(KConfigGroup& group)
1434{
1435 QString value;
1436
1437 value = group.readPathEntry("WorkingDir", QString());
1438 if (!value.isEmpty()) setInitialWorkingDirectory(value);
1439 value = group.readEntry("LocalTab");
1440 if (!value.isEmpty()) setTabTitleFormat(LocalTabTitle, value);
1441 value = group.readEntry("RemoteTab");
1442 if (!value.isEmpty()) setTabTitleFormat(RemoteTabTitle, value);
1443 value = group.readEntry("SessionGuid");
1444 if (!value.isEmpty()) _uniqueIdentifier = QUuid(value);
1445 value = group.readEntry("Encoding");
1446 if (!value.isEmpty()) setCodec(value.toUtf8());
1447}
1448
1449SessionGroup::SessionGroup(QObject* parent)
1450 : QObject(parent), _masterMode(0)
1451{
1452}
1453SessionGroup::~SessionGroup()
1454{
1455}
1456int SessionGroup::masterMode() const
1457{
1458 return _masterMode;
1459}
1460QList<Session*> SessionGroup::sessions() const
1461{
1462 return _sessions.keys();
1463}
1464bool SessionGroup::masterStatus(Session* session) const
1465{
1466 return _sessions[session];
1467}
1468
1469void SessionGroup::addSession(Session* session)
1470{
1471 connect(session, SIGNAL(finished()), this, SLOT(sessionFinished()));
1472 _sessions.insert(session, false);
1473}
1474void SessionGroup::removeSession(Session* session)
1475{
1476 disconnect(session, SIGNAL(finished()), this, SLOT(sessionFinished()));
1477 setMasterStatus(session, false);
1478 _sessions.remove(session);
1479}
1480void SessionGroup::sessionFinished()
1481{
1482 Session* session = qobject_cast<Session*>(sender());
1483 Q_ASSERT(session);
1484 removeSession(session);
1485}
1486void SessionGroup::setMasterMode(int mode)
1487{
1488 _masterMode = mode;
1489}
1490QList<Session*> SessionGroup::masters() const
1491{
1492 return _sessions.keys(true);
1493}
1494void SessionGroup::setMasterStatus(Session* session , bool master)
1495{
1496 const bool wasMaster = _sessions[session];
1497
1498 if (wasMaster == master) {
1499 // No status change -> nothing to do.
1500 return;
1501 }
1502 _sessions[session] = master;
1503
1504 if (master) {
1505 connect(session->emulation(), SIGNAL(sendData(const char*,int)),
1506 this, SLOT(forwardData(const char*,int)));
1507 } else {
1508 disconnect(session->emulation(), SIGNAL(sendData(const char*,int)),
1509 this, SLOT(forwardData(const char*,int)));
1510 }
1511}
1512void SessionGroup::forwardData(const char* data, int size)
1513{
1514 static bool _inForwardData = false;
1515 if (_inForwardData) { // Avoid recursive calls among session groups!
1516 // A recursive call happens when a master in group A calls forwardData()
1517 // in group B. If one of the destination sessions in group B is also a
1518 // master of a group including the master session of group A, this would
1519 // again call forwardData() in group A, and so on.
1520 return;
1521 }
1522
1523 _inForwardData = true;
1524 foreach(Session* other, _sessions.keys()) {
1525 if (!_sessions[other]) {
1526 other->emulation()->sendString(data, size);
1527 }
1528 }
1529 _inForwardData = false;
1530}
1531
1532#include "Session.moc"
1533
1534