Qtの基礎 - プロセス

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

QProcessクラスは、外部プロセスを起動して、それらと通信するためのQtのクラスである。
外部プログラムの実行、入出力の制御、プロセスの状態監視等を行うことができる。

QProcessクラスはQIODeviceクラスを継承しているため、標準的なQt入出力機能を利用できる。
これにより、readメソッドやwriteメソッド等の操作が可能であり、外部プロセスとのデータのやり取りが容易になる。

プロセスの起動方法は、startメソッドを使用する。

 QProcess process;
 process.start("notepad.exe", QStringList());


プロセスの実行状態は、様々なシグナルを通じて監視することができる。
主要なシグナルとしては、プロセスが開始された時のstartedメソッド、終了した時のfinishedメソッド、エラーが発生した時のerrorOccurredメソッド等がある。

標準入出力とエラー出力の制御も可能である。
setStandardOutputProcessメソッドを使用して、あるプロセスの標準出力を別のプロセスの標準入力にパイプすることができる。
また、setProcessChannelModeメソッドを使用して、標準出力とエラー出力の扱い方を制御することも可能である。

エラーハンドリングでは、errorメソッドを使用してエラーの種類を確認することができる。
一般的なエラーとしては、プロセスが見つからない、アクセス権限がない、タイムアウト等がある。

プロセスの終了制御も柔軟であり、通常の終了要求であるterminateメソッドや強制終了のkillメソッドが用意されている。
また、waitForFinishedメソッドを使用して、プロセスの終了を待機することもできる。

環境変数の制御も可能で、setEnvironmentメソッドやsetWorkingDirectoryメソッドを使用して、プロセスの実行環境をカスタマイズできる。


QProcess::ExitStatus列挙体

QProcess::ExitStatus列挙体
指定可能な値 説明
QProcess::NormalExit
  • プロセスが正常に終了した場合
  • プロセスが自身で終了処理を実行して、正常に完了した状態


例: 外部コマンドがreturn 0やexit(0)で終了した場合は、exitCodeの値で具体的な終了状態を確認できる。

QProcess::CrashExit
  • プロセスが異常終了 (クラッシュ) した場合
  • プロセスが予期せず終了した状態


クラッシュ例:

  • セグメンテーション違反
  • スタックオーバーフロー
  • 実行時エラーでのクラッシュ
  • OSによるプロセスの強制終了
  • killシグナルによる強制終了


これらの状態を監視することにより、外部プロセスの終了の状態 (正常あるいは異常) を判断して、適切なエラーハンドリングを実装することができる。

 // 使用例
 QObject::connect(process, &QProcess::finished, [=](int exitCode, QProcess::ExitStatus exitStatus) {
    if (exitStatus == QProcess::NormalExit) {
       // プロセスが正常に終了した場合
       if (exitCode == 0) qDebug() << "プロセスは正常に終了しました";
       else               qDebug() << "プロセスはエラーコード" << exitCode << "で終了しました";
    }
    else {
       // QProcess::CrashExit (プロセスがクラッシュした場合)
       qDebug() << "プロセスがクラッシュしました";
    }
 });



プロセスの非同期実行

プロセスを非同期で実行する場合はQProcess::startメソッド、同期して実行する場合はQProcess.executeメソッドを使用する。
また、非同期で実行する場合、標準出力や標準エラー出力を取得してウィンドウに表示することもできる。

以下の例は、プロセスを非同期で実行して、プロセスの標準出力と標準エラー出力をウィンドウに表示している。

 // MainWindow.h
 
 #ifndef MAINWINDOW_H
 #define MAINWINDOW_H
 
 #include <QMainWindow>
 #include <QProcess>
 
 QT_BEGIN_NAMESPACE
 namespace Ui { class MainWindow; }
 QT_END_NAMESPACE
 
 class MainWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    QProcess m_proc;
  
 public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
 
 private slots:
    // プロセス用のスロット
    void ProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
    void UpdateOutput();
    void UpdateError();
 };
 #endif // MAINWINDOW_H


 // MainWindow.cpp
 
 #include <QApplication>
 #include <QProcess>
 #include "Mainwindow.h"
 #include "ui_Mainwindow.h"
 
 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
 {
    ui->setupUi(this);
 
    // プロセス用のシグナルとスロットを接続する
    // Qt 4の記述方法
    //QObject::connect(&m_proc, SIGNAL(readyReadStandardOutput()), this, SLOT(UpdateOutput()));
    //QObject::connect(&m_proc, SIGNAL(readyReadStandardError()), this, SLOT(UpdateError()));
    //QObject::connect(&m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(ProcessFinished(int, QProcess::ExitStatus)));

    // Qt 5の規約
    QObject::connect(&m_proc, &QProcess::readyReadStandardOutput, this, &MainWindow::UpdateOutput);
    QObject::connect(&m_proc, &QProcess::readyReadStandardError, this, &MainWindow::UpdateError);
    QObject::connect(&m_proc, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &MainWindow::ProcessFinished);
    
    // Qt 5 (C++14以降)
    //QObject::connect(&m_proc, qOverload<int, QProcess::ExitStatus >(&QProcess::finished), this, &MainWindow::ProcessFinished);
 }
 
 MainWindow::~MainWindow()
 {
    delete ui;
 }
 
 // スロット : 標準出力を表示する
 void MainWindow::UpdateOutput()
 {
    // 標準出力を取得して文字列にする
    QByteArray output = m_proc.readAllStandardOutput();
    QString str = QString::fromLocal8Bit(output);
 }
 
 // スロット : 標準エラー出力を表示する
 void MainWindow::UpdateError()
 {
    // 標準エラー出力を取得して文字列にする
    QByteArray output = m_proc.readAllStandardError();
    QString str = QString::fromLocal8Bit(output);
 }
 
 // スロット : プロセス終了時
 void MainWindow::ProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
 {
    if(exitStatus == QProcess::CrashExit)
    {
       QMessageBox::warning(this, tr("Error"), tr("Crashed"));
    }
    else if(exitCode != 0)
    {
       QMessageBox::warning(this, tr("Error"), tr("Failed"));
    }
    else
    {  // 正常終了時の処理
    }
 }
 
 // プロセスの起動
 void MainWindow::ProcessStart()
 {
    QStringList listArgs;
    listArgs << "<引数1>" << "<引数2>";
 
    m_proc.start("<実行するプログラムのパス>", listArgs);
 }


また、QProcessクラスのwaitForFinishedメソッドは、プロセスが終了するまで待機するメソッドである。
プロセスが正常に終了した場合はtrue、そうでない場合(操作のタイムアウト、エラーの発生、既にQProcessのオブジェクトが終了している)はfalseとなる。

 QProcess proc;
 proc.start("/usr/bin/sh" args);
 
 bool bStatus = proc.waitForFinished(10 * 1000);  // 最大10[s]待機
 if (m_proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0)
 {
    std::fprintf(stderr, "error : %s", proc.readAllStandardError().constData());
    return -1;
 }



プロセスの同期実行

以下の例では、whichコマンドを実行して、whoamiコマンドが存在しているかどうかを確認している。

 #include <QProcess>
 
 QProcess process;
 process.start("which", QStringList() << "whoami");
 process.waitForFinished();
 
 // process.exitCode()メソッドの戻り値が0の場合は、whoamiコマンドが存在している
 // 0以外の戻り値の場合は、whoamiコマンドは存在していない
 auto iRet = process.exitCode();


以下の例では、外部コマンドとしてwhoamiコマンドを実行して、その結果であるユーザ名を取得している。

 #include <QProcess>
 
 // 現在のユーザ名を取得
 QProcess process;
 process.start("whoami");
 process.waitForFinished();
 
 // process.exitCode()メソッドの戻り値が0の場合は、whoamiコマンドが正常に実行されている
 // 0以外の戻り値の場合は、whoamiコマンドの実行に失敗している
 if (process.exitCode() == 0) {
    QString currentUser = QString(process.readAllStandardOutput()).trimmed();
 }


余談ではあるが、Linuxシステムでは、C++の標準ライブラリとPOSIX APIを使用してユーザ名を直接取得することができる。

 #include <unistd.h>
 #include <pwd.h>
 
 uid_t uid = geteuid();
 struct passwd *pw = getpwuid(uid);
 if (pw) {
    auto str = QString::fromLocal8Bit(pw->pw_name);
 }


このアプロ―チの利点として、以下に示すものが挙げられる。

  • 外部コマンドに依存しないため、whoamiコマンドが利用できないシステムでも動作する。
  • プロセスの起動オーバーヘッドがないため、より高速である。
  • セキュリティの観点からも、外部コマンドを実行するよりも安全である。


※注意

  • この方法は、POSIX準拠のシステム (Linuxを含む多くのUnixライクシステム) でのみ動作するため、Windowsでは異なるアプローチが必要となる。
  • unistd.hファイルとpwd.hファイルのヘッダファイルを含める必要がある。
    これらは標準的なPOSIXヘッダであるが、クロスプラットフォーム開発の場合は注意が必要である。



切り離されたプロセス

QProcess::startDetachedメソッド

Qt 5.10以降、QProcessクラスを使用して、プロセスを切り離して実行できる。

startメソッドやexecuteメソッドでは、プロセスを切り離さずに実行するため、QProcessクラスのデストラクタがプロセスを終了する。
これに対して、切り離されたプロセスは、呼び出したプロセスが終了しても影響を受けずに実行し続ける。

Linuxでは、切り離されたプロセスは独自のセッションで実行されて、デーモンのように動作する。

切り離されたプロセスを実行するには、QProcess::startDetachedメソッドを使用する。
なお、QProcessクラスのstartDetachedメソッドは、静的メソッドであることに注意する。

以下の例では、aplayコマンドを使用して、Sample.wavを再生している。

 QProcess::startDetached("aplay Sample.wav");


通常のプロセスと同様に、複数の引数と作業ディレクトリを渡すことができる。
また、開始されたプロセスのPIDを取得することができる。

 // プロセスの開始
 qint64 pid;
 QProcess::startDetached("mpg123", {"Sample.mp3", <引数2>, <引数3>, ...}, musicDirPath, &pid);
 // ...略


QProcess::startDetachedメソッドで取得したPIDを使用して、プロセスをkillすることもできる。

 // プロセスの終了
 QProcess::startDetached("kill", {QString::number(pid)});


切り離されたプロセスを実行するにあたり、プロセスの出力を制御することもできる。
(使用するコマンドの中には、--quietオプションが存在するものもある)

  1. プロセス環境を設定する。
  2. stdinstdoutstderrをファイルにリダイレクトする。
  3. ネイティブな引数を設定、および、切り離されたプロセス用のCreateProcess関数(Windows API)の引数の設定


ただし、staticstartDetachedメソッドではなく、非staticQProcessクラスのstartDetachedメソッドを使用する必要がある。
これは、QProcessオブジェクトのプロパティを読み込み、それに応じて切り離されたプロセスを開始する。

以下の例では、mpg123コマンドを使用して、Sample.mp3を再生している。
/dev/nullへのリダイレクトは、ユーザがオーディオプレーヤの視覚的な出力を非表示にしている。

 QProcess process;
 
 process.setProgram("mpg123");
 process.setArguments({"Sample.mp3"});
 process.setWorkingDirectory(musicDirPath);  // 例. ファイルが存在するディレクトリのパス(/home/hpge/music等)
 
 process.setStandardOutputFile(QProcess::nullDevice());  // /dev/nullへのリダイレクト
 process.setStandardErrorFile(QProcess::nullDevice());   // /dev/nullへのリダイレクト
 
 qint64 pid;
 process.startDetached(&pid);
 
 // ...略


以下に、startDetachedメソッドでサポートされているサブセットを示す。
サブセットを使用する場合、QProcessクラスの他の全てのプロパティは無視されることに注意すること。

  • setArgumentsメソッド
  • setCreateProcessArgumentsModifierメソッド
  • setNativeArgumentsメソッド
  • setProcessEnvironmentメソッド
  • setProgramメソッド
  • setStandardErrorFileメソッド
  • setStandardInputFileメソッド
  • setStandardOutputFileメソッド
  • setWorkingDirectoryメソッド


プロセスの完了を検知

QProcess::startDetachedメソッドを使用した場合、プロセスの完了通知を直接受け取ることはできない。
これは、QProcess::startDetachedメソッドはデタッチ (切り離し) されたプロセスを起動するため、親プロセス (アプリケーション) との接続が切られるためである。

そのため、QProcess::startDetachedメソッドは使用せずに、QProcessクラスのstartメソッドをバッググラウンドで実行する必要がある。

 // プロセスを定義
 QProcess *process = new QProcess();
 process->setProgram("aplay");
 process->setArguments(QStringList() << "Sample.wav");
 
 // プロセスを非同期実行 (バックグラウンド) で実行しつつ、完了通知を受け取る
 QObject::connect(process, &QProcess::finished, [=](int exitCode, QProcess::ExitStatus exitStatus) {
    process->deleteLater();  // メモリリーク防止
 
    // 何らかの処理
    // ...略
 });
 
 process->start();


 // クラス内でシグナル / スロットを定義する場合 (ラムダ式の部分をクラスのメソッドとして記述することも可能)
 
 // プロセスを定義
 m_Process = new QProcess(this);
 Process->setProgram("aplay");
 m_Process->setArguments(QStringList() << "Sample.wav");
 
 connect(m_Process, &QProcess::finished, this, &SomeClass::someMethod);
 
 // SomeClassクラスのsomeMethodメソッド
 class someClass : public QObject
 {
 private:
    QProcess *m_Process;
 
 private:
    void someMethod(int exitCode, QProcess::ExitStatus exitStatus)
    {
       m_Process->deleteLater();  // メモリリーク防止
 
       // 何らかの処理
       // ...略
    });
 }