diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index aec937f..38d63bf 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -45,7 +45,16 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) connect(ui->actionAbout, &QAction::triggered, this, [this]() { QString infostr = QStringLiteral("LSL library version: ") + QString::number(lsl::library_version()) + - "\nLSL library info:" + lsl::library_info(); + "\nLSL library info: " + lsl::library_info(); + // Add security version information if available + if (lsl::is_secure_build()) { + infostr += "\n\nSecurity: Enabled"; + infostr += "\nBase version: " + QString(lsl::base_version()); + infostr += "\nSecurity version: " + QString(lsl::security_version()); + infostr += "\nFull version: " + QString(lsl::full_version()); + } else { + infostr += "\n\nSecurity: Not available"; + } QMessageBox::about(this, "About this app", infostr); }); @@ -94,8 +103,35 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) load_config(cfgfilepath); } -void MainWindow::statusUpdate() const { +void MainWindow::statusUpdate() { if (currentRecording) { + // Check for errors from the recording threads + auto errors = currentRecording->getErrors(); + for (const auto& error : errors) { + QString errorMsg = QString::fromStdString(error.error_message); + QString streamName = QString::fromStdString(error.stream_name); + + if (error.is_security_error) { + // Extract the most relevant part of the security error message + QString displayMsg = errorMsg; + if (errorMsg.contains("403")) { + // Extract the 403 message for cleaner display + int idx = errorMsg.indexOf("403"); + if (idx >= 0) { + displayMsg = errorMsg.mid(idx); + } + } + // Show security errors prominently + QMessageBox::warning(const_cast(this), + "Security Error - " + streamName, + "Failed to connect to stream '" + streamName + "':\n\n" + displayMsg + + "\n\nMake sure all devices have matching security configurations."); + } else { + // Log non-security errors to console + qWarning() << "Stream error for" << streamName << ":" << errorMsg; + } + } + auto elapsed = static_cast(lsl::local_clock() - startTime); QString recFilename = replaceFilename(QDir::cleanPath(ui->lineEdit_template->text())); auto fileinfo = QFileInfo(QDir::cleanPath(ui->rootEdit->text()) + '/' + recFilename); @@ -284,7 +320,8 @@ void MainWindow::save_config(QString filename) { } QString info_to_listName(const lsl::stream_info& info) { - return QString::fromStdString(info.name() + " (" + info.hostname() + ")"); + QString suffix = info.security_enabled() ? QString::fromUtf8(" \xF0\x9F\x94\x92") : QString(""); // Lock emoji at end + return QString::fromStdString(info.name() + " (" + info.hostname() + ")") + suffix; } /** @@ -304,7 +341,8 @@ std::vector MainWindow::refreshStreams() { } if (!known) { bool found = missingStreams.contains(info_to_listName(s)); - knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found); + knownStreams << StreamItem(s.name(), s.type(), s.source_id(), s.hostname(), found, + s.security_enabled(), s.security_fingerprint()); if (found) { missingStreams.remove(info_to_listName(s)); } } } @@ -392,6 +430,49 @@ void MainWindow::startRecording() { msgBox.setDefaultButton(QMessageBox::No); if (msgBox.exec() != QMessageBox::Yes) return; } + + // Check for security mismatches between local config and selected streams + bool localSecurityEnabled = lsl::local_security_enabled(); + QStringList securityMismatchStreams; + for (const auto& stream : requestedAndAvailableStreams) { + bool streamSecure = stream.security_enabled(); + if (streamSecure && !localSecurityEnabled) { + // Stream requires security but local security is not enabled + securityMismatchStreams.append(QString::fromStdString(stream.name())); + } else if (!streamSecure && localSecurityEnabled) { + // Stream is insecure but local security is enabled + securityMismatchStreams.append(QString::fromStdString(stream.name()) + " (insecure)"); + } + } + if (!securityMismatchStreams.isEmpty()) { + QString errorMsg; + // Format stream names as a bulleted list with blue color + QString streamList; + for (const auto& s : securityMismatchStreams) { + streamList += "  • " + s + "
"; + } + if (!localSecurityEnabled) { + errorMsg = "The following streams require security, but Lab Recorder does not have " + "security credentials configured:

" + streamList + + "
To fix this:
" + "  1. Run 'lsl-keygen' to generate credentials, or
" + "  2. Import shared credentials from an authorized device

" + "Recording cannot proceed with mismatched security settings."; + } else { + errorMsg = "Security mismatch detected for the following streams:

" + + streamList + + "
All devices must have the same security configuration " + "(either all secure or all insecure).

" + "Recording cannot proceed with mismatched security settings."; + } + QMessageBox msgBox(this); + msgBox.setWindowTitle("Security Mismatch"); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setTextFormat(Qt::RichText); + msgBox.setText(errorMsg); + msgBox.exec(); + return; + } } // don't hide critical errors. diff --git a/src/mainwindow.h b/src/mainwindow.h index abeaad4..588c659 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -19,18 +19,24 @@ class recording; class RemoteControlSocket; class StreamItem { - + public: StreamItem(std::string stream_name, std::string stream_type, std::string source_id, - std::string hostname, bool required) - : name(stream_name), type(stream_type), id(source_id), host(hostname), checked(required) {} - - QString listName() { return QString::fromStdString(name + " (" + host + ")"); } + std::string hostname, bool required, bool secure = false, std::string fingerprint = "") + : name(stream_name), type(stream_type), id(source_id), host(hostname), + checked(required), security_enabled(secure), security_fingerprint(fingerprint) {} + + QString listName() { + QString prefix = security_enabled ? QString::fromUtf8("\xF0\x9F\x94\x92 ") : QString(""); // Lock emoji + return prefix + QString::fromStdString(name + " (" + host + ")"); + } std::string name; std::string type; std::string id; std::string host; bool checked; + bool security_enabled; + std::string security_fingerprint; }; @@ -42,7 +48,7 @@ class MainWindow : public QMainWindow { ~MainWindow() noexcept override; private slots: - void statusUpdate(void) const; + void statusUpdate(void); void closeEvent(QCloseEvent *ev) override; void blockSelected(const QString &block); std::vector refreshStreams(void); diff --git a/src/recording.cpp b/src/recording.cpp index 0b8b43e..f2ad39c 100644 --- a/src/recording.cpp +++ b/src/recording.cpp @@ -121,6 +121,30 @@ void recording::requestStop() noexcept shutdown_ = true; } +void recording::reportError(const std::string& stream_name, const std::string& error_msg) { + std::lock_guard lock(error_mut_); + // Check if this is a security-related error + bool is_security = (error_msg.find("403") != std::string::npos) || + (error_msg.find("security") != std::string::npos) || + (error_msg.find("Security") != std::string::npos) || + (error_msg.find("encryption") != std::string::npos) || + (error_msg.find("public key") != std::string::npos) || + (error_msg.find("Public key") != std::string::npos); + errors_.push_back({stream_name, error_msg, is_security}); +} + +std::vector recording::getErrors() { + std::lock_guard lock(error_mut_); + std::vector result(errors_.begin(), errors_.end()); + errors_.clear(); + return result; +} + +bool recording::hasErrors() const { + std::lock_guard lock(error_mut_); + return !errors_.empty(); +} + void recording::record_from_query_results(const std::string &query) { try { std::set known_uids; // set of previously seen stream uid's @@ -154,7 +178,9 @@ void recording::record_from_query_results(const std::string &query) { // wait for all our threads to join timed_join_or_detach(threads, max_join_wait); } catch (std::exception &e) { - std::cout << "Error in the record_from_query_results thread: " << e.what() << std::endl; + std::string error_msg = e.what(); + std::cout << "Error in the record_from_query_results thread: " << error_msg << std::endl; + reportError("query: " + query, error_msg); } } @@ -277,7 +303,9 @@ void recording::record_from_streaminfo(const lsl::stream_info &src, bool phase_l throw; } } catch (std::exception &e) { - std::cout << "Error in the record_from_streaminfo thread: " << e.what() << std::endl; + std::string error_msg = e.what(); + std::cout << "Error in the record_from_streaminfo thread: " << error_msg << std::endl; + reportError(src.name(), error_msg); } } diff --git a/src/recording.h b/src/recording.h index 0b198ba..0446ee8 100644 --- a/src/recording.h +++ b/src/recording.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,12 @@ using offset_list = std::list>; // a map from streamid to offset_list using offset_lists = std::map; +// Structure to hold stream errors +struct StreamError { + std::string stream_name; + std::string error_message; + bool is_security_error; +}; /** * A recording process using the lab streaming layer. @@ -74,7 +81,17 @@ class recording { void requestStop() noexcept; + /// Get and clear any errors that occurred during recording + /// Thread-safe, returns errors since last call + std::vector getErrors(); + + /// Check if there are any errors without removing them + bool hasErrors() const; + private: + /// Report an error from a recording thread + void reportError(const std::string& stream_name, const std::string& error_msg); + // the file stream XDFWriter file_; // the file output stream // static information @@ -104,6 +121,10 @@ class recording { offset_lists_; // the clock offset lists for each stream (to be written into the footer) std::mutex offset_mut_; // a mutex to protect the offset lists + // error reporting for the UI + std::deque errors_; + mutable std::mutex error_mut_; + // data for shutdown / final joining std::list stream_threads_; // the spawned stream handling threads thread_p boundary_thread_; // the spawned boundary-recording thread