From d907cebae9b1b7f96480e082df3b9e73ebbe148c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Sep 2024 15:07:36 -0500 Subject: [PATCH 1/5] 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(); +} From 6292cdec0ec9904c96b155a6069d7542d9cee3e5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Sep 2024 15:18:18 -0500 Subject: [PATCH 2/5] remove unnecessary imports --- src/cli/crash_report.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index d8086b506..69d7c872e 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -1,9 +1,7 @@ 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 { From 1a6c9289513d5908c784e7434865f64dcee95124 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Sep 2024 15:19:03 -0500 Subject: [PATCH 3/5] update README --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a3c3a9055..2667b7dc5 100644 --- a/README.md +++ b/README.md @@ -459,16 +459,17 @@ crash report was generated. > [!NOTE] > -> A future version of Ghostty will make the crash reports more easily -> viewable through the CLI and GUI. For now, you must manually check the -> directory. +> Use the `ghostty +crash-report` CLI command to get a list of available crash +> reports. A future version of Ghostty will make the contents of the crash +> reports more easily viewable through the CLI and GUI. -Crash reports end in the `.ghosttycrash` extension. The crash reports are -in [Sentry envelope format](https://develop.sentry.dev/sdk/envelopes/). You -can upload these to your own Sentry account to view their contents, but the -format is also publicly documented so any other available tools can also -be used. A future version of Ghostty will show you the contents of the -crash report directly in the terminal. +Crash reports end in the `.ghosttycrash` extension. The crash reports are in +[Sentry envelope format](https://develop.sentry.dev/sdk/envelopes/). You can +upload these to your own Sentry account to view their contents, but the format +is also publicly documented so any other available tools can also be used. +The `ghostty +crash-report` CLI command can be used to list any crash reports. +A future version of Ghostty will show you the contents of the crash report +directly in the terminal. If Ghostty crashed, you can help the project by sending the crash report to a maintainer. We do not recommend uploading crash reports directly From 4e166246761474c3c524def1c993bd23a0d33fce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Sep 2024 21:14:55 -0700 Subject: [PATCH 4/5] crash: add directory listing, allocation free --- src/crash/dir.zig | 66 ++++++++++++++++++++++++++++++++++++++++++++ src/crash/main.zig | 5 ++++ src/crash/sentry.zig | 48 ++------------------------------ 3 files changed, 74 insertions(+), 45 deletions(-) create mode 100644 src/crash/dir.zig diff --git a/src/crash/dir.zig b/src/crash/dir.zig new file mode 100644 index 000000000..e5a8f8a0c --- /dev/null +++ b/src/crash/dir.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +/// Returns a Dir for the default directory. The Dir.path field must be +/// freed with the given allocator. +pub fn defaultDir(alloc: Allocator) !Dir { + const crash_dir = try internal_os.xdg.state(alloc, .{ .subdir = "ghostty/crash" }); + errdefer alloc.free(crash_dir); + return .{ .path = crash_dir }; +} + +pub const Dir = struct { + /// The directory where crash reports are stored. This memory is owned + /// by the caller. + path: []const u8, + + /// Returns an iterator over the crash reports in this directory. This + /// iterator must be freed with `ReportIterator.deinit`. The iterator + /// may have no reports. + pub fn iterator(self: *const Dir) !ReportIterator { + var dir = std.fs.openDirAbsolute( + self.path, + .{ .iterate = true }, + ) catch return .{}; + errdefer dir.close(); + + return .{ + .dir = dir, + .it = dir.iterate(), + }; + } +}; + +pub const ReportIterator = struct { + dir: ?std.fs.Dir = null, + it: std.fs.Dir.Iterator = undefined, + + pub fn deinit(self: *ReportIterator) void { + if (self.dir) |dir| dir.close(); + } + + pub fn next(self: *ReportIterator) !?Report { + // If we have no dir then we failed to open the directory. + const dir = self.dir orelse return null; + + // Get the next file entry, if any. + const entry = entry: while (true) { + const entry = try self.it.next() orelse return null; + if (entry.kind != .file) continue; + break :entry entry; + }; + + const stat = try dir.statFile(entry.name); + return .{ + .name = entry.name, + .mtime = stat.mtime, + }; + } +}; + +pub const Report = struct { + name: []const u8, + mtime: i128, +}; diff --git a/src/crash/main.zig b/src/crash/main.zig index 1e6044c4b..1ac971851 100644 --- a/src/crash/main.zig +++ b/src/crash/main.zig @@ -2,10 +2,15 @@ //! whether that's setting up the system to catch crashes (Sentry client), //! introspecting crash reports, writing crash reports to disk, etc. +const dir = @import("dir.zig"); const sentry_envelope = @import("sentry_envelope.zig"); pub const sentry = @import("sentry.zig"); pub const Envelope = sentry_envelope.Envelope; +pub const defaultDir = dir.defaultDir; +pub const Dir = dir.Dir; +pub const ReportIterator = dir.ReportIterator; +pub const Report = dir.Report; // The main init/deinit functions for global state. pub const init = sentry.init; diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 1df311599..afff83f50 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -253,12 +253,12 @@ pub const Transport = struct { // Get our XDG state directory where we'll store the crash reports. // This directory must exist for writing to work. - const crash_dir = try internal_os.xdg.state(alloc, .{ .subdir = "ghostty/crash" }); - try std.fs.cwd().makePath(crash_dir); + const dir = try crash.defaultDir(alloc); + try std.fs.cwd().makePath(dir.path); // Build our final path and write to it. const path = try std.fs.path.join(alloc, &.{ - crash_dir, + dir.path, try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}), }); const file = try std.fs.cwd().createFile(path, .{}); @@ -277,45 +277,3 @@ 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(); -} From 11c3ca69f552cc2280ba540ae7ff3a49da331f41 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Sep 2024 21:14:55 -0700 Subject: [PATCH 5/5] cli/crash-report: make it simpler (uglier, honestly) --- src/cli/crash_report.zig | 87 ++++++++++++++++++--------------------- src/cli/list_keybinds.zig | 7 +--- src/cli/tui.zig | 6 +++ 3 files changed, 49 insertions(+), 51 deletions(-) create mode 100644 src/cli/tui.zig diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index 69d7c872e..0ec6a8ce0 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -1,16 +1,11 @@ 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 sentry = @import("../crash/sentry.zig"); +const crash = @import("../crash/main.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; } @@ -22,14 +17,18 @@ pub const Options = struct { } }; -/// The `crash-report command is used to list/view/send crash reports. +/// The `crash-report` command is used to inspect and send crash reports. /// -/// When executed without any arguments, this will list any existing crash reports. +/// When executed without any arguments, this will list 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 { +/// This command currently only supports listing crash reports. Viewing +/// and sending crash reports is unimplemented and will be added in the future. +pub fn run(alloc_gpa: Allocator) !u8 { + // Use an arena for the whole command to avoid manual memory management. + var arena = std.heap.ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + var opts: Options = .{}; defer opts.deinit(); @@ -39,47 +38,43 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + const crash_dir = try crash.defaultDir(alloc); + var reports = std.ArrayList(crash.Report).init(alloc); - if (opts.view) |_| { - try stdout.writeAll("viewing crash reports is unimplemented\n"); - return 1; + var it = try crash_dir.iterator(); + while (try it.next()) |report| try reports.append(.{ + .name = try alloc.dupe(u8, report.name), + .mtime = report.mtime, + }); + + const stdout = std.io.getStdOut(); + + // If we have no reports, then we're done. If we have a tty then we + // print a message, otherwise we do nothing. + if (reports.items.len == 0) { + if (std.posix.isatty(stdout.handle)) { + try stdout.writeAll("No crash reports! 👻"); + } + return 0; } - if (opts.send) |_| { - try stdout.writeAll("sending crash reports is unimplemented\n"); - return 1; - } + std.mem.sort(crash.Report, reports.items, {}, lt); - 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"); + const writer = stdout.writer(); + for (reports.items) |report| { + 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 writer.print("{s} ({s})\n", .{ report.name, since }); } return 0; } -fn lt(_: void, lhs: sentry.CrashReport, rhs: sentry.CrashReport) bool { +fn lt(_: void, lhs: crash.Report, rhs: crash.Report) bool { return lhs.mtime > rhs.mtime; } diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index ca28a2765..b12694625 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -8,6 +8,7 @@ const configpkg = @import("../config.zig"); const Config = configpkg.Config; const vaxis = @import("vaxis"); const input = @import("../input.zig"); +const tui = @import("tui.zig"); const Binding = input.Binding; pub const Options = struct { @@ -61,12 +62,8 @@ pub fn run(alloc: Allocator) !u8 { const stdout = std.io.getStdOut(); - const can_pretty_print = switch (builtin.os.tag) { - .ios, .tvos, .watchos => false, - else => true, - }; // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 - if (can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { return prettyPrint(alloc, config.keybind); } else { try config.keybind.formatEntryDocs( diff --git a/src/cli/tui.zig b/src/cli/tui.zig new file mode 100644 index 000000000..28d593ed4 --- /dev/null +++ b/src/cli/tui.zig @@ -0,0 +1,6 @@ +const builtin = @import("builtin"); + +pub const can_pretty_print = switch (builtin.os.tag) { + .ios, .tvos, .watchos => false, + else => true, +};