feat: basic +crash-report cli action

Only lists crash reports right now. Viewing and/or submitting crash
reports to come later.
This commit is contained in:
Jeffrey C. Ollie
2024-09-02 15:07:36 -05:00
committed by Mitchell Hashimoto
parent 67abd8804e
commit d907cebae9
4 changed files with 138 additions and 0 deletions

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

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

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

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;

View File

@ -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();
}