Merge pull request #2184 from jcollie/basic-cli-crash-report

feat: basic +crash-report cli action
This commit is contained in:
Mitchell Hashimoto
2024-09-10 21:25:50 -07:00
committed by GitHub
9 changed files with 181 additions and 17 deletions

View File

@ -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

View File

@ -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 `+<action>` 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),
};
}

80
src/cli/crash_report.zig Normal file
View File

@ -0,0 +1,80 @@
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 crash = @import("../crash/main.zig");
pub const Options = struct {
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 inspect and send crash reports.
///
/// When executed without any arguments, this will list existing crash reports.
///
/// 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();
{
var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
const crash_dir = try crash.defaultDir(alloc);
var reports = std.ArrayList(crash.Report).init(alloc);
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;
}
std.mem.sort(crash.Report, reports.items, {}, lt);
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: crash.Report, rhs: crash.Report) bool {
return lhs.mtime > rhs.mtime;
}

View File

@ -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(

6
src/cli/tui.zig Normal file
View File

@ -0,0 +1,6 @@
const builtin = @import("builtin");
pub const can_pretty_print = switch (builtin.os.tag) {
.ios, .tvos, .watchos => false,
else => true,
};

View File

@ -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;

66
src/crash/dir.zig Normal file
View File

@ -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,
};

View File

@ -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;

View File

@ -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, .{});