Aplicaciones de consola con Qt

Aplicaciones de consola con Qt

Qt es un framework utilizado fundamentalmente para desarrollar aplicaciones con interfaz gráfica. Sin embargo, nada impide que también sea utilizado para crear aplicaciones de línea de comandos.

QCoreApplication

Al crear un proyecto de aplicación para consola, el asistente de Qt Creator crea un archivo main.cpp con un contenido similar al siguiente:

#include <QCoreApplication>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    return a.exec();
}

QCoreApplication es una clase que provee un bucle de mensaje para aplicaciones de consola, mientras que para aplicaciones gráficas lo adecuado es usar QApplication. El bucle de mensajes es iniciado con la invocación del método QCoreApplication::exec(), que no retorna hasta que la aplicación finaliza. Por ejemplo cuando el método QCoreApplication::quit() es llamado.

Si la aplicación no necesita de un bucle de mensajes, no es necesario instanciar QCoreApplication, pudiendo desarrollarla como cualquier programa convencional en C++, solo que beneficiándonos de las facilidades ofrecidas por las clases de Qt.

Las clases de Qt que requieren disponer de un bucle de mensajes son:

  • Controles, ventanas y en general todas las relacionadas con el GUI.

  • Temporizadores.

  • Comunicación entre hilos mediante señales.

  • Red. Si se usan los métodos síncronos waitFor* se puede evitar el uso del bucle de mensajes, pero hay que tener en cuenta que las clases de comunicaciones de alto nivel (QHttp, QFtp, etc.) no ofrecen dicha API.

Entrada estándar

Muchas aplicaciones de consola interactúan con el usuario a través de la entrada estándar, para lo cual se pueden usar tanto las clases de la librería estándar de C++:

std::string line;
std::getline(std::cint, line)

como los flujos de Qt:

QTextStream qtin(stdin);
QString line = qtin.readLine();

Sin embargo, es necesario tener presente que en ambos casos el hilo principal se puede bloquear durante la lectura —hasta que haya datos que leer— lo que impediría la ejecución del bucle de mensajes.

Para evitarlo se puede delegar la lectura de la entrada estándar a otro hilo, que se comunicaría con el principal para informar de las acciones del usuario a través del mecanismo de señales y slots de Qt. El procedimiento sería muy similar al que comentamos en una entrada anterior, solo que para leer la entrada estándar en lugar de para ordenar un vector de enteros.

Usando manejadores de señales POSIX con Qt

Los sistemas operativos compatibles con el estándar POSIX implementan un tipo de interrupción por software conocida como señales POSIX, que son enviadas a los procesos para informar de situaciones excepcionales durante la ejecución del programa. El problema es que las señales POSIX pueden llegar en cualquier momento, interrumpiendo así la secuencia normal de ejecución de instrucciones del proceso, lo que puede introducir problemas de concurrencia debido al posible acceso del manejador —o de funciones invocadas por el manejador— a datos que estén siendo manipulados por el programa en el momento en que es interrumpido.

Lo cierto es que sólo unas pocas funciones de la librería del sistema son seguras frente a señales POSIX. Y obviamente no hay seguridad de que las funciones del API de Qt lo sean, por lo que no podemos invocarlas directamente desde los manejadores de señal. Además, Qt no integra ninguna solución que encapsule y simplifique la gestión de señales POSIX, puesto que estas no están disponibles en sistemas operativos no POSIX y Qt solo implementa características portables entre sistemas operativos.

Aun así, en la documentación de Qt se describe una forma de usar señales POSIX, que en realidad es muy sencilla:

  • Basta con que al recibir la señal POSIX el manejador haga algo que provoque que Qt emita una señal, antes de retornar. Por ejemplo, escribir algunos bytes en un socket que está conectado a otro gestionado por Qt.

  • Al volver a la secuencia normal de ejecución del programa, tarde o temprano la aplicación volverá al bucle de mensajes. Entonces la condición será detectada —en el ejemplo anterior, sería que han llegado algunos bytes a través del socket que Qt gestiona— y se emitiría la señal de Qt correspondiente, que invocaría al slot al que está conectada, desde donde se podrían ejecutar de forma segura las operaciones que fueran necesarias.

Concretamente en el artículo Calling Qt Functions From Unix Signal Handlers se propone la solución esquematizada en la siguiente ilustración:

Solución de manejo de señales POSIX en Qt.

Así que comenzaremos declarando una clase que contenga los manejadores de señal, los slots y otros elementos que comentaremos posteriomente.

class MyDaemon : public QObject
{
    Q_OBJECT

public:
    explicit MyDaemon(QObject *parent = 0);
    ~MyDaemon();

    // Manejador POSIX de la señal SIGTERM.
    // Esta función será invocada cuando llegue la señal SIGTERM al
    // proceso. Debe ser static para poder pasar el método como
    // puntero de función a las llamadas signal() o sigaction()
    static void termSignalHandler(int unused);

public slots:
    // Slot donde se atenderá realmente la señal SIGTERM cuando la
    // convirtamos en la señal de Qt sigTermNotifier->activated()
    void handleSigTerm();

private:
    // Pareja de sockets conectados. Un par por señal a manejar.
    // En este caso es para la señal SIGTERM.
    // En uno de ellos escribiremos desde termSignalHandler(), el otro
    // será gestionado por Qt a través del objeto sigTermNotifier de
    // clase QSocketNotifier.
    static int sigTermSd[2];

    // Objeto para monitorizar uno de los sockets de sigTermSd[2]
    QSocketNotifier *sigTermNotifier;
};

En el constructor de la clase anterior, para cada señal que se quiere manejar, se usa la llamada al sistema socketpair() para crear una pareja de sockets de dominio UNIX anónimos conectados entre sí. Al estar conectados desde el principio, lo que se escribe en uno de los sockets de la pareja se puede leer en el otro. Además, se crea un objeto QSocketNotifier para que gestione uno de los sockets de la pareja, con el objeto de detectar cuándo hay datos disponibles para ser leídos, en cuyo caso QSocketNotifier envía la señal QSocketNotifier::activated().

MyDaemon::MyDaemon(QObject *parent) : QObject(parent)
{
    // Crear una la pareja de sockets de UNIX conectados
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sigTermSd)) {
      qFatal("Couldn't create TERM socketpair");
    }

    // Crear el objeto para monitorizar el segundo socket de la pareja.
    sigTermNotifier = new QSocketNotifier(sigTermSd[1],
        QSocketNotifier::Read, this);

    // Conectar la señal activated() del objeto QSocketNotifier con el
    // slot handleSigTerm() para manejar la señal. Esta señal será
    // emitida cuando hayan datos para ser leidos en el socket
    // monitorizado.
    connect(sigTermNotifier, SIGNAL(activated(int)), this, SLOT(handleSigTerm()));
}

Entonces el manejador de señal POSIX lo único que tiene que hacer cuando es invocado es escribir algo en el socket que no gestiona QSocketNotifier.

//
// Manejador de la señal POSIX SIGTERM.
//
void MyDaemon::termSignalHandler(int)
{
    // Con enviar un byte es suficiente.
    char a = 1;
    write(sigTermSd[0], &a, sizeof(a));
}

Mientras que en el slot al que conectamos la señal QSocketNotifier::activated() se lee lo escrito desde el manejador anterior en el socket, para después pasar a tratar la señal como creamos conveniente. Como este slot es invocado desde el bucle de mensajes de la aplicación, podemos hacer cualquier acción que nos venga en gana, al contrario de lo que pasa dentro de un manejador de señal POSIX como MyDaemon::termSignalHandler(), donde solo podemos invocar unas pocas funciones.

void MyDaemon::handleSigTerm()
{
    // Desactivar la monitorización del socket para que por el momento
    // no lleguen más señales de Qt
    termNotifier->setEnabled(false);

    // Leer y desechar el byte enviado por MyDaemon::termSignalHandler()
    char tmp;
    read(sigTermSd[1], &tmp, sizeof(tmp));

    // ...tu código aquí...

    // Todos las acciones que quieras hacer para responder convenientemente
    // a la señal SIGTERM van aquí.
    // P. ej. invocar qApp->quit() para detener la aplicación.

    // Activar la monitorización para que vuelvan a llegar señales de Qt
    termNotifier->setEnabled(true);
}

Por conveniencia, podemos añadir una función para asignar el manejador MyDaemon::termSignalHandler a la señal SIGTERM usando la llamada al sistema sigaction(). Recordemos que los métodos que se van a utilizar como manejadores se declaran como static para que puedan ser pasados como un puntero de función a la llamada al sistema sigaction().

bool setupUnixSignalHandlers()
{
    sigaction term;

    // Establecer el puntero al método encargado de manejar la señal
    term.sa_handler = &MyDaemon::termSignalHandler;

    // Vaciamos el conjunto de señales en sa_mask para indicar que no queremos
    // bloquear la llegada de ninguna señal POSIX mientras manejamos SIGTERM.
    sigemptyset(&term.sa_mask);

    // El flag SA_RESTART indica que si la señal interrumpe alguna llamada al
    // sistema, al volver del manejador la llamada al sistema debe continuar
    // y no terminar con un error. Esto es importante para no afectar al
    // comportamiento esperado por Qt de las llamadas al sistema.
    term.sa_flags = SA_RESTART;

    // Establecer manejador de la señal SIGTERM
    if (sigaction(SIGTERM, &term, 0) > 0)
        return false;

    return true;
}