From 27b4f2939a680722754ad5d185b4f83aa12a75dc Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Fri, 26 Jun 2020 21:35:29 +0100 Subject: [PATCH 1/4] More flexible execution of the user's hardware controller Updated ways to implement a user defined hardware controller which is executed just after band changes during WSPR band hopping operation. Allows the user_hardware executable to be located in any directory on the PATH environment variable. On Windows any file extension listed on the PATHEXT environment variable may be used, the first match using PATH and PATHEXT will be executed. On Windows this is achieved by using CMD.EXE with a '/C' command line flag, i.e. the user's hardware controller is executed like this: CMD.EXE /C user_hardware nn where 'nn' is the new band as an integer in meters. On non-Windows systems the user's executable will be run if it is found on the directories specified by the PATH environment variable, and it is executable, i.e. it is equivalent to something like: /bin/sh -c user_hardware nn where 'nn' is the new band as an integer in meters. In all cases the user_hardware controller should exit with a zero status, otherwise it have been deemed to have failed. On Windows avoid an exit status of one as that is utilized by CMD.EXE to indicate the file was not found, which WSJT-X ignores silently. This change means the prior need to put the user's hardware controller into a WSJT-X installation directory like /usr/local/bin or C:\WSJT\wsjtx\bin is no longer necessary. --- widgets/mainwindow.cpp | 79 +++++++++++++++++++++++++++--------------- widgets/mainwindow.h | 3 +- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index 0ed391faa..d2fc5184b 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -662,7 +662,12 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, #endif connect(&proc_jt9, static_cast (&QProcess::finished), [this] (int exitCode, QProcess::ExitStatus status) { - subProcessFailed (&proc_jt9, exitCode, status); + if (subProcessFailed (&proc_jt9, exitCode, status)) + { + m_valid = false; // ensures exit if still + // constructing + QTimer::singleShot (0, this, SLOT (close ())); + } }); connect(&p1, &QProcess::started, [this] () { showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p1.program ()).arg (p1.arguments ().join (QLatin1String {"\" \""}))); @@ -680,25 +685,42 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, #endif connect(&p1, static_cast (&QProcess::finished), [this] (int exitCode, QProcess::ExitStatus status) { - subProcessFailed (&p1, exitCode, status); + if (subProcessFailed (&p1, exitCode, status)) + { + m_valid = false; // ensures exit if still + // constructing + QTimer::singleShot (0, this, SLOT (close ())); + } }); #if QT_VERSION < QT_VERSION_CHECK (5, 6, 0) connect(&p3, static_cast (&QProcess::error), [this] (QProcess::ProcessError error) { - subProcessError (&p3, error); - }); #else connect(&p3, &QProcess::errorOccurred, [this] (QProcess::ProcessError error) { - subProcessError (&p3, error); - }); #endif +#if not defined(Q_OS_WIN) + if (QProcess::FailedToStart != error) +#endif + { + subProcessError (&p3, error); + } + }); connect(&p3, &QProcess::started, [this] () { showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p3.program ()).arg (p3.arguments ().join (QLatin1String {"\" \""}))); }); connect(&p3, static_cast (&QProcess::finished), [this] (int exitCode, QProcess::ExitStatus status) { - subProcessFailed (&p3, exitCode, status); +#if defined(Q_OS_WIN) + // We forgo detecting user_hardware failures with exit + // code 1 on Windows. This is because we use CMD.EXE to + // run the executable. CMD.EXE returns exit code 1 when it + // can't find the target executable. + if (exitCode != 1) // CMD.EXE couldn't find file to execute +#endif + { + subProcessFailed (&p3, exitCode, status); + } }); // hook up save WAV file exit handling @@ -2335,7 +2357,7 @@ void MainWindow::setup_status_bar (bool vhf) } } -void MainWindow::subProcessFailed (QProcess * process, int exit_code, QProcess::ExitStatus status) +bool MainWindow::subProcessFailed (QProcess * process, int exit_code, QProcess::ExitStatus status) { if (m_valid && (exit_code || QProcess::NormalExit != status)) { @@ -2352,9 +2374,9 @@ void MainWindow::subProcessFailed (QProcess * process, int exit_code, QProcess:: , tr ("Running: %1\n%2") .arg (process->program () + ' ' + arguments.join (' ')) .arg (QString {process->readAllStandardError()})); - QTimer::singleShot (0, this, SLOT (close ())); - m_valid = false; // ensures exit if still constructing + return true; } + return false; } void MainWindow::subProcessError (QProcess * process, QProcess::ProcessError) @@ -2372,8 +2394,8 @@ void MainWindow::subProcessError (QProcess * process, QProcess::ProcessError) , tr ("Running: %1\n%2") .arg (process->program () + ' ' + arguments.join (' ')) .arg (process->errorString ())); - QTimer::singleShot (0, this, SLOT (close ())); m_valid = false; // ensures exit if still constructing + QTimer::singleShot (0, this, SLOT (close ())); } } @@ -7895,22 +7917,25 @@ void MainWindow::WSPR_scheduling () if (hop_data.frequencies_index_ >= 0) { // new band ui->bandComboBox->setCurrentIndex (hop_data.frequencies_index_); on_bandComboBox_activated (hop_data.frequencies_index_); - QStringList prefixes {".bat", ".cmd", ".exe", ""}; - QString target; - for (auto const& prefix : prefixes) - { - target = QDir {m_appDir}.absoluteFilePath (QLatin1String {"user_hardware"}); - QFileInfo f {target + prefix}; - if (f.isExecutable ()) { - break; - } - } - if (target.size ()) - { - // Execute user's hardware controller - p3.start(QDir::toNativeSeparators (target) - , QStringList {m_config.bands ()->find (m_freqNominal).remove ('m')}); - } + // Execute user's hardware controller + auto const& band = m_config.bands ()->find (m_freqNominal).remove ('m'); +#if defined(Q_OS_WIN) + // On windows we use CMD.EXE to find and execute the + // user_hardware executable. This means that the first matching + // file extension on the PATHEXT environment variable found on + // the PATH environment variable path list. This give maximum + // flexibility for users to write user_hardware in their + // language of choice, and place the file anywhere on the PATH + // environment variable. Equivalent to typing user_hardware + // without any path or extension at the CMD.EXE prompt. + p3.start("CMD", QStringList {QLatin1String {"/C"}, QLatin1String {"user_hardware"}, band}); +#else + // On non-Windows systems we expect the user_hardware executable + // to be anywhere in the paths specified in the PATH environment + // variable path list, and executable. Equivalent to typing + // user_hardware without any path at the shell prompt. + p3.start(QLatin1String {"user_hardware"}, QStringList {band}); +#endif // Produce a short tuneup signal m_tuneup = false; diff --git a/widgets/mainwindow.h b/widgets/mainwindow.h index 6da871b37..047f39205 100644 --- a/widgets/mainwindow.h +++ b/widgets/mainwindow.h @@ -3,6 +3,7 @@ #define MAINWINDOW_H #include +#include #include #include #include @@ -749,7 +750,7 @@ private: void rm_tb4(QString houndCall); void read_wav_file (QString const& fname); void decodeDone (); - void subProcessFailed (QProcess *, int exit_code, QProcess::ExitStatus); + bool subProcessFailed (QProcess *, int exit_code, QProcess::ExitStatus); void subProcessError (QProcess *, QProcess::ProcessError); void statusUpdate () const; void update_watchdog_label (); From b4bbc44d5b04615d29e024c223cb962a5242263c Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Mon, 27 Jul 2020 00:51:12 +0100 Subject: [PATCH 2/4] Invoke user_hardware correctly --- widgets/mainwindow.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index c0c2be4a4..323305287 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -670,7 +670,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, } }); connect(&p1, &QProcess::started, [this] () { - showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p1.program ()).arg (p1.arguments ().join (QLatin1String {"\" \""}))); + showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p1.program ()).arg (p1.arguments ().join ("\" \""))); }); connect(&p1, &QProcess::readyReadStandardOutput, this, &MainWindow::p1ReadFromStdout); #if QT_VERSION < QT_VERSION_CHECK (5, 6, 0) @@ -699,15 +699,17 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, #else connect(&p3, &QProcess::errorOccurred, [this] (QProcess::ProcessError error) { #endif -#if not defined(Q_OS_WIN) +#if !defined(Q_OS_WIN) if (QProcess::FailedToStart != error) +#else + if (QProcess::Crashed != error) #endif { subProcessError (&p3, error); } }); connect(&p3, &QProcess::started, [this] () { - showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p3.program ()).arg (p3.arguments ().join (QLatin1String {"\" \""}))); + showStatusMessage (QString {"Started: %1 \"%2\""}.arg (p3.program ()).arg (p3.arguments ().join ("\" \""))); }); connect(&p3, static_cast (&QProcess::finished), [this] (int exitCode, QProcess::ExitStatus status) { @@ -7913,7 +7915,7 @@ void MainWindow::WSPR_scheduling () { m_WSPR_tx_next = false; if (m_config.is_transceiver_online () // need working rig control for hopping - && !m_config.is_dummy_rig () + // && !m_config.is_dummy_rig () && ui->band_hopping_group_box->isChecked ()) { auto hop_data = m_WSPR_band_hopping.next_hop (m_auto); qDebug () << "hop data: period:" << hop_data.period_name_ @@ -7925,7 +7927,7 @@ void MainWindow::WSPR_scheduling () ui->bandComboBox->setCurrentIndex (hop_data.frequencies_index_); on_bandComboBox_activated (hop_data.frequencies_index_); // Execute user's hardware controller - auto const& band = m_config.bands ()->find (m_freqNominal).remove ('m'); + auto band = m_config.bands ()->find (m_freqNominal).remove ('m'); #if defined(Q_OS_WIN) // On windows we use CMD.EXE to find and execute the // user_hardware executable. This means that the first matching @@ -7935,13 +7937,13 @@ void MainWindow::WSPR_scheduling () // language of choice, and place the file anywhere on the PATH // environment variable. Equivalent to typing user_hardware // without any path or extension at the CMD.EXE prompt. - p3.start("CMD", QStringList {QLatin1String {"/C"}, QLatin1String {"user_hardware"}, band}); + p3.start("CMD", QStringList {"/C", "user_hardware", band}); #else // On non-Windows systems we expect the user_hardware executable // to be anywhere in the paths specified in the PATH environment // variable path list, and executable. Equivalent to typing // user_hardware without any path at the shell prompt. - p3.start(QLatin1String {"user_hardware"}, QStringList {band}); + p3.start("user_hardware", QStringList {band}); #endif // Produce a short tuneup signal From a2ad0be85b57ef6b7c6ce67d661cabe6eb59d4fb Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Mon, 27 Jul 2020 12:19:47 +0100 Subject: [PATCH 3/4] Use shell to execute user_hardware via PATH --- widgets/mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index 323305287..487f2cbf8 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -7943,7 +7943,7 @@ void MainWindow::WSPR_scheduling () // to be anywhere in the paths specified in the PATH environment // variable path list, and executable. Equivalent to typing // user_hardware without any path at the shell prompt. - p3.start("user_hardware", QStringList {band}); + p3.start("/bin/sh", QStringList {"-c", "user_hardware " + band}); #endif // Produce a short tuneup signal From bb927a611510f0f7e8fff95d0757a046bd1a8fde Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Mon, 27 Jul 2020 13:13:04 +0100 Subject: [PATCH 4/4] Handle missing user_hardware on non-Windows systems --- widgets/mainwindow.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index 487f2cbf8..dc3002462 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -719,6 +719,12 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, // run the executable. CMD.EXE returns exit code 1 when it // can't find the target executable. if (exitCode != 1) // CMD.EXE couldn't find file to execute +#else + // We forgo detecting user_hardware failures with exit + // code 127 non-Windows. This is because we use /bin/sh to + // run the executable. /bin/sh returns exit code 127 when it + // can't find the target executable. + if (exitCode != 127) // /bin/sh couldn't find file to execute #endif { subProcessFailed (&p3, exitCode, status);