mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge pull request #2184 from jcollie/basic-cli-crash-report
feat: basic +crash-report cli action
This commit is contained in:
19
README.md
19
README.md
@ -459,16 +459,17 @@ crash report was generated.
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
> A future version of Ghostty will make the crash reports more easily
|
> Use the `ghostty +crash-report` CLI command to get a list of available crash
|
||||||
> viewable through the CLI and GUI. For now, you must manually check the
|
> reports. A future version of Ghostty will make the contents of the crash
|
||||||
> directory.
|
> reports more easily viewable through the CLI and GUI.
|
||||||
|
|
||||||
Crash reports end in the `.ghosttycrash` extension. The crash reports are
|
Crash reports end in the `.ghosttycrash` extension. The crash reports are in
|
||||||
in [Sentry envelope format](https://develop.sentry.dev/sdk/envelopes/). You
|
[Sentry envelope format](https://develop.sentry.dev/sdk/envelopes/). You can
|
||||||
can upload these to your own Sentry account to view their contents, but the
|
upload these to your own Sentry account to view their contents, but the format
|
||||||
format is also publicly documented so any other available tools can also
|
is also publicly documented so any other available tools can also be used.
|
||||||
be used. A future version of Ghostty will show you the contents of the
|
The `ghostty +crash-report` CLI command can be used to list any crash reports.
|
||||||
crash report directly in the terminal.
|
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
|
If Ghostty crashed, you can help the project by sending the crash report
|
||||||
to a maintainer. We do not recommend uploading crash reports directly
|
to a maintainer. We do not recommend uploading crash reports directly
|
||||||
|
@ -11,6 +11,7 @@ const list_colors = @import("list_colors.zig");
|
|||||||
const list_actions = @import("list_actions.zig");
|
const list_actions = @import("list_actions.zig");
|
||||||
const show_config = @import("show_config.zig");
|
const show_config = @import("show_config.zig");
|
||||||
const validate_config = @import("validate_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
|
/// Special commands that can be invoked via CLI flags. These are all
|
||||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
/// 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 passed config file
|
||||||
@"validate-config",
|
@"validate-config",
|
||||||
|
|
||||||
|
// List, (eventually) view, and (eventually) send crash reports.
|
||||||
|
@"crash-report",
|
||||||
|
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
/// Multiple actions were detected. You can specify at most one
|
/// Multiple actions were detected. You can specify at most one
|
||||||
/// action on the CLI otherwise the behavior desired is ambiguous.
|
/// 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),
|
.@"list-actions" => try list_actions.run(alloc),
|
||||||
.@"show-config" => try show_config.run(alloc),
|
.@"show-config" => try show_config.run(alloc),
|
||||||
.@"validate-config" => try validate_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
80
src/cli/crash_report.zig
Normal 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;
|
||||||
|
}
|
@ -8,6 +8,7 @@ const configpkg = @import("../config.zig");
|
|||||||
const Config = configpkg.Config;
|
const Config = configpkg.Config;
|
||||||
const vaxis = @import("vaxis");
|
const vaxis = @import("vaxis");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
|
const tui = @import("tui.zig");
|
||||||
const Binding = input.Binding;
|
const Binding = input.Binding;
|
||||||
|
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
@ -61,12 +62,8 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
|
|
||||||
const stdout = std.io.getStdOut();
|
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
|
// 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);
|
return prettyPrint(alloc, config.keybind);
|
||||||
} else {
|
} else {
|
||||||
try config.keybind.formatEntryDocs(
|
try config.keybind.formatEntryDocs(
|
||||||
|
6
src/cli/tui.zig
Normal file
6
src/cli/tui.zig
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
pub const can_pretty_print = switch (builtin.os.tag) {
|
||||||
|
.ios, .tvos, .watchos => false,
|
||||||
|
else => true,
|
||||||
|
};
|
@ -4126,6 +4126,10 @@ pub const Duration = struct {
|
|||||||
return self.duration == other.duration;
|
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 {
|
pub fn parseCLI(input: ?[]const u8) !Duration {
|
||||||
var remaining = input orelse return error.ValueRequired;
|
var remaining = input orelse return error.ValueRequired;
|
||||||
|
|
||||||
|
66
src/crash/dir.zig
Normal file
66
src/crash/dir.zig
Normal 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,
|
||||||
|
};
|
@ -2,10 +2,15 @@
|
|||||||
//! whether that's setting up the system to catch crashes (Sentry client),
|
//! whether that's setting up the system to catch crashes (Sentry client),
|
||||||
//! introspecting crash reports, writing crash reports to disk, etc.
|
//! introspecting crash reports, writing crash reports to disk, etc.
|
||||||
|
|
||||||
|
const dir = @import("dir.zig");
|
||||||
const sentry_envelope = @import("sentry_envelope.zig");
|
const sentry_envelope = @import("sentry_envelope.zig");
|
||||||
|
|
||||||
pub const sentry = @import("sentry.zig");
|
pub const sentry = @import("sentry.zig");
|
||||||
pub const Envelope = sentry_envelope.Envelope;
|
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.
|
// The main init/deinit functions for global state.
|
||||||
pub const init = sentry.init;
|
pub const init = sentry.init;
|
||||||
|
@ -253,12 +253,12 @@ pub const Transport = struct {
|
|||||||
|
|
||||||
// Get our XDG state directory where we'll store the crash reports.
|
// Get our XDG state directory where we'll store the crash reports.
|
||||||
// This directory must exist for writing to work.
|
// This directory must exist for writing to work.
|
||||||
const crash_dir = try internal_os.xdg.state(alloc, .{ .subdir = "ghostty/crash" });
|
const dir = try crash.defaultDir(alloc);
|
||||||
try std.fs.cwd().makePath(crash_dir);
|
try std.fs.cwd().makePath(dir.path);
|
||||||
|
|
||||||
// Build our final path and write to it.
|
// Build our final path and write to it.
|
||||||
const path = try std.fs.path.join(alloc, &.{
|
const path = try std.fs.path.join(alloc, &.{
|
||||||
crash_dir,
|
dir.path,
|
||||||
try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}),
|
try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}),
|
||||||
});
|
});
|
||||||
const file = try std.fs.cwd().createFile(path, .{});
|
const file = try std.fs.cwd().createFile(path, .{});
|
||||||
|
Reference in New Issue
Block a user