From d907cebae9b1b7f96480e082df3b9e73ebbe148c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Sep 2024 15:07:36 -0500 Subject: [PATCH] feat: basic +crash-report cli action Only lists crash reports right now. Viewing and/or submitting crash reports to come later. --- src/cli/action.zig | 5 +++ src/cli/crash_report.zig | 87 ++++++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 4 ++ src/crash/sentry.zig | 42 +++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 src/cli/crash_report.zig diff --git a/src/cli/action.zig b/src/cli/action.zig index 8113f991a..950577158 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -11,6 +11,7 @@ const list_colors = @import("list_colors.zig"); const list_actions = @import("list_actions.zig"); const show_config = @import("show_config.zig"); const validate_config = @import("validate_config.zig"); +const crash_report = @import("crash_report.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -43,6 +44,9 @@ pub const Action = enum { // Validate passed config file @"validate-config", + // List, (eventually) view, and (eventually) send crash reports. + @"crash-report", + pub const Error = error{ /// Multiple actions were detected. You can specify at most one /// action on the CLI otherwise the behavior desired is ambiguous. @@ -134,6 +138,7 @@ pub const Action = enum { .@"list-actions" => try list_actions.run(alloc), .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), + .@"crash-report" => try crash_report.run(alloc), }; } diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig new file mode 100644 index 000000000..d8086b506 --- /dev/null +++ b/src/cli/crash_report.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const args = @import("args.zig"); +const Action = @import("action.zig").Action; +const Config = @import("../config.zig").Config; +const cli = @import("../cli.zig"); +const sentry = @import("../crash/sentry.zig"); + +pub const Options = struct { + /// View the crash report locally (unimplemented). + view: ?[:0]const u8 = null, + + /// Send the crash report to the Ghostty community (unimplemented). + send: ?[:0]const u8 = null, + + pub fn deinit(self: Options) void { + _ = self; + } + + /// Enables "-h" and "--help" to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `crash-report command is used to list/view/send crash reports. +/// +/// When executed without any arguments, this will list any existing crash reports. +/// +/// The `--view` argument can be used to inspect a particular crash report. +/// +/// The `--send` argument can be used to send a crash report to the Ghostty community. +pub fn run(alloc: std.mem.Allocator) !u8 { + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + try args.parse(Options, alloc, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + + if (opts.view) |_| { + try stdout.writeAll("viewing crash reports is unimplemented\n"); + return 1; + } + + if (opts.send) |_| { + try stdout.writeAll("sending crash reports is unimplemented\n"); + return 1; + } + + if (try sentry.listCrashReports(alloc)) |reports| { + defer { + for (reports) |report| { + alloc.free(report.name); + } + alloc.free(reports); + } + + std.mem.sort(sentry.CrashReport, reports, {}, lt); + try stdout.print("\n {d:} crash reports!\n\n", .{reports.len}); + + for (reports, 0..) |report, count| { + var buf: [128]u8 = undefined; + const now = std.time.nanoTimestamp(); + const diff = now - report.mtime; + const since = if (diff < 0) "now" else s: { + const d = Config.Duration{ .duration = @intCast(diff) }; + break :s try std.fmt.bufPrint(&buf, "{s} ago", .{d.round(std.time.ns_per_s)}); + }; + try stdout.print("{d: >4} — {s} ({s})\n", .{ count, report.name, since }); + } + try stdout.writeAll("\n"); + } else { + try stdout.writeAll("\n No crash reports! 👻\n\n"); + } + + return 0; +} + +fn lt(_: void, lhs: sentry.CrashReport, rhs: sentry.CrashReport) bool { + return lhs.mtime > rhs.mtime; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 1f9a78f31..d853f0481 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4126,6 +4126,10 @@ pub const Duration = struct { return self.duration == other.duration; } + pub fn round(self: Duration, to: u64) Duration { + return .{ .duration = self.duration / to * to }; + } + pub fn parseCLI(input: ?[]const u8) !Duration { var remaining = input orelse return error.ValueRequired; diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 1a4be6841..1df311599 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -277,3 +277,45 @@ pub const Transport = struct { return true; } }; + +pub const CrashReport = struct { + name: []const u8, + mtime: i128, +}; + +pub fn listCrashReports(alloc: std.mem.Allocator) !?[]CrashReport { + const crash_dir = try internal_os.xdg.state(alloc, .{ .subdir = "ghostty/crash" }); + defer alloc.free(crash_dir); + + var dir = std.fs.openDirAbsolute(crash_dir, .{ .iterate = true }) catch return null; + + defer dir.close(); + + var list = std.ArrayList(CrashReport).init(alloc); + errdefer { + for (list.items) |item| { + alloc.free(item.name); + } + list.deinit(); + } + + var it = dir.iterate(); + while (try it.next()) |entry| { + switch (entry.kind) { + .file => { + if (std.mem.endsWith(u8, entry.name, ".ghosttycrash")) { + const stat = dir.statFile(entry.name) catch continue; + try list.append(.{ + .name = try alloc.dupe(u8, entry.name), + .mtime = stat.mtime, + }); + } + }, + else => {}, + } + } + + if (list.items.len == 0) return null; + + return try list.toOwnedSlice(); +}