#include "tools/cabana/settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #include #include #include #include #include #include #include "json11/json11.hpp" #include "tools/cabana/utils/util.h" const int MIN_CACHE_MINIUTES = 30; const int MAX_CACHE_MINIUTES = 120; Settings settings; namespace { std::filesystem::path settingsFile() { return utils::configPath() / "cabana.json"; } struct LoadedSettings { json11::Json::object values; bool exists = false; bool valid = true; }; class FileLock { public: explicit FileLock(const std::filesystem::path &path) { fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600); if (fd < 0 || flock(fd, LOCK_EX) < 0) { fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno)); if (fd >= 0) close(fd); fd = -1; } } ~FileLock() { if (fd >= 0) close(fd); } bool isLocked() const { return fd >= 0; } private: int fd = -1; }; LoadedSettings loadSettings() { std::ifstream input(settingsFile()); if (!input) return {}; const std::string contents{std::istreambuf_iterator(input), std::istreambuf_iterator()}; std::string error; auto settings_json = json11::Json::parse(contents, error); if (!error.empty() || !settings_json.is_object()) { fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str()); return {.exists = true, .valid = false}; } return {.values = settings_json.object_items(), .exists = true}; } bool ensureSettingsDirectory() { const auto path = settingsFile(); std::error_code error; std::filesystem::create_directories(path.parent_path(), error); if (error) { fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str()); return false; } return true; } bool writeAll(int fd, const std::string &data) { size_t written = 0; while (written < data.size()) { ssize_t result = write(fd, data.data() + written, data.size() - written); if (result < 0 && errno == EINTR) continue; if (result <= 0) return false; written += result; } return true; } bool saveSettings(const json11::Json::object &settings_json) { const auto path = settingsFile(); const std::string contents = json11::Json(settings_json).dump(); std::string temporary_path = path.string() + ".tmp.XXXXXX"; int fd = mkstemp(temporary_path.data()); if (fd < 0) { fprintf(stderr, "failed to create temporary Cabana settings %s: %s\n", temporary_path.c_str(), strerror(errno)); return false; } bool success = writeAll(fd, contents) && fsync(fd) == 0; if (close(fd) < 0) success = false; if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false; if (success) { int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC); success = dir_fd >= 0 && fsync(dir_fd) == 0; if (dir_fd >= 0 && close(dir_fd) < 0) success = false; } if (!success) { const int saved_errno = errno; unlink(temporary_path.c_str()); fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno)); } return success; } bool preserveCorruptSettings() { const auto path = settingsFile(); auto backup = path; backup += ".corrupt"; for (int i = 1; std::filesystem::exists(backup); ++i) { backup = path; backup += ".corrupt." + std::to_string(i); } if (rename(path.c_str(), backup.c_str()) < 0) { fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno)); return false; } fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str()); return true; } // TODO: Remove the legacy QSettings migration after users have had time to migrate to cabana.json. struct LegacyValue { std::vector strings; std::string bytes; bool is_byte_array = false; }; using LegacySettings = std::map; int hexDigit(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } #ifndef __APPLE__ void appendUtf8(std::string &result, uint32_t codepoint) { if (codepoint <= 0x7f) { result.push_back(codepoint); } else if (codepoint <= 0x7ff) { result.push_back(0xc0 | (codepoint >> 6)); result.push_back(0x80 | (codepoint & 0x3f)); } else if (codepoint <= 0xffff) { result.push_back(0xe0 | (codepoint >> 12)); result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); result.push_back(0x80 | (codepoint & 0x3f)); } else { result.push_back(0xf0 | (codepoint >> 18)); result.push_back(0x80 | ((codepoint >> 12) & 0x3f)); result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); result.push_back(0x80 | (codepoint & 0x3f)); } } LegacyValue decodeIniValue(std::string_view encoded) { std::vector> decoded(1); std::vector quoted(1, false); bool in_quotes = false; for (size_t i = 0; i < encoded.size();) { char c = encoded[i++]; if (c == '"') { in_quotes = !in_quotes; quoted.back() = true; } else if (c == ',' && !in_quotes) { decoded.emplace_back(); quoted.push_back(false); while (i < encoded.size() && (encoded[i] == ' ' || encoded[i] == '\t')) ++i; } else if (c == '\\' && i < encoded.size()) { c = encoded[i++]; static const std::map escapes = { {'a', '\a'}, {'b', '\b'}, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'}, {'v', '\v'}, {'"', '"'}, {'?', '?'}, {'\'', '\''}, {'\\', '\\'}, }; if (auto it = escapes.find(c); it != escapes.end()) { decoded.back().push_back(static_cast(it->second)); } else if (c == 'x' && i < encoded.size() && hexDigit(encoded[i]) >= 0) { uint32_t value = 0; while (i < encoded.size() && hexDigit(encoded[i]) >= 0) value = (value << 4) + hexDigit(encoded[i++]); decoded.back().push_back(value & 0xffff); } else if (c >= '0' && c <= '7') { uint32_t value = c - '0'; while (i < encoded.size() && encoded[i] >= '0' && encoded[i] <= '7') value = (value << 3) + (encoded[i++] - '0'); decoded.back().push_back(value & 0xffff); } } else { decoded.back().push_back(static_cast(c)); } } LegacyValue result; for (size_t i = 0; i < decoded.size(); ++i) { auto &value = decoded[i]; if (!quoted[i]) { while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin()); while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back(); } std::string string_value; for (size_t j = 0; j < value.size(); ++j) { uint32_t codepoint = value[j]; if (codepoint >= 0xd800 && codepoint <= 0xdbff && j + 1 < value.size() && value[j + 1] >= 0xdc00 && value[j + 1] <= 0xdfff) { codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (value[++j] - 0xdc00); } appendUtf8(string_value, codepoint); } result.strings.push_back(std::move(string_value)); } if (result.strings.size() == 1 && result.strings[0] == "@Invalid()") { result.strings.clear(); } else if (decoded.size() == 1) { static constexpr std::string_view prefix = "@ByteArray("; const auto &value = decoded[0]; if (value.size() >= prefix.size() + 1 && std::equal(prefix.begin(), prefix.end(), value.begin()) && value.back() == ')') { result.is_byte_array = true; result.bytes.reserve(value.size() - prefix.size() - 1); for (size_t i = prefix.size(); i + 1 < value.size(); ++i) result.bytes.push_back(value[i] & 0xff); } } if (!result.is_byte_array) { for (auto &value : result.strings) { if (value.compare(0, 2, "@@") == 0) value.erase(0, 1); } } return result; } LegacySettings loadLegacySettings() { auto path = settingsFile(); path.replace_filename("cabana.conf"); std::ifstream input(path); if (!input) return {}; LegacySettings settings; bool in_general_section = false; std::string line; while (std::getline(input, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); if (line == "[General]") { in_general_section = true; continue; } if (!line.empty() && line.front() == '[') { in_general_section = false; continue; } if (!in_general_section || line.empty() || line.front() == ';') continue; if (auto separator = line.find('='); separator != std::string::npos) { settings[line.substr(0, separator)] = decodeIniValue(std::string_view(line).substr(separator + 1)); } } return settings; } #else std::string cfStringToUtf8(CFStringRef value) { CFIndex length = CFStringGetLength(value); CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; std::string result(size, '\0'); if (!CFStringGetCString(value, result.data(), size, kCFStringEncodingUTF8)) return {}; result.resize(strlen(result.c_str())); return result; } LegacyValue cfStringValue(CFStringRef string) { LegacyValue value; if (CFStringHasPrefix(string, CFSTR("@ByteArray(")) && CFStringHasSuffix(string, CFSTR(")"))) { CFRange range{11, CFStringGetLength(string) - 12}; std::vector data(range.length); CFStringGetCharacters(string, range, data.data()); value.is_byte_array = true; value.bytes.reserve(data.size()); for (UniChar c : data) value.bytes.push_back(c & 0xff); } else { std::string string_value = cfStringToUtf8(string); if (string_value.compare(0, 2, "@@") == 0) string_value.erase(0, 1); value.strings.push_back(std::move(string_value)); } return value; } LegacySettings loadLegacySettings() { LegacySettings settings; CFDictionaryRef values = CFPreferencesCopyMultiple(nullptr, CFSTR("com.cabana"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost); if (values == nullptr) return settings; CFIndex count = CFDictionaryGetCount(values); std::vector keys(count); std::vector objects(count); CFDictionaryGetKeysAndValues(values, keys.data(), objects.data()); for (CFIndex i = 0; i < count; ++i) { if (CFGetTypeID(keys[i]) != CFStringGetTypeID()) continue; std::string key = cfStringToUtf8(static_cast(keys[i])); CFTypeRef object = objects[i]; LegacyValue value; if (CFGetTypeID(object) == CFBooleanGetTypeID()) { value.strings.push_back(CFBooleanGetValue(static_cast(object)) ? "true" : "false"); } else if (CFGetTypeID(object) == CFNumberGetTypeID()) { int number = 0; if (CFNumberGetValue(static_cast(object), kCFNumberIntType, &number)) value.strings.push_back(std::to_string(number)); } else if (CFGetTypeID(object) == CFStringGetTypeID()) { value = cfStringValue(static_cast(object)); } else if (CFGetTypeID(object) == CFDataGetTypeID()) { auto data = static_cast(object); value.is_byte_array = true; value.bytes.assign(reinterpret_cast(CFDataGetBytePtr(data)), CFDataGetLength(data)); } else if (CFGetTypeID(object) == CFArrayGetTypeID()) { auto array = static_cast(object); for (CFIndex j = 0; j < CFArrayGetCount(array); ++j) { CFTypeRef item = CFArrayGetValueAtIndex(array, j); if (CFGetTypeID(item) != CFStringGetTypeID()) { value.strings.clear(); break; } auto item_value = cfStringValue(static_cast(item)); if (item_value.strings.size() != 1) { value.strings.clear(); break; } value.strings.push_back(std::move(item_value.strings[0])); } } if (!value.strings.empty() || value.is_byte_array || CFGetTypeID(object) == CFArrayGetTypeID()) { settings.emplace(std::move(key), std::move(value)); } } CFRelease(values); return settings; } #endif template void readLegacySetting(const LegacySettings &legacy_settings, const char *key, T &value) { auto it = legacy_settings.find(key); if (it == legacy_settings.end() || it->second.strings.size() != 1) return; const auto &stored = it->second.strings[0]; if constexpr (std::is_same_v) { if (stored == "true") value = true; if (stored == "false") value = false; } else if constexpr (std::is_integral_v || std::is_enum_v) { int number = 0; auto [end, error] = std::from_chars(stored.data(), stored.data() + stored.size(), number); if (error == std::errc{} && end == stored.data() + stored.size()) value = static_cast(number); } } void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::string &value) { auto it = legacy_settings.find(key); if (it != legacy_settings.end() && it->second.strings.size() == 1) value = it->second.strings[0]; } void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { auto it = legacy_settings.find(key); if (it != legacy_settings.end() && !it->second.is_byte_array) value = it->second.strings; } void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { auto it = legacy_settings.find(key); if (it != legacy_settings.end() && it->second.is_byte_array) { value.assign(it->second.bytes.begin(), it->second.bytes.end()); } } template void readSetting(const json11::Json::object &settings_json, const char *key, T &value) { auto it = settings_json.find(key); if (it == settings_json.end()) return; if constexpr (std::is_same_v) { if (it->second.is_bool()) value = it->second.bool_value(); } else if constexpr (std::is_integral_v) { if (it->second.is_number()) value = it->second.int_value(); } else if constexpr (std::is_enum_v) { if (it->second.is_number()) value = static_cast(it->second.int_value()); } } void readSetting(const json11::Json::object &settings_json, const char *key, std::string &value) { auto it = settings_json.find(key); if (it != settings_json.end() && it->second.is_string()) value = it->second.string_value(); } void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { auto it = settings_json.find(key); if (it == settings_json.end() || !it->second.is_array()) return; std::vector stored; for (const auto &item : it->second.array_items()) { if (!item.is_string()) return; stored.push_back(item.string_value()); } value = std::move(stored); } void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { auto it = settings_json.find(key); if (it == settings_json.end() || !it->second.is_string()) return; const auto &hex = it->second.string_value(); if (hex.size() % 2 == 0 && std::all_of(hex.begin(), hex.end(), [](unsigned char c) { return std::isxdigit(c); })) { value.clear(); value.reserve(hex.size() / 2); for (size_t i = 0; i < hex.size(); i += 2) { value.push_back((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 1])); } } } template void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) { if constexpr (std::is_same_v) { settings_json[key] = value; } else if constexpr (std::is_integral_v || std::is_enum_v) { settings_json[key] = static_cast(value); } } void writeSetting(json11::Json::object &settings_json, const char *key, const std::string &value) { settings_json[key] = value; } void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { settings_json[key] = value; } void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { static const char digits[] = "0123456789abcdef"; std::string hex; hex.reserve(value.size() * 2); for (uint8_t b : value) { hex.push_back(digits[b >> 4]); hex.push_back(digits[b & 0xf]); } settings_json[key] = hex; } template void settingsOp(Store &s, SettingOperation op) { op(s, "absolute_time", settings.absolute_time); op(s, "fps", settings.fps); op(s, "max_cached_minutes", settings.max_cached_minutes); op(s, "chart_height", settings.chart_height); op(s, "chart_range", settings.chart_range); op(s, "chart_column_count", settings.chart_column_count); op(s, "last_dir", settings.last_dir); op(s, "last_route_dir", settings.last_route_dir); op(s, "window_state", settings.window_state); op(s, "geometry", settings.geometry); op(s, "video_splitter_state", settings.video_splitter_state); op(s, "recent_files", settings.recent_files); op(s, "message_header_state", settings.message_header_state); op(s, "chart_series_type", settings.chart_series_type); op(s, "theme", settings.theme); op(s, "sparkline_range", settings.sparkline_range); op(s, "multiple_lines_hex", settings.multiple_lines_hex); op(s, "log_livestream", settings.log_livestream); op(s, "log_path", settings.log_path); op(s, "drag_direction", (int &)settings.drag_direction); op(s, "suppress_defined_signals", settings.suppress_defined_signals); op(s, "recent_dbc_file", settings.recent_dbc_file); op(s, "active_msg_id", settings.active_msg_id); op(s, "selected_msg_ids", settings.selected_msg_ids); op(s, "active_charts", settings.active_charts); } } // namespace Settings::Settings() { last_dir = last_route_dir = utils::homePath(); log_path = utils::homePath() + "/cabana_live_stream/"; const auto stored_settings = loadSettings(); if (stored_settings.valid) { if (stored_settings.exists) { settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); }); } else { auto legacy_settings = loadLegacySettings(); settingsOp(legacy_settings, [](const auto &s, const char *key, auto &value) { readLegacySetting(s, key, value); }); } } fps = std::clamp(fps, 1, 100); } // Must be called before main() returns: json11's internal statistics are constructed on first // use at runtime, so they are destroyed before this pre-main global. Saving from ~Settings // would use them after destruction and corrupt the heap. void Settings::save() { if (!ensureSettingsDirectory()) return; auto lock_path = settingsFile(); lock_path += ".lock"; FileLock lock(lock_path); if (!lock.isLocked()) return; auto stored_settings = loadSettings(); if (!stored_settings.valid && !preserveCorruptSettings()) return; settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); }); saveSettings(stored_settings.values); } // SettingsDlg SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Settings")); QVBoxLayout *main_layout = new QVBoxLayout(this); QGroupBox *groupbox = new QGroupBox("General"); QFormLayout *form_layout = new QFormLayout(groupbox); form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); theme->setToolTip(tr("You may need to restart cabana after changes theme")); theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); theme->setCurrentIndex(settings.theme); form_layout->addRow("FPS", fps = new QSpinBox(this)); fps->setRange(10, 100); fps->setSingleStep(10); fps->setValue(settings.fps); form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); cached_minutes->setSingleStep(1); cached_minutes->setValue(settings.max_cached_minutes); main_layout->addWidget(groupbox); groupbox = new QGroupBox("New Signal Settings"); form_layout = new QFormLayout(groupbox); form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); drag_direction->setCurrentIndex(settings.drag_direction); main_layout->addWidget(groupbox); groupbox = new QGroupBox("Chart"); form_layout = new QFormLayout(groupbox); form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); chart_height->setRange(100, 500); chart_height->setSingleStep(10); chart_height->setValue(settings.chart_height); main_layout->addWidget(groupbox); log_livestream = new QGroupBox(tr("Enable live stream logging"), this); log_livestream->setCheckable(true); log_livestream->setChecked(settings.log_livestream); QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); log_path->setReadOnly(true); auto browse_btn = new QPushButton(tr("B&rowse...")); path_layout->addWidget(browse_btn); main_layout->addWidget(log_livestream); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); main_layout->addWidget(buttonBox); setFixedSize(400, sizeHint().height()); QObject::connect(browse_btn, &QPushButton::clicked, [this]() { QString fn = QFileDialog::getExistingDirectory( this, tr("Log File Location"), QString::fromStdString(utils::homePath()), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!fn.isEmpty()) { log_path->setText(fn); } }); QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); } void SettingsDlg::save() { if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { // set theme before emit changed utils::setTheme(settings.theme); } settings.fps = fps->value(); settings.max_cached_minutes = cached_minutes->value(); settings.chart_height = chart_height->value(); settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text().toStdString(); settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); emit settings.changed(); QDialog::accept(); }