Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 85 additions & 4 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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<MainWindow*>(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<int>(lsl::local_clock() - startTime);
QString recFilename = replaceFilename(QDir::cleanPath(ui->lineEdit_template->text()));
auto fileinfo = QFileInfo(QDir::cleanPath(ui->rootEdit->text()) + '/' + recFilename);
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -304,7 +341,8 @@ std::vector<lsl::stream_info> 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)); }
}
}
Expand Down Expand Up @@ -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 += "&nbsp;&nbsp;&bull; <span style='color: #0066cc;'>" + s + "</span><br>";
}
if (!localSecurityEnabled) {
errorMsg = "The following streams require security, but Lab Recorder does not have "
"security credentials configured:<br><br>" + streamList +
"<br>To fix this:<br>"
"&nbsp;&nbsp;1. Run 'lsl-keygen' to generate credentials, or<br>"
"&nbsp;&nbsp;2. Import shared credentials from an authorized device<br><br>"
"<span style='color: red; font-weight: bold;'>Recording cannot proceed with mismatched security settings.</span>";
} else {
errorMsg = "Security mismatch detected for the following streams:<br><br>" +
streamList +
"<br>All devices must have the same security configuration "
"(either all secure or all insecure).<br><br>"
"<span style='color: red; font-weight: bold;'>Recording cannot proceed with mismatched security settings.</span>";
}
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.
Expand Down
18 changes: 12 additions & 6 deletions src/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};


Expand All @@ -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<lsl::stream_info> refreshStreams(void);
Expand Down
32 changes: 30 additions & 2 deletions src/recording.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::mutex> 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<StreamError> recording::getErrors() {
std::lock_guard<std::mutex> lock(error_mut_);
std::vector<StreamError> result(errors_.begin(), errors_.end());
errors_.clear();
return result;
}

bool recording::hasErrors() const {
std::lock_guard<std::mutex> lock(error_mut_);
return !errors_.empty();
}

void recording::record_from_query_results(const std::string &query) {
try {
std::set<std::string> known_uids; // set of previously seen stream uid's
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/recording.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <iostream>
#include <list>
#include <lsl_cpp.h>
Expand Down Expand Up @@ -43,6 +44,12 @@ using offset_list = std::list<std::pair<double, double>>;
// a map from streamid to offset_list
using offset_lists = std::map<streamid_t, offset_list>;

// 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.
Expand Down Expand Up @@ -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<StreamError> 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
Expand Down Expand Up @@ -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<StreamError> errors_;
mutable std::mutex error_mut_;

// data for shutdown / final joining
std::list<thread_p> stream_threads_; // the spawned stream handling threads
thread_p boundary_thread_; // the spawned boundary-recording thread
Expand Down