Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
94c3ff9
feat(ipc): add notify message type to IPCServer
Charliechen114514 Jul 10, 2026
8b96e97
feat(notification): NotificationService + Notification model
Charliechen114514 Jul 10, 2026
9a41369
feat(notification): banner + notification center panels
Charliechen114514 Jul 10, 2026
8c81451
feat(control-center): MS6 panel with sliders, toggles, theme switch
Charliechen114514 Jul 10, 2026
c34e1a4
feat(statusbar): clock + notification click signals, vector bell icon
Charliechen114514 Jul 10, 2026
aacb972
feat(shell): wire control center + notifications into CFDesktopEntity
Charliechen114514 Jul 10, 2026
fa45e03
docs(status): record Phase F minimal closed loop
Charliechen114514 Jul 10, 2026
8cb3267
feat(control-center): test notification stub + fix panel height
Charliechen114514 Jul 10, 2026
a463678
fix(notification): center banner, slow enter, guard DND echo loop
Charliechen114514 Jul 10, 2026
231361a
docs(home_page): fix stale comment — gadget grid replaced app-grid pl…
Charliechen114514 Jul 10, 2026
fefb2e8
feat(crash): addr2line symbolization for raw_frames + test
Charliechen114514 Jul 10, 2026
925d046
feat(crash): CrashReporter dialog + .seen marker
Charliechen114514 Jul 10, 2026
970ed68
feat(shell): show crash reporter on boot for unseen crashes
Charliechen114514 Jul 10, 2026
ee9be8a
feat(settings): SettingsWindow with wallpaper/theme/about tabs
Charliechen114514 Jul 10, 2026
57a3e82
feat(shell): wire Settings button in control center -> settings window
Charliechen114514 Jul 10, 2026
d05c81a
docs(status): record settings window + crash phase2 minimal loop
Charliechen114514 Jul 10, 2026
6a9435b
feat(settings): MD3 card groups, value readouts, theme highlight, ric…
Charliechen114514 Jul 10, 2026
0d134d3
feat(wallpaper): live reload on config change (settings applies insta…
Charliechen114514 Jul 10, 2026
7d60184
test(wallpaper): init ConfigStore in test main (fix UB exposed by watch)
Charliechen114514 Jul 10, 2026
26a8a3f
fix(settings): theme switches + About card fills the tab
Charliechen114514 Jul 10, 2026
55ef8a2
fix(theme): use correct theme token names (theme.material.light/dark)
Charliechen114514 Jul 10, 2026
88a35dc
fix(theme): sync QGuiApplication palette on theme change (dark native…
Charliechen114514 Jul 10, 2026
f2564d8
fix(settings): enlarge tab/card font sizes (were too small)
Charliechen114514 Jul 10, 2026
1284260
fix(settings): enlarge About tab fonts further (body 15->20, title 22…
Charliechen114514 Jul 10, 2026
9d85451
fix(settings): Theme tab — one big title + two buttons (drop the card)
Charliechen114514 Jul 10, 2026
53a24a8
fix(theme): push palette onto every widget on theme change
Charliechen114514 Jul 10, 2026
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
1 change: 1 addition & 0 deletions base/crash_handler/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ PRIVATE
src/crash_handler.cpp
src/crash_report.cpp
src/crash_signal_handler.cpp
src/symbolizer.cpp
)

target_include_directories(cfcrash PUBLIC
Expand Down
3 changes: 2 additions & 1 deletion base/crash_handler/include/cfcrash/crash_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ class CrashHandler {
* @ingroup crash
*/
std::size_t finalizePendingReports(const std::string& logger_path,
std::size_t tail_lines = kDefaultTailLogLines);
std::size_t tail_lines = kDefaultTailLogLines,
const std::string& exe_path = "");

/**
* @brief Deletes oldest @c .json reports beyond the retention cap.
Expand Down
25 changes: 24 additions & 1 deletion base/crash_handler/include/cfcrash/crash_report.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ inline constexpr std::size_t kDefaultTailLogLines{50};
/// @brief Default maximum number of finalized reports kept on disk.
inline constexpr std::size_t kDefaultMaxReports{20};

/**
* @brief One symbol-resolved stack frame.
*
* Produced by the symbolizer (addr2line on Linux) from a bare address: the
* demangled function name, source file, and line. Holds "??"/"0" when the
* address could not be resolved.
*
* @ingroup crash
*/
struct ResolvedFrame {
/// @brief Demangled function name (or "??").
std::string function;

/// @brief Source file path (or "??").
std::string file;

/// @brief Source line number as a string (or "0").
std::string line;
};

/**
* @brief A finalized crash report.
*
Expand All @@ -54,9 +74,12 @@ struct CrashReport {
/// @brief Human-readable signal name ("SIGSEGV", "SIGABRT", ...).
std::string signal_name;

/// @brief Bare stack frame addresses as hex strings; resolved in Phase 2.
/// @brief Bare stack frame addresses as hex strings.
std::vector<std::string> raw_frames;

/// @brief Symbol-resolved frames (function/file/line); empty until finalize.
std::vector<ResolvedFrame> resolved_frames;

/// @brief Tail of the logger file captured at finalize time.
std::vector<std::string> last_logs;

Expand Down
61 changes: 61 additions & 0 deletions base/crash_handler/include/cfcrash/symbolizer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @file symbolizer.h
* @brief Symbol resolution for raw crash stack frames.
*
* Phase 2: converts the bare addresses captured by the signal handler into
* (function, file, line) triples via addr2line on Linux. Non-Linux targets
* get a stub (Windows dbghelp is deferred). resolveFrames() runs in a normal
* context (next boot), never in a signal handler, so it may spawn a subprocess.
*
* @author CFDesktop Team
* @date 2026-07-10
* @version 0.19.0
* @since 0.19.0
* @ingroup crash
*/

#pragma once

#include "cfcrash/crash_report.h"

#include <cstddef>
#include <string>
#include <vector>

namespace cf::crash {

/**
* @brief Resolves bare frame addresses against the executable.
*
* @param[in] raw_frames Hex address strings (e.g. "0x7f...").
* @param[in] exe_path Path to the executable the addresses belong to.
* @return One ResolvedFrame per input address; empty on non-Linux or when
* exe_path is empty or addr2line is unavailable.
* @throws None
* @note Linux only; spawns addr2line via popen.
* @warning Not async-signal-safe; call from a normal context only.
* @since 0.19.0
* @ingroup crash
*/
std::vector<ResolvedFrame> resolveFrames(const std::vector<std::string>& raw_frames,
const std::string& exe_path);

/**
* @brief Parses addr2line output lines into resolved frames.
*
* addr2line prints two lines per address (function, then file:line). This
* helper is split out so it can be unit-tested without spawning the tool.
*
* @param[in] lines Output lines from addr2line.
* @param[in] frame_count Number of addresses that were queried.
* @return Up to frame_count ResolvedFrames; fewer if lines run out.
* @throws None
* @note None
* @warning None
* @since 0.19.0
* @ingroup crash
*/
std::vector<ResolvedFrame> parseAddr2LineOutput(const std::vector<std::string>& lines,
std::size_t frame_count);

} // namespace cf::crash
8 changes: 7 additions & 1 deletion base/crash_handler/src/crash_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "cfcrash/crash_handler.h"

#include "cfcrash/crash_report.h"
#include "cfcrash/symbolizer.h"

#include <algorithm>
#include <cstdlib>
Expand Down Expand Up @@ -129,7 +130,8 @@ void CrashHandler::uninstall() {
}

std::size_t CrashHandler::finalizePendingReports(const std::string& logger_path,
std::size_t tail_lines) {
std::size_t tail_lines,
const std::string& exe_path) {
if (crashes_dir_.empty()) {
return 0;
}
Expand All @@ -153,6 +155,10 @@ std::size_t CrashHandler::finalizePendingReports(const std::string& logger_path,
// fall back to the caller-supplied path for snapshots without one.
const std::string effective_logger = pending_logger.empty() ? logger_path : pending_logger;
report.last_logs = tailLines(effective_logger, tail_lines);
// Phase 2: symbolize raw addresses against the running executable.
if (!exe_path.empty()) {
report.resolved_frames = resolveFrames(report.raw_frames, exe_path);
}

const auto out_path = fs::path(crashes_dir_) / (stem + ".json");
std::ofstream of(out_path);
Expand Down
22 changes: 22 additions & 0 deletions base/crash_handler/src/crash_report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ std::string toJsonArray(const std::vector<std::string>& v) {
return out;
}

/// @brief Renders resolved frames as a pretty JSON object array (one/line).
std::string toJsonResolvedArray(const std::vector<ResolvedFrame>& v) {
if (v.empty()) {
return "[]";
}
std::string out = "[";
for (std::size_t i = 0; i < v.size(); ++i) {
out += "\n {\"function\": \"";
out += escapeJson(v[i].function);
out += "\", \"file\": \"";
out += escapeJson(v[i].file);
out += "\", \"line\": \"";
out += escapeJson(v[i].line);
out += "\"}";
out += (i + 1 == v.size()) ? "" : ",";
}
out += "\n ]";
return out;
}

} // namespace

std::string CrashReport::toJson() const {
Expand All @@ -74,6 +94,8 @@ std::string CrashReport::toJson() const {
out += std::format(" \"signal_name\": \"{}\",\n", escapeJson(signal_name));
out += " \"raw_frames\": ";
out += toJsonArray(raw_frames);
out += ",\n \"resolved_frames\": ";
out += toJsonResolvedArray(resolved_frames);
out += ",\n \"last_logs\": ";
out += toJsonArray(last_logs);
out += '\n';
Expand Down
93 changes: 93 additions & 0 deletions base/crash_handler/src/symbolizer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @file symbolizer.cpp
* @brief addr2line-based symbol resolution for crash frames.
*
* @author CFDesktop Team
* @date 2026-07-10
* @version 0.19.0
* @since 0.19.0
* @ingroup crash
*/

#include "cfcrash/symbolizer.h"

#include <array>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>

namespace cf::crash {

std::vector<ResolvedFrame> parseAddr2LineOutput(const std::vector<std::string>& lines,
std::size_t frame_count) {
std::vector<ResolvedFrame> out;
out.reserve(frame_count);
for (std::size_t i = 0; i < frame_count; ++i) {
const std::size_t func_idx = i * 2;
const std::size_t file_idx = i * 2 + 1;
if (file_idx >= lines.size()) {
break; // addr2line produced fewer lines than expected
}
ResolvedFrame frame;
frame.function = lines[func_idx];
const std::string& fl = lines[file_idx];
// addr2line prints "path/file.cpp:123" (or "??:0"); split on the last ':'.
const auto colon = fl.rfind(':');
if (colon != std::string::npos) {
frame.file = fl.substr(0, colon);
frame.line = fl.substr(colon + 1);
} else {
frame.file = fl;
}
out.push_back(std::move(frame));
}
return out;
}

#ifdef __linux__
std::vector<ResolvedFrame> resolveFrames(const std::vector<std::string>& raw_frames,
const std::string& exe_path) {
if (raw_frames.empty() || exe_path.empty()) {
return {};
}
// One addr2line invocation for all addresses; -f prints function then
// file:line per address, -C demangles. exe_path is quoted; paths with
// spaces would still need shell-safe escaping but applicationFilePath()
// does not contain spaces in practice.
std::string cmd = "addr2line -e \"";
cmd += exe_path;
cmd += "\" -f -C";
for (const auto& addr : raw_frames) {
cmd += ' ';
cmd += addr;
}
cmd += " 2>/dev/null";

FILE* pipe = popen(cmd.c_str(), "r");
if (pipe == nullptr) {
return {};
}
std::array<char, 512> buf{};
std::vector<std::string> lines;
while (std::fgets(buf.data(), static_cast<int>(buf.size()), pipe) != nullptr) {
std::string line(buf.data());
if (!line.empty() && line.back() == '\n') {
line.pop_back();
}
lines.push_back(std::move(line));
}
pclose(pipe);
return parseAddr2LineOutput(lines, raw_frames.size());
}
#else
std::vector<ResolvedFrame> resolveFrames(const std::vector<std::string>& /*raw_frames*/,
const std::string& /*exe_path*/) {
// Non-Linux (Windows): SetUnhandledExceptionFilter + dbghelp SymFromAddr
// land in a later phase. Returning empty leaves resolved_frames blank,
// which the reporter renders as raw addresses.
return {};
}
#endif

} // namespace cf::crash
24 changes: 21 additions & 3 deletions base/ipc/include/cfipc/ipc_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* @brief Single-instance IPC server for the desktop shell.
*
* Listens on a QLocalServer socket so a second shell instance can signal
* the running one. Uses a line-based protocol: each message is a
* newline-terminated token. The only token defined in this phase is
* "raise", which asks the running shell to come to the foreground.
* the running one. Uses a line-based JSON protocol: each message is a
* newline-terminated object carrying a type tag plus a payload. The shell
* routes incoming messages by type via IPCMessageRegistry. Types defined
* here are "raise" (bring the shell to the foreground) and "notify" (post a
* desktop notification whose payload carries title/message/app_id).
*
* The server is a process-wide singleton; the early-session boot chain
* starts it, and the desktop entity connects to its signals.
Expand All @@ -19,6 +21,7 @@

#pragma once

#include <QJsonObject>
#include <QLocalServer>
#include <QObject>
#include <QString>
Expand Down Expand Up @@ -102,6 +105,21 @@ class IPCServer : public QObject {
*/
void raiseRequested();

/**
* @brief Emitted when a peer sends a "notify" message.
*
* The shell should post a desktop notification from the carried payload
* (title / message / app_id, etc.).
*
* @param[in] payload The notification payload, forwarded verbatim.
* @throws None
* @note None
* @warning None
* @since 0.19.0
* @ingroup ipc
*/
void notifyReceived(const QJsonObject& payload);

private slots:
/**
* @brief Handles an incoming peer connection.
Expand Down
5 changes: 5 additions & 0 deletions base/ipc/src/ipc_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ IPCServer::IPCServer() {
// by whoever registered them in the registry.
IPCMessageRegistry::instance().registerHandler(
"raise", [this](const QJsonObject&) { emit raiseRequested(); });
// "notify": a peer (external app) asks the shell to post a desktop
// notification. The payload (title/message/app_id/...) is forwarded as-is
// to the NotificationService via the desktop entity's signal wiring.
IPCMessageRegistry::instance().registerHandler(
"notify", [this](const QJsonObject& payload) { emit notifyReceived(payload); });
}

bool IPCServer::start(const QString& socket_path) {
Expand Down
Loading
Loading