QProcess is a very convenient (and cross-platform) class for starting and controlling other processes. It is derived from QObject and takes full advantage of signals and slots to make it easier to "hook up" with other Qt classes.
We discuss now a simple example that starts a process and views its continually running output. Example 12.1 shows the definition of a simple class derived from QProcess.[1]
[1] tail -f runs forever showing whatever is appended to a file, and is useful for showing the contents of a log file of a running process.
Example 12.1. src/logtail/logtail.h
[ . . . . ] #include #include class LogTail : public QProcess { Q_OBJECT public: LogTail(QString fn = QString()); ~LogTail(); public slots: void logOutput(); }; [ . . . . ] |
A QProcess can spawn another process using the start() function. The new process is a child process in that it will terminate when the parent process does. Example 12.2 shows the implementation of the constructor and destructor of the LogTail class.[2]
[2] It is also possible to use startDetached() to start a process that will continue to live after the calling process exits.
Example 12.2. src/logtail/logtail.cpp
[ . . . . ] LogTail::LogTail(QString fn) { if (fn == QString()) { fn = "/var/log/apache/access.log"; } connect (this, SIGNAL(readyReadStandardOutput()), this, SLOT(logOutput())); <-- 1 QStringList argv; argv << "-f" << fn; // We want to exec "tail -f filename" start("tail", argv); <-- 2 } LogTail::~LogTail() { terminate(); <-- 3 }
|
The child process can be treated as a sequential I/O device with two predefined output channels that represent two separate streams of data: stdout and stderr. The parent process can select an output channel with setReadChannel() (default is stdout). The signal readyReadStandardOutput() is emitted when data is available on the selected channel of the child process. The parent process can then read its output by calling read(), readLine(), or getChar(). If the child process has standard input enabled, the parent can use write() to send data to it.
Example 12.3 shows the implementation of the slot logOutput(), which is connected to the signal readyReadStandardOutput() and uses readAllStandardOutput() so that it pays attention only to stdout.
Example 12.3. src/logtail/logtail.cpp
[ . . . . ] // tail sends its output to stdout. void LogTail::logOutput() { <-- 1 QByteArray bytes = readAllStandardOutput(); QStringList lines = QString(bytes).split(" "); foreach (QString line, lines) { qDebug() << line; } }
|
The use of signals eliminates the need for a read loop. When there is no more input to be read, the slot will no longer be called. Signals and slots make concurrent code much simpler to read, because they hide the event-handling and dispatching code. Some client code is shown in Example 12.4.
Example 12.4. src/logtail/logtail.cpp
[ . . . . ] #include #include #include int main (int argc, char* argv[]) { QApplication app(argc, argv); ArgumentList al; LogWindow lw("debug"); <-- 1 lw.setWindowTitle("logtail demo"); QString filename; if (al.size() > 1) filename = al[1]; LogTail tail(filename); <-- 2 lw.show(); return app.exec(); }
|
This application appends lines to the LogWindow whenever they appear in the specified log file. The default log file is named access.log for the apache Web server on a typical *nix local host.
12.1.1. Processes and Environment
Environment variables are name/value string pairs that can be stored quite easily in a map or a hash table. Every running process has an environment, or a collection of environment variables. Most programming languages support a way of getting and setting these variables.
The most common environment variables are
Another environment variable you will encounter in the context of the exercises is CPPLIBS, which denotes the location of our own C++ libraries.
Environment variables and their values are arbitrarily set by the parent process, so they can be any pair of strings. Programs that depend on specific variables are generally not portable, but depend somehow on their parent process. Variables provide a convenient cross-language mechanism for communicating information between processes.
Operating system shells allow the user to set environment variables for that process and its future children. Here are some examples.
Many programming languages support getting and setting environment variables too, as listed here.
[3] Some platforms also offer setenv(), which is more convenient but less portable.
Changing the environment does not affect other processes that are already running. Environments are inherited from the parent process at process creation time.
Any program that you run is quite possibly a few levels deep in the process tree. This is because the typical desktop operating system environment consists of many processes running together. In Figure 12.1 indentation levels indicate the parent-child relationships. Processes that are at the same indentation level are siblings (i.e., children of the same parent).
Figure 12.1. Linux process hierarchy
Whenever program A runs program B, A is the parent process of B. B inherits (a copy of) A's environment when B is created. Changes to B's environment from inside B will only affect B and B's future children, and will always be invisible to A.
In Example 12.5, we verify that the value given to setenv() is propagated to its child process.
Example 12.5. src/environment/setenv.cpp
#include #include #include #include class Fork : public QProcess { public: Fork(QStringList argv = QStringList() ) { execute("environment", argv); <-- 1 } ~Fork() { waitForFinished(); } }; int main(int argc, char* argv[]) { using namespace qstd; ArgumentList al(argc, argv); al.removeFirst(); bool fork=al.getSwitch("-f"); QStringList extraVars; if (al.count() > 0) { setenv("PENGUIN", al.first().toAscii(), true); } cout << " HOME=" << getenv("HOME") << endl; cout << " PWD=" << getenv("PWD") << endl; cout << " PENGUIN=" << getenv ("PENGUIN") << endl; if (fork) { Fork f; } }
|
When this program is run, our output looks like this:
/home/lazarus/src/environment> export PENGUIN=tux /home/lazarus/src/environment> ./environment -f HOME=/home/lazarus PWD=/home/lazarus/src/environment PENGUIN=tux HOME=/home/lazarus PWD=/home/lazarus/src/environment PENGUIN=tux /home/lazarus/src/environment> ./environment -f opus HOME=/home/lazarus PWD=/home/lazarus/src/environment PENGUIN=opus HOME=/home/lazarus PWD=/home/lazarus/src/environment PENGUIN=opus
12.1.2. Qonsole: Writing an Xterm in Qt
A command line shell reads commands from the user and prints the output. In this example, we have a LogWindow (see Section 7.4) providing a view of the output of another running process, in this case, bash. The QProcess is a model, representing a running process. Figure 12.2 shows a screenshot of Qonsole, our first attempt at a GUI for a command shell.
Figure 12.2. Qonsole1
Because it connects signals to slots and handles user interactions, Qonsole is considered a controller. Because it derives from QMainWindow, it also contains some view code. The UML diagram in Figure 12.3 shows the relationships between the classes in this application.
Figure 12.3. Qonsole UML: Model and view
In Example 12.6 we see how the constructor establishes the structure of Qonsole and the important connections between its components.
Example 12.6. src/qonsole/qonsole1/qonsole.cpp
[ . . . . ] Qonsole::Qonsole() { m_Logw = new LogWindow("debug"); m_Logw->setReadOnly(true); setCentralWidget(m_Logw); m_InputArea = new QLineEdit(); QDockWidget* qdw = new QDockWidget("Type commands here"); qdw->setWidget(m_InputArea); addDockWidget(Qt::BottomDockWidgetArea, qdw); connect (m_InputArea, SIGNAL(returnPressed()), this, SLOT(execute())); m_Bash = new QProcess(); m_Bash->setReadChannelMode(QProcess::MergedChannels); <-- 1 connect (m_Bash, SIGNAL(readyReadStandardOutput()), this, SLOT(showOutput())); m_Bash->start("bash", QStringList() << "-i"); <-- 2 }
|
Whenever bash outputs anything, Qonsole sends it to the LogWindow. Whenever the user presses the return key, Qonsole grabs any text that is in the QLineEdit and sends it to bash, which interprets it as a command, as we see in Example 12.7.
Example 12.7. src/qonsole/qonsole1/qonsole.cpp
[ . . . . ] void Qonsole::showOutput() { <-- 1 QByteArray bytes = m_Bash->readAllStandardOutput(); QStringList lines = QString(bytes).split(" "); foreach (QString line, lines) { m_Logw->append(line); } } void Qonsole::execute() { QString cmdStr = m_InputArea->text() + " "; m_InputArea->setText(""); m_Logw->append(cmdStr); QByteArray bytes = cmdStr.toUtf8(); <-- 2 m_Bash->write(bytes); <-- 3 }
|
Example 12.8 shows the client code that launches this application.
Example 12.8. src/qonsole/qonsole1/qonsole.cpp
[ . . . . ] #include int main(int argc, char* argv[]) { QApplication app(argc, argv); Qonsole qon; qon.show(); return app.exec(); } |
12.1.3. Qonsole with Keyboard Events
In the preceding example, Qonsole had a separate widget for user input. For a more authentic xterm experience, the user should be able to type commands in the command output window. To accomplish this Qonsole needs to capture keyboard events. The first step is to override the QObject base class eventFilter() method, as we see in Example 12.9, the revised class definition.
Example 12.9. src/qonsole/keyevents/qonsole.h
[ . . . . ] class Qonsole : public QMainWindow { Q_OBJECT public: Qonsole(); public slots: void execute(); void showOutput(); bool eventFilter(QObject *o, QEvent *e) ; protected: void updateCursor(); private: QString m_UserInput; LogWindow* m_Logw; QProcess* m_Bash; }; [ . . . . ] |
As we discussed in Section 9.3, an event is an object derived from QEvent. Within the context of an application, such a QEvent is associated with a QObject that is its intended receiver. The receiving object has a handler to process the event. An eventFilter examines a QEvent and determines whether or not to permit it to be processed by the intended receiver. We have provided our revised Qonsole application with an eventFilter() function that will be used to filter keyboard events from m_Logw, an extended QTextEdit. QOnsole, an extended QMainWindow, is the intended recipient of those events. The implementation of this function is shown in Example 12.10.
Example 12.10. src/qonsole/keyevents/qonsole.cpp
[ . . . . ] bool Qonsole::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::KeyPress) { QKeyEvent *k = static_cast (e); int key = k->key(); QString str = k->text(); m_UserInput.append(str); updateCursor(); if ((key == Qt::Key_Return) || (key == Qt::Key_Enter) ) { execute(); return true; <-- 1 } else { m_Logw->insertPlainText(str); return true; } } return QMainWindow::eventFilter(o,e); <-- 2 }
|
Each time a key is pressed by the user the character generated by that key is appended to the mUserInput string and the position of the cursor in the LogWindow is adjusted by the member function updateCursor(). When the Enter key is pressed, the member function execute() is called so that the command string can be sent to the shell and then reset. Example 12.11 shows the implementation of these two functions.
Example 12.11. src/qonsole/keyevents/qonsole.cpp
[ . . . . ] bool Qonsole::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::KeyPress) { QKeyEvent *k = static_cast (e); int key = k->key(); QString str = k->text(); m_UserInput.append(str); updateCursor(); if ((key == Qt::Key_Return) || (key == Qt::Key_Enter) ) { execute(); return true; } else { m_Logw->insertPlainText(str); return true; <-- 1 } } return QMainWindow::eventFilter(o, e); <-- 2 }
|
All that remains to be done is to call the base class function installEventFilter() on mLogw, the widget whose events we want to capture. This is done in the constructor, as we see in Example 12.12.
Here is the controller code that sets up Qonsole.
Example 12.12. src/qonsole/keyevents/qonsole.cpp
[ . . . . ] Qonsole::Qonsole() { m_Logw = new LogWindow("debug"); setCentralWidget(m_Logw); m_Logw->installEventFilter(this); m_Logw->setLineWrapMode(QTextEdit::WidgetWidth); m_Bash = new QProcess(); m_Bash->setReadChannelMode(QProcess::MergedChannels); connect (m_Bash, SIGNAL(readyReadStandardOutput()), this, SLOT(showOutput())); m_Bash->start("bash", QStringList("-i"), QIODevice::ReadWrite); } |
Exercises: QProcess and Process Control
1. |
Modify Qonsole to support the backspace key. |
2. |
Modify Qonsole to support multiple simultaneous terminals in separate tabs. |
3. |
The command htpasswd[4] can be run from the shell, and it provides a command-line interface for encrypting and storing passwords. It stores a list of names and passwords separated by colons in a text file. The passwords are encrypted with a user-selectable protocol.
|
If htpasswd has been installed on your *nix system you can learn about its command-line options by viewing its manual page.
Write a Qt wrapper, called Users, around the htpasswd command. It should have the interface shown in the following figure.
The constructor should load all users and encrypted passwords into a QMap. addUser will require using QProcess to run htpasswd. checkPassword will use the in-memory map and the htpasswd -nb command to verify that the password is correct.
Write a graphical front-end to this applicationa login screen, as in the figure that follows.
Use message boxes to describe precisely any errors that occur during login. For example if the user enters the correct username but an incorrect password, the error message should be different from the one for an incorrect username. In a real-world security application, you would not have different error messages (no reason to help would-be hackers gaining access to our systems, right?) but we want a different message to help in testing.
If the user clicks Register, show another dialog (or modify the existing one) so that it asks for the password twice, to verify that the user entered it correctly.
The authors encountered some difficulties using the crypt()(-d) and MD5 (-m) encryption schemes for this problem, so we recommend using SHA (-s) encryption. |
Threads and QThread |
Part I: Introduction to C++ and Qt 4
C++ Introduction
Classes
Introduction to Qt
Lists
Functions
Inheritance and Polymorphism
Part II: Higher-Level Programming
Libraries
Introduction to Design Patterns
QObject
Generics and Containers
Qt GUI Widgets
Concurrency
Validation and Regular Expressions
Parsing XML
Meta Objects, Properties, and Reflective Programming
More Design Patterns
Models and Views
Qt SQL Classes
Part III: C++ Language Reference
Types and Expressions
Scope and Storage Class
Statements and Control Structures
Memory Access
Chapter Summary
Inheritance in Detail
Miscellaneous Topics
Part IV: Programming Assignments
MP3 Jukebox Assignments
Part V: Appendices
MP3 Jukebox Assignments
Bibliography
MP3 Jukebox Assignments