/* This file is part of Konsole Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Session.h" // Standard #include #include // Qt #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include #include #include "ProcessInfo.h" #include "Pty.h" #include "TerminalDisplay.h" #include "ShellCommand.h" #include "Vt102Emulation.h" #include "ZModemDialog.h" #include "History.h" using namespace Konsole; int Session::lastSessionId = 0; // HACK This is copied out of QUuid::createUuid with reseeding forced. // Required because color schemes repeatedly seed the RNG... // ...with a constant. QUuid createUuid() { static const int intbits = sizeof(int) * 8; static int randbits = 0; if (!randbits) { int max = RAND_MAX; do { ++randbits; } while ((max = max >> 1)); } qsrand(uint(QDateTime::currentDateTime().toTime_t())); qrand(); // Skip first QUuid result; uint* data = &(result.data1); int chunks = 16 / sizeof(uint); while (chunks--) { uint randNumber = 0; for (int filled = 0; filled < intbits; filled += randbits) randNumber |= qrand() << filled; *(data + chunks) = randNumber; } result.data4[0] = (result.data4[0] & 0x3F) | 0x80; // UV_DCE result.data3 = (result.data3 & 0x0FFF) | 0x4000; // UV_Random return result; } Session::Session(QObject* parent) : QObject(parent) , _shellProcess(0) , _emulation(0) , _monitorActivity(false) , _monitorSilence(false) , _notifiedActivity(false) , _silenceSeconds(10) , _autoClose(true) , _closePerUserRequest(false) , _addToUtmp(true) , _flowControl(true) , _sessionId(0) , _sessionProcessInfo(0) , _foregroundProcessInfo(0) , _foregroundPid(0) , _zmodemBusy(false) , _zmodemProc(0) , _zmodemProgress(0) , _hasDarkBackground(false) { _uniqueIdentifier = createUuid(); //prepare DBus communication new SessionAdaptor(this); _sessionId = ++lastSessionId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Sessions/") + QString::number(_sessionId), this); //create emulation backend _emulation = new Vt102Emulation(); connect(_emulation, SIGNAL(titleChanged(int,QString)), this, SLOT(setUserTitle(int,QString))); connect(_emulation, SIGNAL(stateSet(int)), this, SLOT(activityStateSet(int))); connect(_emulation, SIGNAL(zmodemDetected()), this, SLOT(fireZModemDetected())); connect(_emulation, SIGNAL(changeTabTextColorRequest(int)), this, SIGNAL(changeTabTextColorRequest(int))); connect(_emulation, SIGNAL(profileChangeCommandReceived(QString)), this, SIGNAL(profileChangeCommandReceived(QString))); connect(_emulation, SIGNAL(flowControlKeyPressed(bool)), this, SLOT(updateFlowControlState(bool))); connect(_emulation, SIGNAL(primaryScreenInUse(bool)), this, SLOT(onPrimaryScreenInUse(bool))); connect(_emulation, SIGNAL(selectedText(QString)), this, SLOT(onSelectedText(QString))); //create new teletype for I/O with shell process openTeletype(-1); //setup timer for monitoring session activity _silenceTimer = new QTimer(this); _silenceTimer->setSingleShot(true); connect(_silenceTimer, SIGNAL(timeout()), this, SLOT(silenceTimerDone())); _activityTimer = new QTimer(this); _activityTimer->setSingleShot(true); connect(_activityTimer, SIGNAL(timeout()), this, SLOT(activityTimerDone())); } void Session::openTeletype(int fd) { if (isRunning()) { kWarning() << "Attempted to open teletype in a running session."; return; } delete _shellProcess; if (fd < 0) _shellProcess = new Pty(); else _shellProcess = new Pty(fd); _shellProcess->setUtf8Mode(_emulation->utf8()); //connect teletype to emulation backend connect(_shellProcess, SIGNAL(receivedData(const char*,int)), this, SLOT(onReceiveBlock(const char*,int))); connect(_emulation, SIGNAL(sendData(const char*,int)), _shellProcess, SLOT(sendData(const char*,int))); connect(_emulation, SIGNAL(lockPtyRequest(bool)), _shellProcess, SLOT(lockPty(bool))); connect(_emulation, SIGNAL(useUtf8Request(bool)), _shellProcess, SLOT(setUtf8Mode(bool))); connect(_shellProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(done(int,QProcess::ExitStatus))); connect(_emulation, SIGNAL(imageSizeChanged(int,int)), this, SLOT(updateWindowSize(int,int))); connect(_emulation, SIGNAL(imageSizeInitialized()), this, SLOT(run())); } WId Session::windowId() const { // Returns a window ID for this session which is used // to set the WINDOWID environment variable in the shell // process. // // Sessions can have multiple views or no views, which means // that a single ID is not always going to be accurate. // // If there are no views, the window ID is just 0. If // there are multiple views, then the window ID for the // top-level window which contains the first view is // returned if (_views.count() == 0) { return 0; } else { QWidget* window = _views.first(); Q_ASSERT(window); while (window->parentWidget() != 0) window = window->parentWidget(); return window->winId(); } } void Session::setDarkBackground(bool darkBackground) { _hasDarkBackground = darkBackground; } bool Session::hasDarkBackground() const { return _hasDarkBackground; } bool Session::isRunning() const { return _shellProcess && (_shellProcess->state() == QProcess::Running); } void Session::setCodec(QTextCodec* codec) { emulation()->setCodec(codec); } bool Session::setCodec(QByteArray name) { QTextCodec* codec = QTextCodec::codecForName(name); if (codec) { setCodec(codec); return true; } else { return false; } } QByteArray Session::codec() { return _emulation->codec()->name(); } void Session::setProgram(const QString& program) { _program = ShellCommand::expand(program); } void Session::setInitialWorkingDirectory(const QString& dir) { _initialWorkingDir = KShell::tildeExpand(ShellCommand::expand(dir)); } void Session::setArguments(const QStringList& arguments) { _arguments = ShellCommand::expand(arguments); } QString Session::currentWorkingDirectory() { // only returned cached value if (_currentWorkingDir.isEmpty()) updateWorkingDirectory(); return _currentWorkingDir; } ProcessInfo* Session::updateWorkingDirectory() { ProcessInfo* process = getProcessInfo(); _currentWorkingDir = process->validCurrentDir(); return process; } QList Session::views() const { return _views; } void Session::addView(TerminalDisplay* widget) { Q_ASSERT(!_views.contains(widget)); _views.append(widget); if (_emulation != 0) { // connect emulation - view signals and slots connect(widget, SIGNAL(keyPressedSignal(QKeyEvent*)), _emulation, SLOT(sendKeyEvent(QKeyEvent*))); connect(widget, SIGNAL(mouseSignal(int,int,int,int)), _emulation, SLOT(sendMouseEvent(int,int,int,int))); connect(widget, SIGNAL(sendStringToEmu(const char*)), _emulation, SLOT(sendString(const char*))); // allow emulation to notify view when the foreground process // indicates whether or not it is interested in mouse signals connect(_emulation, SIGNAL(programUsesMouseChanged(bool)), widget, SLOT(setUsesMouse(bool))); widget->setUsesMouse(_emulation->programUsesMouse()); widget->setScreenWindow(_emulation->createWindow()); } //connect view signals and slots QObject::connect(widget, SIGNAL(changedContentSizeSignal(int,int)), this, SLOT(onViewSizeChange(int,int))); QObject::connect(widget, SIGNAL(destroyed(QObject*)), this, SLOT(viewDestroyed(QObject*))); } void Session::viewDestroyed(QObject* view) { TerminalDisplay* display = (TerminalDisplay*)view; Q_ASSERT(_views.contains(display)); removeView(display); } void Session::removeView(TerminalDisplay* widget) { _views.removeAll(widget); disconnect(widget, 0, this, 0); if (_emulation != 0) { // disconnect // - key presses signals from widget // - mouse activity signals from widget // - string sending signals from widget // // ... and any other signals connected in addView() disconnect(widget, 0, _emulation, 0); // disconnect state change signals emitted by emulation disconnect(_emulation, 0, widget, 0); } // close the session automatically when the last view is removed if (_views.count() == 0) { close(); } } QString Session::checkProgram(const QString& program) const { // Upon a KPty error, there is no description on what that error was... // Check to see if the given program is executable. QString exec = QFile::encodeName(program); if (exec.isEmpty()) return QString(); exec = KRun::binaryName(exec, false); exec = KShell::tildeExpand(exec); QString pexec = KStandardDirs::findExe(exec); if (pexec.isEmpty()) { kError() << i18n("Could not find binary: ") << exec; return QString(); } return exec; } void Session::terminalWarning(const QString& message) { static const QByteArray warningText = i18nc("@info:shell Alert the user with red color text", "Warning: ").toLocal8Bit(); QByteArray messageText = message.toLocal8Bit(); static const char redPenOn[] = "\033[1m\033[31m"; static const char redPenOff[] = "\033[0m"; _emulation->receiveData(redPenOn, qstrlen(redPenOn)); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(warningText.constData(), qstrlen(warningText.constData())); _emulation->receiveData(messageText.constData(), qstrlen(messageText.constData())); _emulation->receiveData("\n\r\n\r", 4); _emulation->receiveData(redPenOff, qstrlen(redPenOff)); } QString Session::shellSessionId() const { QString friendlyUuid(_uniqueIdentifier.toString()); friendlyUuid.remove('-').remove('{').remove('}'); return friendlyUuid; } void Session::run() { // extra safeguard for potential bug. if (isRunning()) { kWarning() << "Attempted to re-run an already running session."; return; } //check that everything is in place to run the session if (_program.isEmpty()) { kWarning() << "Session::run() - program to run not set."; } if (_arguments.isEmpty()) { kWarning() << "Session::run() - no command line arguments specified."; } if (_uniqueIdentifier.isNull()) { _uniqueIdentifier = createUuid(); } const int CHOICE_COUNT = 3; // if '_program' is empty , fall back to default shell. If that is not set // then fall back to /bin/sh QString programs[CHOICE_COUNT] = {_program, qgetenv("SHELL"), "/bin/sh"}; QString exec; int choice = 0; while (choice < CHOICE_COUNT) { exec = checkProgram(programs[choice]); if (exec.isEmpty()) choice++; else break; } // if a program was specified via setProgram(), but it couldn't be found, print a warning if (choice != 0 && choice < CHOICE_COUNT && !_program.isEmpty()) { terminalWarning(i18n("Could not find '%1', starting '%2' instead. Please check your profile settings.", _program, exec)); } // if none of the choices are available, print a warning else if (choice == CHOICE_COUNT) { terminalWarning(i18n("Could not find an interactive shell to start.")); return; } // if no arguments are specified, fall back to program name QStringList arguments = _arguments.join(QChar(' ')).isEmpty() ? QStringList() << exec : _arguments; QString dbusService = QDBusConnection::sessionBus().baseService(); if (!_initialWorkingDir.isEmpty()) _shellProcess->setInitialWorkingDirectory(_initialWorkingDir); else _shellProcess->setInitialWorkingDirectory(QDir::homePath()); _shellProcess->setFlowControlEnabled(_flowControl); _shellProcess->setEraseChar(_emulation->eraseChar()); // this is not strictly accurate use of the COLORFGBG variable. This does not // tell the terminal exactly which colors are being used, but instead approximates // the color scheme as "black on white" or "white on black" depending on whether // the background color is deemed dark or not QString backgroundColorHint = _hasDarkBackground ? "COLORFGBG=15;0" : "COLORFGBG=0;15"; _environment << backgroundColorHint; _environment << QString("SHELL_SESSION_ID=%1").arg(shellSessionId()); int result = _shellProcess->start(exec, arguments, _environment, windowId(), _addToUtmp, dbusService, (QLatin1String("/Sessions/") + QString::number(_sessionId))); if (result < 0) { terminalWarning(i18n("Could not start program '%1' with arguments '%2'.", exec, arguments.join(" "))); return; } _shellProcess->setWriteable(false); // We are reachable via kwrited. emit started(); } void Session::setUserTitle(int what, const QString& caption) { //set to true if anything is actually changed (eg. old _nameTitle != new _nameTitle ) bool modified = false; if ((what == IconNameAndWindowTitle) || (what == WindowTitle)) { if (_userTitle != caption) { _userTitle = caption; modified = true; } } if ((what == IconNameAndWindowTitle) || (what == IconName)) { if (_iconText != caption) { _iconText = caption; modified = true; } } if (what == TextColor || what == BackgroundColor) { QString colorString = caption.section(';', 0, 0); QColor color = QColor(colorString); if (color.isValid()) { if (what == TextColor) emit changeForegroundColorRequest(color); else emit changeBackgroundColorRequest(color); } } if (what == SessionName) { if (_nameTitle != caption) { setTitle(Session::NameRole, caption); return; } } if (what == 31) { QString cwd = caption; cwd = cwd.replace(QRegExp("^~"), QDir::homePath()); emit openUrlRequest(cwd); } // change icon via \033]32;Icon\007 if (what == 32) { if (_iconName != caption) { _iconName = caption; modified = true; } } if (what == ProfileChange) { emit profileChangeCommandReceived(caption); return; } if (modified) emit titleChanged(); } QString Session::userTitle() const { return _userTitle; } void Session::setTabTitleFormat(TabTitleContext context , const QString& format) { if (context == LocalTabTitle) _localTabTitleFormat = format; else if (context == RemoteTabTitle) _remoteTabTitleFormat = format; } QString Session::tabTitleFormat(TabTitleContext context) const { if (context == LocalTabTitle) return _localTabTitleFormat; else if (context == RemoteTabTitle) return _remoteTabTitleFormat; return QString(); } void Session::silenceTimerDone() { //FIXME: The idea here is that the notification popup will appear to tell the user than output from //the terminal has stopped and the popup will disappear when the user activates the session. // //This breaks with the addition of multiple views of a session. The popup should disappear //when any of the views of the session becomes active //FIXME: Make message text for this notification and the activity notification more descriptive. if (_monitorSilence) { KNotification::event("Silence", i18n("Silence in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); emit stateChanged(NOTIFYSILENCE); } else { emit stateChanged(NOTIFYNORMAL); } _notifiedActivity = false; } void Session::activityTimerDone() { _notifiedActivity = false; } void Session::updateFlowControlState(bool suspended) { if (suspended) { if (flowControlEnabled()) { foreach(TerminalDisplay * display, _views) { if (display->flowControlWarningEnabled()) display->outputSuspended(true); } } } else { foreach(TerminalDisplay * display, _views) { display->outputSuspended(false); } } } void Session::onPrimaryScreenInUse(bool use) { emit primaryScreenInUse(use); } void Session::onSelectedText(const QString & text) { emit selectedText(text); } void Session::activityStateSet(int state) { if (state == NOTIFYBELL) { emit bellRequest(i18n("Bell in session '%1'", _nameTitle)); } else if (state == NOTIFYACTIVITY) { if (_monitorActivity && !_notifiedActivity) { KNotification::event("Activity", i18n("Activity in session '%1'", _nameTitle), QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); // mask activity notification for a while to avoid flooding // TODO: should this hardcoded interval be user configurable? _notifiedActivity = true; const int activitMaskSeconds = 15; _activityTimer->start(activitMaskSeconds * 1000); } if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } } if (state == NOTIFYACTIVITY && !_monitorActivity) state = NOTIFYNORMAL; if (state == NOTIFYSILENCE && !_monitorSilence) state = NOTIFYNORMAL; emit stateChanged(state); } void Session::onViewSizeChange(int /*height*/, int /*width*/) { updateTerminalSize(); } void Session::updateTerminalSize() { QListIterator viewIter(_views); int minLines = -1; int minColumns = -1; // minimum number of lines and columns that views require for // their size to be taken into consideration ( to avoid problems // with new view widgets which haven't yet been set to their correct size ) const int VIEW_LINES_THRESHOLD = 2; const int VIEW_COLUMNS_THRESHOLD = 2; //select largest number of lines and columns that will fit in all visible views while (viewIter.hasNext()) { TerminalDisplay* view = viewIter.next(); if (view->isHidden() == false && view->lines() >= VIEW_LINES_THRESHOLD && view->columns() >= VIEW_COLUMNS_THRESHOLD) { minLines = (minLines == -1) ? view->lines() : qMin(minLines , view->lines()); minColumns = (minColumns == -1) ? view->columns() : qMin(minColumns , view->columns()); view->processFilters(); } } // backend emulation must have a _terminal of at least 1 column x 1 line in size if (minLines > 0 && minColumns > 0) { _emulation->setImageSize(minLines , minColumns); } } void Session::updateWindowSize(int lines, int columns) { Q_ASSERT(lines > 0 && columns > 0); _shellProcess->setWindowSize(lines, columns); } void Session::refresh() { // attempt to get the shell process to redraw the display // // this requires the program running in the shell // to cooperate by sending an update in response to // a window size change // // the window size is changed twice, first made slightly larger and then // resized back to its normal size so that there is actually a change // in the window size (some shells do nothing if the // new and old sizes are the same) // // if there is a more 'correct' way to do this, please // send an email with method or patches to konsole-devel@kde.org const QSize existingSize = _shellProcess->windowSize(); _shellProcess->setWindowSize(existingSize.height(), existingSize.width() + 1); _shellProcess->setWindowSize(existingSize.height(), existingSize.width()); } bool Session::kill(int signal) { int result = ::kill(_shellProcess->pid(), signal); if (result == 0) { if (_shellProcess->waitForFinished(1000)) return true; else return false; } else return false; } void Session::close() { if (isRunning()) { if (!closeInNormalWay()) closeInForceWay(); } else { // terminal process has finished, just close the session QTimer::singleShot(1, this, SIGNAL(finished())); } } bool Session::closeInNormalWay() { _autoClose = true; _closePerUserRequest = true; // for the possible case where following events happen in sequence: // // 1). the terminal process crashes // 2). the tab stays open and displays warning message // 3). the user closes the tab explicitly // if (!isRunning()) { emit finished(); return true; } if (kill(SIGHUP)) { return true; } else { kWarning() << "Process " << _shellProcess->pid() << " did not die with SIGHUP"; _shellProcess->pty()->close(); return (_shellProcess->waitForFinished(1000)) ; } } bool Session::closeInForceWay() { _autoClose = true; _closePerUserRequest = true; if (kill(SIGKILL)) { return true; } else { kWarning() << "Process " << _shellProcess->pid() << " did not die with SIGKILL"; return false; } } void Session::sendText(const QString& text) const { _emulation->sendText(text); } void Session::sendMouseEvent(int buttons, int column, int line, int eventType) { _emulation->sendMouseEvent(buttons, column, line, eventType); } Session::~Session() { delete _foregroundProcessInfo; delete _sessionProcessInfo; delete _emulation; delete _shellProcess; delete _zmodemProc; } void Session::done(int exitCode, QProcess::ExitStatus exitStatus) { if (!_autoClose) { _userTitle = i18nc("@info:shell This session is done", "Finished"); emit titleChanged(); return; } if (_closePerUserRequest) { emit finished(); return; } QString message; if (exitCode != 0) { if (exitStatus != QProcess::NormalExit) message = i18n("Program '%1' crashed.", _program); else message = i18n("Program '%1' exited with status %2.", _program, exitCode); //FIXME: See comments in Session::silenceTimerDone() KNotification::event("Finished", message , QPixmap(), QApplication::activeWindow(), KNotification::CloseWhenWidgetActivated); } if (exitStatus != QProcess::NormalExit) { // this seeming duplicated line is for the case when exitCode is 0 message = i18n("Program '%1' crashed.", _program); terminalWarning(message); } else { emit finished(); } } Emulation* Session::emulation() const { return _emulation; } QString Session::keyBindings() const { return _emulation->keyBindings(); } QStringList Session::environment() const { return _environment; } void Session::setEnvironment(const QStringList& environment) { _environment = environment; } int Session::sessionId() const { return _sessionId; } void Session::setKeyBindings(const QString& id) { _emulation->setKeyBindings(id); } void Session::setTitle(TitleRole role , const QString& newTitle) { if (title(role) != newTitle) { if (role == NameRole) _nameTitle = newTitle; else if (role == DisplayedTitleRole) _displayTitle = newTitle; emit titleChanged(); } } QString Session::title(TitleRole role) const { if (role == NameRole) return _nameTitle; else if (role == DisplayedTitleRole) return _displayTitle; else return QString(); } ProcessInfo* Session::getProcessInfo() { ProcessInfo* process = 0; if (isForegroundProcessActive()) { process = _foregroundProcessInfo; } else { updateSessionProcessInfo(); process = _sessionProcessInfo; } return process; } void Session::updateSessionProcessInfo() { Q_ASSERT(_shellProcess); if (!_sessionProcessInfo) { _sessionProcessInfo = ProcessInfo::newInstance(processId()); _sessionProcessInfo->setUserHomeDir(); } _sessionProcessInfo->update(); } bool Session::updateForegroundProcessInfo() { Q_ASSERT(_shellProcess); int pid = _shellProcess->foregroundProcessGroup(); if (pid != _foregroundPid) { delete _foregroundProcessInfo; _foregroundProcessInfo = ProcessInfo::newInstance(pid); _foregroundPid = pid; } if (_foregroundProcessInfo) { _foregroundProcessInfo->update(); return _foregroundProcessInfo->isValid(); } else { return false; } } bool Session::isRemote() { ProcessInfo* process = getProcessInfo(); bool ok = false; return (process->name(&ok) == "ssh" && ok); } QString Session::getDynamicTitle() { // update current directory from process ProcessInfo* process = updateWorkingDirectory(); // format tab titles using process info bool ok = false; QString title; if (process->name(&ok) == "ssh" && ok) { SSHProcessInfo sshInfo(*process); title = sshInfo.format(tabTitleFormat(Session::RemoteTabTitle)); } else { title = process->format(tabTitleFormat(Session::LocalTabTitle)); } return title; } KUrl Session::getUrl() { QString path; updateSessionProcessInfo(); if (_sessionProcessInfo->isValid()) { bool ok = false; // check if foreground process is bookmark-able if (isForegroundProcessActive()) { // for remote connections, save the user and host // bright ideas to get the directory at the other end are welcome :) if (_foregroundProcessInfo->name(&ok) == "ssh" && ok) { SSHProcessInfo sshInfo(*_foregroundProcessInfo); path = "ssh://" + sshInfo.userName() + '@' + sshInfo.host(); QString port = sshInfo.port(); if (!port.isEmpty() && port != "22") { path.append(':' + port); } } else { path = _foregroundProcessInfo->currentDir(&ok); if (!ok) path.clear(); } } else { // otherwise use the current working directory of the shell process path = _sessionProcessInfo->currentDir(&ok); if (!ok) path.clear(); } } return KUrl(path); } void Session::setIconName(const QString& iconName) { if (iconName != _iconName) { _iconName = iconName; emit titleChanged(); } } void Session::setIconText(const QString& iconText) { _iconText = iconText; } QString Session::iconName() const { return _iconName; } QString Session::iconText() const { return _iconText; } void Session::setHistoryType(const HistoryType& hType) { _emulation->setHistory(hType); } const HistoryType& Session::historyType() const { return _emulation->history(); } void Session::clearHistory() { _emulation->clearHistory(); } QStringList Session::arguments() const { return _arguments; } QString Session::program() const { return _program; } // unused currently bool Session::isMonitorActivity() const { return _monitorActivity; } // unused currently bool Session::isMonitorSilence() const { return _monitorSilence; } void Session::setMonitorActivity(bool monitor) { _monitorActivity = monitor; _notifiedActivity = false; activityStateSet(NOTIFYNORMAL); } void Session::setMonitorSilence(bool monitor) { if (_monitorSilence == monitor) return; _monitorSilence = monitor; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } else { _silenceTimer->stop(); } activityStateSet(NOTIFYNORMAL); } void Session::setMonitorSilenceSeconds(int seconds) { _silenceSeconds = seconds; if (_monitorSilence) { _silenceTimer->start(_silenceSeconds * 1000); } } void Session::setAddToUtmp(bool set) { _addToUtmp = set; } void Session::setFlowControlEnabled(bool enabled) { _flowControl = enabled; if (_shellProcess) _shellProcess->setFlowControlEnabled(_flowControl); emit flowControlEnabledChanged(enabled); } bool Session::flowControlEnabled() const { if (_shellProcess) return _shellProcess->flowControlEnabled(); else return _flowControl; } void Session::fireZModemDetected() { if (!_zmodemBusy) { QTimer::singleShot(10, this, SIGNAL(zmodemDetected())); _zmodemBusy = true; } } void Session::cancelZModem() { _shellProcess->sendData("\030\030\030\030", 4); // Abort _zmodemBusy = false; } void Session::startZModem(const QString& zmodem, const QString& dir, const QStringList& list) { _zmodemBusy = true; _zmodemProc = new KProcess(); _zmodemProc->setOutputChannelMode(KProcess::SeparateChannels); *_zmodemProc << zmodem << "-v" << list; if (!dir.isEmpty()) _zmodemProc->setWorkingDirectory(dir); connect(_zmodemProc, SIGNAL(readyReadStandardOutput()), this, SLOT(zmodemReadAndSendBlock())); connect(_zmodemProc, SIGNAL(readyReadStandardError()), this, SLOT(zmodemReadStatus())); connect(_zmodemProc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(zmodemFinished())); _zmodemProc->start(); disconnect(_shellProcess, SIGNAL(receivedData(const char*,int)), this, SLOT(onReceiveBlock(const char*,int))); connect(_shellProcess, SIGNAL(receivedData(const char*,int)), this, SLOT(zmodemRcvBlock(const char*,int))); _zmodemProgress = new ZModemDialog(QApplication::activeWindow(), false, i18n("ZModem Progress")); connect(_zmodemProgress, SIGNAL(user1Clicked()), this, SLOT(zmodemFinished())); _zmodemProgress->show(); } void Session::zmodemReadAndSendBlock() { _zmodemProc->setReadChannel(QProcess::StandardOutput); QByteArray data = _zmodemProc->readAll(); if (data.count() == 0) return; _shellProcess->sendData(data.constData(), data.count()); } void Session::zmodemReadStatus() { _zmodemProc->setReadChannel(QProcess::StandardError); QByteArray msg = _zmodemProc->readAll(); while (!msg.isEmpty()) { int i = msg.indexOf('\015'); int j = msg.indexOf('\012'); QByteArray txt; if ((i != -1) && ((j == -1) || (i < j))) { msg = msg.mid(i + 1); } else if (j != -1) { txt = msg.left(j); msg = msg.mid(j + 1); } else { txt = msg; msg.truncate(0); } if (!txt.isEmpty()) _zmodemProgress->addProgressText(QString::fromLocal8Bit(txt)); } } void Session::zmodemRcvBlock(const char* data, int len) { QByteArray ba(data, len); _zmodemProc->write(ba); } void Session::zmodemFinished() { /* zmodemFinished() is called by QProcess's finished() and ZModemDialog's user1Clicked(). Therefore, an invocation by user1Clicked() will recursively invoke this function again when the KProcess is deleted! */ if (_zmodemProc) { KProcess* process = _zmodemProc; _zmodemProc = 0; // Set _zmodemProc to 0 avoid recursive invocations! _zmodemBusy = false; delete process; // Now, the KProcess may be disposed safely. disconnect(_shellProcess, SIGNAL(receivedData(const char*,int)), this , SLOT(zmodemRcvBlock(const char*,int))); connect(_shellProcess, SIGNAL(receivedData(const char*,int)), this, SLOT(onReceiveBlock(const char*,int))); _shellProcess->sendData("\030\030\030\030", 4); // Abort _shellProcess->sendData("\001\013\n", 3); // Try to get prompt back _zmodemProgress->transferDone(); } } void Session::onReceiveBlock(const char* buf, int len) { _emulation->receiveData(buf, len); emit receivedData(QString::fromLatin1(buf, len)); } QSize Session::size() { return _emulation->imageSize(); } void Session::setSize(const QSize& size) { if ((size.width() <= 1) || (size.height() <= 1)) return; emit resizeRequest(size); } int Session::processId() const { return _shellProcess->pid(); } void Session::setTitle(int role , const QString& title) { switch (role) { case (0): this->setTitle(Session::NameRole, title); break; case (1): this->setTitle(Session::DisplayedTitleRole, title); // without these, that title will be overridden by the expansion of // title format shortly after, which will confuses users. _localTabTitleFormat = title; _remoteTabTitleFormat = title; break; } } QString Session::title(int role) const { switch (role) { case (0): return this->title(Session::NameRole); case (1): return this->title(Session::DisplayedTitleRole); default: return QString(); } } void Session::setTabTitleFormat(int context , const QString& format) { switch (context) { case (0): this->setTabTitleFormat(Session::LocalTabTitle, format); break; case (1): this->setTabTitleFormat(Session::RemoteTabTitle, format); break; } } QString Session::tabTitleFormat(int context) const { switch (context) { case (0): return this->tabTitleFormat(Session::LocalTabTitle); case (1): return this->tabTitleFormat(Session::RemoteTabTitle); default: return QString(); } } int Session::foregroundProcessId() { int pid; bool ok = false; pid = getProcessInfo()->pid(&ok); if (!ok) pid = -1; return pid; } bool Session::isForegroundProcessActive() { // foreground process info is always updated after this return updateForegroundProcessInfo() && (processId() != _foregroundPid); } QString Session::foregroundProcessName() { QString name; if (updateForegroundProcessInfo()) { bool ok = false; name = _foregroundProcessInfo->name(&ok); if (!ok) name.clear(); } return name; } void Session::saveSession(KConfigGroup& group) { group.writePathEntry("WorkingDir", currentWorkingDirectory()); group.writeEntry("LocalTab", tabTitleFormat(LocalTabTitle)); group.writeEntry("RemoteTab", tabTitleFormat(RemoteTabTitle)); group.writeEntry("SessionGuid", _uniqueIdentifier.toString()); group.writeEntry("Encoding", QString(codec())); } void Session::restoreSession(KConfigGroup& group) { QString value; value = group.readPathEntry("WorkingDir", QString()); if (!value.isEmpty()) setInitialWorkingDirectory(value); value = group.readEntry("LocalTab"); if (!value.isEmpty()) setTabTitleFormat(LocalTabTitle, value); value = group.readEntry("RemoteTab"); if (!value.isEmpty()) setTabTitleFormat(RemoteTabTitle, value); value = group.readEntry("SessionGuid"); if (!value.isEmpty()) _uniqueIdentifier = QUuid(value); value = group.readEntry("Encoding"); if (!value.isEmpty()) setCodec(value.toUtf8()); } SessionGroup::SessionGroup(QObject* parent) : QObject(parent), _masterMode(0) { } SessionGroup::~SessionGroup() { } int SessionGroup::masterMode() const { return _masterMode; } QList SessionGroup::sessions() const { return _sessions.keys(); } bool SessionGroup::masterStatus(Session* session) const { return _sessions[session]; } void SessionGroup::addSession(Session* session) { connect(session, SIGNAL(finished()), this, SLOT(sessionFinished())); _sessions.insert(session, false); } void SessionGroup::removeSession(Session* session) { disconnect(session, SIGNAL(finished()), this, SLOT(sessionFinished())); setMasterStatus(session, false); _sessions.remove(session); } void SessionGroup::sessionFinished() { Session* session = qobject_cast(sender()); Q_ASSERT(session); removeSession(session); } void SessionGroup::setMasterMode(int mode) { _masterMode = mode; } QList SessionGroup::masters() const { return _sessions.keys(true); } void SessionGroup::setMasterStatus(Session* session , bool master) { const bool wasMaster = _sessions[session]; if (wasMaster == master) { // No status change -> nothing to do. return; } _sessions[session] = master; if (master) { connect(session->emulation(), SIGNAL(sendData(const char*,int)), this, SLOT(forwardData(const char*,int))); } else { disconnect(session->emulation(), SIGNAL(sendData(const char*,int)), this, SLOT(forwardData(const char*,int))); } } void SessionGroup::forwardData(const char* data, int size) { static bool _inForwardData = false; if (_inForwardData) { // Avoid recursive calls among session groups! // A recursive call happens when a master in group A calls forwardData() // in group B. If one of the destination sessions in group B is also a // master of a group including the master session of group A, this would // again call forwardData() in group A, and so on. return; } _inForwardData = true; QListIterator iter(_sessions.keys()); while (iter.hasNext()) { Session* other = iter.next(); if (!_sessions[other]) { other->emulation()->sendString(data, size); } } _inForwardData = false; } #include "Session.moc"