Merge pull request #2173 from ghostty-org/crashlog

Initial Crash Logging
This commit is contained in:
Mitchell Hashimoto
2024-09-02 10:35:27 -07:00
committed by GitHub
30 changed files with 6842 additions and 6 deletions

1
.gitattributes vendored
View File

@ -1,5 +1,6 @@
vendor/** linguist-vendored
website/** linguist-documentation
pkg/breakpad/vendor/** linguist-vendored
pkg/cimgui/vendor/** linguist-vendored
pkg/simdutf/vendor/** linguist-vendored
src/terminal/res/** linguist-vendored

View File

@ -445,6 +445,38 @@ and more use cases. At the time of writing this, `libghostty` is very
Mac-centric -- particularly around rendering -- and we have work to do to
expand this to other platforms.
## Crash Reports
Ghostty has a built-in crash reporter that will generate and save crash
reports to disk. The crash reports are saved to the `$XDG_STATE_HOME/ghostty/crash`
directory. If `$XDG_STATE_HOME` is not set, the default is `~/.local/state`.
**Crash reports are _not_ automatically sent anywhere off your machine.**
Crash reports are only generated the next time Ghostty is started after a
crash. If Ghostty crashes and you want to generate a crash report, you must
restart Ghostty at least once. You should see a message in the log that a
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.
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.
If Ghostty crashed, you can help the project by attaching the crash report
to a GitHub issue. The crash report doesn't contain any purposefully
sensitive information, but it may contain paths to files on your system,
information about your OS, or other details you may not want to share.
If you are concerned about this, you're welcome to transfer the crash report
privately to me.
## Developing Ghostty
To build Ghostty, you need [Zig 0.13](https://ziglang.org/) installed.

View File

@ -1012,6 +1012,11 @@ fn addDeps(
.target = target,
.optimize = optimize,
});
const sentry_dep = b.dependency("sentry", .{
.target = target,
.optimize = optimize,
.backend = .breakpad,
});
const zlib_dep = b.dependency("zlib", .{
.target = target,
.optimize = optimize,
@ -1115,6 +1120,7 @@ fn addDeps(
step.root_module.addImport("xev", libxev_dep.module("xev"));
step.root_module.addImport("opengl", opengl_dep.module("opengl"));
step.root_module.addImport("pixman", pixman_dep.module("pixman"));
step.root_module.addImport("sentry", sentry_dep.module("sentry"));
step.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph"));
step.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
@ -1162,6 +1168,19 @@ fn addDeps(
step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin());
if (target.result.os.tag != .windows) {
// Sentry
step.linkLibrary(sentry_dep.artifact("sentry"));
try static_libs.append(sentry_dep.artifact("sentry").getEmittedBin());
// We also need to include breakpad in the static libs.
const breakpad_dep = sentry_dep.builder.dependency("breakpad", .{
.target = target,
.optimize = optimize,
});
try static_libs.append(breakpad_dep.artifact("breakpad").getEmittedBin());
}
// Dynamic link
if (!config.static) {
step.addIncludePath(freetype_dep.path(""));

View File

@ -37,6 +37,7 @@
.oniguruma = .{ .path = "./pkg/oniguruma" },
.opengl = .{ .path = "./pkg/opengl" },
.pixman = .{ .path = "./pkg/pixman" },
.sentry = .{ .path = "./pkg/sentry" },
.simdutf = .{ .path = "./pkg/simdutf" },
.utfcpp = .{ .path = "./pkg/utfcpp" },
.zlib = .{ .path = "./pkg/zlib" },

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-WvHgckWfWLXyvmz8alpqwyAhPaCYe+HdshpcTwrOaf8="
"sha256-mIUl5j3JxtydoV7ayy3aNrt/jR8+a68lQw6lQimLZEw="

157
pkg/breakpad/build.zig Normal file
View File

@ -0,0 +1,157 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const upstream = b.dependency("breakpad", .{});
const lib = b.addStaticLibrary(.{
.name = "breakpad",
.target = target,
.optimize = optimize,
});
lib.linkLibCpp();
lib.addIncludePath(upstream.path("src"));
lib.addIncludePath(b.path("vendor"));
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{});
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = common,
.flags = flags.items,
});
if (target.result.isDarwin()) {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = common_apple,
.flags = flags.items,
});
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = client_apple,
.flags = flags.items,
});
switch (target.result.os.tag) {
.macos => {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = common_mac,
.flags = flags.items,
});
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = client_mac,
.flags = flags.items,
});
},
.ios => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = client_ios,
.flags = flags.items,
}),
else => {},
}
}
if (target.result.os.tag == .linux) {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = common_linux,
.flags = flags.items,
});
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = client_linux,
.flags = flags.items,
});
}
lib.installHeadersDirectory(
upstream.path("src"),
"",
.{ .include_extensions = &.{".h"} },
);
b.installArtifact(lib);
}
const common: []const []const u8 = &.{
"src/common/convert_UTF.cc",
"src/common/md5.cc",
"src/common/string_conversion.cc",
};
const common_linux: []const []const u8 = &.{
"src/common/linux/elf_core_dump.cc",
"src/common/linux/elfutils.cc",
"src/common/linux/file_id.cc",
"src/common/linux/guid_creator.cc",
"src/common/linux/linux_libc_support.cc",
"src/common/linux/memory_mapped_file.cc",
"src/common/linux/safe_readlink.cc",
"src/common/linux/scoped_pipe.cc",
"src/common/linux/scoped_tmpfile.cc",
"src/common/linux/breakpad_getcontext.S",
};
const common_apple: []const []const u8 = &.{
"src/common/mac/arch_utilities.cc",
"src/common/mac/file_id.cc",
"src/common/mac/macho_id.cc",
"src/common/mac/macho_utilities.cc",
"src/common/mac/macho_walker.cc",
"src/common/mac/string_utilities.cc",
};
const common_mac: []const []const u8 = &.{
"src/common/mac/MachIPC.mm",
"src/common/mac/bootstrap_compat.cc",
};
const client_linux: []const []const u8 = &.{
"src/client/minidump_file_writer.cc",
"src/client/linux/crash_generation/crash_generation_client.cc",
"src/client/linux/crash_generation/crash_generation_server.cc",
"src/client/linux/dump_writer_common/thread_info.cc",
"src/client/linux/dump_writer_common/ucontext_reader.cc",
"src/client/linux/handler/exception_handler.cc",
"src/client/linux/handler/minidump_descriptor.cc",
"src/client/linux/log/log.cc",
"src/client/linux/microdump_writer/microdump_writer.cc",
"src/client/linux/minidump_writer/linux_core_dumper.cc",
"src/client/linux/minidump_writer/linux_dumper.cc",
"src/client/linux/minidump_writer/linux_ptrace_dumper.cc",
"src/client/linux/minidump_writer/minidump_writer.cc",
"src/client/linux/minidump_writer/pe_file.cc",
};
const client_apple: []const []const u8 = &.{
"src/client/minidump_file_writer.cc",
"src/client/mac/handler/breakpad_nlist_64.cc",
"src/client/mac/handler/dynamic_images.cc",
"src/client/mac/handler/minidump_generator.cc",
};
const client_mac: []const []const u8 = &.{
"src/client/mac/handler/exception_handler.cc",
"src/client/mac/crash_generation/crash_generation_client.cc",
};
const client_ios: []const []const u8 = &.{
"src/client/ios/exception_handler_no_mach.cc",
"src/client/ios/handler/ios_exception_minidump_generator.mm",
"src/client/mac/crash_generation/ConfigFile.mm",
"src/client/mac/handler/protected_memory_allocator.cc",
};

View File

@ -0,0 +1,13 @@
.{
.name = "breakpad",
.version = "0.1.0",
.paths = .{""},
.dependencies = .{
.breakpad = .{
.url = "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz",
.hash = "12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

File diff suppressed because it is too large Load Diff

241
pkg/sentry/build.zig Normal file
View File

@ -0,0 +1,241 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const backend = b.option(Backend, "backend", "Backend") orelse .inproc;
const transport = b.option(Transport, "transport", "Transport") orelse .none;
const upstream = b.dependency("sentry", .{});
const module = b.addModule("sentry", .{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
});
module.addIncludePath(upstream.path("include"));
const lib = b.addStaticLibrary(.{
.name = "sentry",
.target = target,
.optimize = optimize,
});
lib.linkLibC();
lib.addIncludePath(upstream.path("include"));
lib.addIncludePath(upstream.path("src"));
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
try apple_sdk.addPaths(b, module);
}
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{});
if (target.result.os.tag == .windows) {
try flags.appendSlice(&.{
"-DSENTRY_WITH_UNWINDER_DBGHELP",
});
} else {
try flags.appendSlice(&.{
"-DSENTRY_WITH_UNWINDER_LIBBACKTRACE",
});
}
switch (backend) {
.crashpad => try flags.append("-DSENTRY_BACKEND_CRASHPAD"),
.breakpad => try flags.append("-DSENTRY_BACKEND_BREAKPAD"),
.inproc => try flags.append("-DSENTRY_BACKEND_INPROC"),
.none => {},
}
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = srcs,
.flags = flags.items,
});
// Linux-only
if (target.result.os.tag == .linux) {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"vendor/stb_sprintf.c",
},
.flags = flags.items,
});
}
// Symbolizer + Unwinder
if (target.result.os.tag == .windows) {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/sentry_windows_dbghelp.c",
"src/path/sentry_path_windows.c",
"src/symbolizer/sentry_symbolizer_windows.c",
"src/unwinder/sentry_unwinder_dbghelp.c",
},
.flags = flags.items,
});
} else {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/sentry_unix_pageallocator.c",
"src/path/sentry_path_unix.c",
"src/symbolizer/sentry_symbolizer_unix.c",
"src/unwinder/sentry_unwinder_libbacktrace.c",
},
.flags = flags.items,
});
}
// Module finder
switch (target.result.os.tag) {
.windows => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/modulefinder/sentry_modulefinder_windows.c",
},
.flags = flags.items,
}),
.macos, .ios => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/modulefinder/sentry_modulefinder_apple.c",
},
.flags = flags.items,
}),
.linux => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/modulefinder/sentry_modulefinder_linux.c",
},
.flags = flags.items,
}),
.freestanding => {},
else => {
std.log.warn("target={} not supported", .{target.result.os.tag});
return error.UnsupportedTarget;
},
}
// Transport
switch (transport) {
.curl => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/transports/sentry_transport_curl.c",
},
.flags = flags.items,
}),
.winhttp => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/transports/sentry_transport_winhttp.c",
},
.flags = flags.items,
}),
.none => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/transports/sentry_transport_none.c",
},
.flags = flags.items,
}),
}
// Backend
switch (backend) {
.crashpad => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/backends/sentry_backend_crashpad.cpp",
},
.flags = flags.items,
}),
.breakpad => {
lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/backends/sentry_backend_breakpad.cpp",
},
.flags = flags.items,
});
const breakpad_dep = b.dependency("breakpad", .{
.target = target,
.optimize = optimize,
});
lib.linkLibrary(breakpad_dep.artifact("breakpad"));
// We need to add this because Sentry includes some breakpad
// headers that include this vendored file...
lib.addIncludePath(breakpad_dep.path("vendor"));
},
.inproc => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/backends/sentry_backend_inproc.c",
},
.flags = flags.items,
}),
.none => lib.addCSourceFiles(.{
.root = upstream.path(""),
.files = &.{
"src/backends/sentry_backend_none.c",
},
.flags = flags.items,
}),
}
lib.installHeadersDirectory(
upstream.path("include"),
"",
.{ .include_extensions = &.{".h"} },
);
b.installArtifact(lib);
}
const srcs: []const []const u8 = &.{
"src/sentry_alloc.c",
"src/sentry_backend.c",
"src/sentry_core.c",
"src/sentry_database.c",
"src/sentry_envelope.c",
"src/sentry_info.c",
"src/sentry_json.c",
"src/sentry_logger.c",
"src/sentry_options.c",
"src/sentry_os.c",
"src/sentry_random.c",
"src/sentry_ratelimiter.c",
"src/sentry_scope.c",
"src/sentry_session.c",
"src/sentry_slice.c",
"src/sentry_string.c",
"src/sentry_sync.c",
"src/sentry_transport.c",
"src/sentry_utils.c",
"src/sentry_uuid.c",
"src/sentry_value.c",
"src/sentry_tracing.c",
"src/path/sentry_path.c",
"src/transports/sentry_disk_transport.c",
"src/transports/sentry_function_transport.c",
"src/unwinder/sentry_unwinder.c",
"vendor/mpack.c",
};
pub const Backend = enum { crashpad, breakpad, inproc, none };
pub const Transport = enum { curl, winhttp, none };

14
pkg/sentry/build.zig.zon Normal file
View File

@ -0,0 +1,14 @@
.{
.name = "sentry",
.version = "0.7.8",
.paths = .{""},
.dependencies = .{
.sentry = .{
.url = "https://github.com/getsentry/sentry-native/archive/refs/tags/0.7.8.tar.gz",
.hash = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e",
},
.apple_sdk = .{ .path = "../apple-sdk" },
.breakpad = .{ .path = "../breakpad" },
},
}

3
pkg/sentry/c.zig Normal file
View File

@ -0,0 +1,3 @@
pub const c = @cImport({
@cInclude("sentry.h");
});

31
pkg/sentry/envelope.zig Normal file
View File

@ -0,0 +1,31 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Value = @import("value.zig").Value;
/// sentry_envelope_t
pub const Envelope = opaque {
pub fn deinit(self: *Envelope) void {
c.sentry_envelope_free(@ptrCast(self));
}
pub fn writeToFile(self: *Envelope, path: []const u8) !void {
if (c.sentry_envelope_write_to_file_n(
@ptrCast(self),
path.ptr,
path.len,
) != 0) return error.WriteFailed;
}
pub fn serialize(self: *Envelope) []u8 {
var len: usize = 0;
const ptr = c.sentry_envelope_serialize(@ptrCast(self), &len).?;
return ptr[0..len];
}
pub fn event(self: *Envelope) ?Value {
const val: Value = .{ .value = c.sentry_envelope_get_event(@ptrCast(self)) };
if (val.isNull()) return null;
return val;
}
};

8
pkg/sentry/level.zig Normal file
View File

@ -0,0 +1,8 @@
/// sentry_level_t
pub const Level = enum(c_int) {
debug = -1,
info = 0,
warning = 1,
err = 2,
fatal = 3,
};

35
pkg/sentry/main.zig Normal file
View File

@ -0,0 +1,35 @@
pub const c = @import("c.zig").c;
const transport = @import("transport.zig");
pub const Envelope = @import("envelope.zig").Envelope;
pub const Level = @import("level.zig").Level;
pub const Transport = transport.Transport;
pub const Value = @import("value.zig").Value;
pub const UUID = @import("uuid.zig").UUID;
pub fn captureEvent(value: Value) ?UUID {
const uuid: UUID = .{ .value = c.sentry_capture_event(value.value) };
if (uuid.isNil()) return null;
return uuid;
}
pub fn setContext(key: []const u8, value: Value) void {
c.sentry_set_context_n(key.ptr, key.len, value.value);
}
pub fn removeContext(key: []const u8) void {
c.sentry_remove_context_n(key.ptr, key.len);
}
pub fn setTag(key: []const u8, value: []const u8) void {
c.sentry_set_tag_n(key.ptr, key.len, value.ptr, value.len);
}
pub fn free(ptr: *anyopaque) void {
c.sentry_free(ptr);
}
test {
@import("std").testing.refAllDecls(@This());
}

26
pkg/sentry/transport.zig Normal file
View File

@ -0,0 +1,26 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Envelope = @import("envelope.zig").Envelope;
/// sentry_transport_t
pub const Transport = opaque {
pub const SendFunc = *const fn (envelope: *Envelope, state: ?*anyopaque) callconv(.C) void;
pub const FreeFunc = *const fn (state: ?*anyopaque) callconv(.C) void;
pub fn init(f: SendFunc) *Transport {
return @ptrCast(c.sentry_transport_new(@ptrCast(f)).?);
}
pub fn deinit(self: *Transport) void {
c.sentry_transport_free(@ptrCast(self));
}
pub fn setState(self: *Transport, state: ?*anyopaque) void {
c.sentry_transport_set_state(@ptrCast(self), state);
}
pub fn setStateFreeFunc(self: *Transport, f: FreeFunc) void {
c.sentry_transport_set_free_func(@ptrCast(self), f);
}
};

22
pkg/sentry/uuid.zig Normal file
View File

@ -0,0 +1,22 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
/// sentry_uuid_t
pub const UUID = struct {
value: c.sentry_uuid_t,
pub fn init() UUID {
return .{ .value = c.sentry_uuid_new_v4() };
}
pub fn isNil(self: UUID) bool {
return c.sentry_uuid_is_nil(&self.value) != 0;
}
pub fn string(self: UUID) [36:0]u8 {
var buf: [36:0]u8 = undefined;
c.sentry_uuid_as_string(&self.value, &buf);
return buf;
}
};

75
pkg/sentry/value.zig Normal file
View File

@ -0,0 +1,75 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Level = @import("level.zig").Level;
/// sentry_value_t
pub const Value = struct {
/// The underlying value. This is a union that could be represented with
/// an extern union but I don't want to risk C ABI issues so we wrap it
/// in a struct.
value: c.sentry_value_t,
pub fn initMessageEvent(
level: Level,
logger: ?[]const u8,
message: []const u8,
) Value {
return .{ .value = c.sentry_value_new_message_event_n(
@intFromEnum(level),
if (logger) |v| v.ptr else null,
if (logger) |v| v.len else 0,
message.ptr,
message.len,
) };
}
pub fn initObject() Value {
return .{ .value = c.sentry_value_new_object() };
}
pub fn initString(value: []const u8) Value {
return .{ .value = c.sentry_value_new_string_n(value.ptr, value.len) };
}
pub fn initBool(value: bool) Value {
return .{ .value = c.sentry_value_new_bool(@intFromBool(value)) };
}
pub fn initInt32(value: i32) Value {
return .{ .value = c.sentry_value_new_int32(value) };
}
pub fn decref(self: Value) void {
c.sentry_value_decref(self.value);
}
pub fn incref(self: Value) Value {
c.sentry_value_incref(self.value);
}
pub fn isNull(self: Value) bool {
return c.sentry_value_is_null(self.value) != 0;
}
/// sentry_value_set_by_key_n
pub fn set(self: Value, key: []const u8, value: Value) void {
_ = c.sentry_value_set_by_key_n(
self.value,
key.ptr,
key.len,
value.value,
);
}
/// sentry_value_set_by_key_n
pub fn get(self: Value, key: []const u8) ?Value {
const val: Value = .{ .value = c.sentry_value_get_by_key_n(
self.value,
key.ptr,
key.len,
) };
if (val.isNull()) return null;
return val;
}
};

View File

@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("global.zig").state;
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const unicode = @import("unicode/main.zig");
const renderer = @import("renderer.zig");
const termio = @import("termio.zig");
@ -1218,6 +1219,10 @@ fn queueRender(self: *Surface) !void {
}
pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
const new_screen_size: renderer.ScreenSize = .{
.width = size.width,
.height = size.height,
@ -1274,6 +1279,10 @@ fn resize(self: *Surface, size: renderer.ScreenSize) !void {
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
// log.debug("text preeditCallback value={any}", .{preedit_});
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
@ -1335,6 +1344,10 @@ pub fn keyCallback(
) !InputEffect {
// log.debug("text keyCallback event={}", .{event});
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// Setup our inspector event if we have an inspector.
var insp_ev: ?inspector.key.Event = if (self.inspector != null) ev: {
var copy = event;
@ -1729,6 +1742,10 @@ fn encodeKey(
/// if bracketed mode is on this will do a bracketed paste. Otherwise,
/// this will filter newlines to '\r'.
pub fn textCallback(self: *Surface, text: []const u8) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
try self.completeClipboardPaste(text, true);
}
@ -1736,6 +1753,10 @@ pub fn textCallback(self: *Surface, text: []const u8) !void {
/// of focus state. This is used to pause rendering when the surface
/// is not visible, and also re-render when it becomes visible again.
pub fn occlusionCallback(self: *Surface, visible: bool) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
_ = self.renderer_thread.mailbox.push(.{
.visible = visible,
}, .{ .forever = {} });
@ -1743,6 +1764,10 @@ pub fn occlusionCallback(self: *Surface, visible: bool) !void {
}
pub fn focusCallback(self: *Surface, focused: bool) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// Notify our render thread of the new state
_ = self.renderer_thread.mailbox.push(.{
.focus = focused,
@ -1813,6 +1838,10 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
}
pub fn refreshCallback(self: *Surface) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// The point of this callback is to schedule a render, so do that.
try self.queueRender();
}
@ -1825,6 +1854,10 @@ pub fn scrollCallback(
) !void {
// log.info("SCROLL: xoff={} yoff={} mods={}", .{ xoff, yoff, scroll_mods });
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// Always show the mouse again if it is hidden
if (self.mouse.hidden) self.showMouse();
@ -1980,6 +2013,10 @@ pub fn scrollCallback(
/// This is called when the content scale of the surface changes. The surface
/// can then update any DPI-sensitive state.
pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// Calculate the new DPI
const x_dpi = content_scale.x * font.face.default_dpi;
const y_dpi = content_scale.y * font.face.default_dpi;
@ -2293,6 +2330,10 @@ pub fn mouseButtonCallback(
button: input.MouseButton,
mods: input.Mods,
) !bool {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// log.debug("mouse action={} button={} mods={}", .{ action, button, mods });
// If we have an inspector, we always queue a render
@ -2790,6 +2831,10 @@ pub fn mousePressureCallback(
stage: input.MousePressureStage,
pressure: f64,
) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// We don't currently use the pressure value for anything. In the
// future, we could report this to applications using new mouse
// events or utilize it for some custom UI.
@ -2824,6 +2869,10 @@ pub fn cursorPosCallback(
self: *Surface,
pos: apprt.CursorPos,
) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// Always show the mouse again if it is hidden
if (self.mouse.hidden) self.showMouse();
@ -3229,6 +3278,10 @@ fn dragLeftClickBefore(
/// Call to notify Ghostty that the color scheme for the terminal has
/// changed.
pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
// If our scheme didn't change, then we don't do anything.
if (self.color_scheme == scheme) return;
@ -3673,6 +3726,20 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.quit => try self.app.setQuit(),
.crash => |location| switch (location) {
.main => @panic("crash binding action, crashing intentionally"),
.render => {
_ = self.renderer_thread.mailbox.push(.{ .crash = {} }, .{ .forever = {} });
self.queueRender() catch |err| {
// Not a big deal if this fails.
log.warn("failed to notify renderer of crash message err={}", .{err});
};
},
.io => self.io.queueMessage(.{ .crash = {} }, .unlocked),
},
.adjust_selection => |direction| {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
@ -4083,6 +4150,13 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
try self.rt_surface.showDesktopNotification(title, body);
}
fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
return .{
.type = .main,
.surface = self,
};
}
pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf");
pub const face_bold_ttf = @embedFile("font/res/JetBrainsMono-Bold.ttf");
pub const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");

16
src/crash/main.zig Normal file
View File

@ -0,0 +1,16 @@
//! The crash package contains all the logic around crash handling,
//! whether that's setting up the system to catch crashes (Sentry client),
//! introspecting crash reports, writing crash reports to disk, etc.
const sentry_envelope = @import("sentry_envelope.zig");
pub const sentry = @import("sentry.zig");
pub const Envelope = sentry_envelope.Envelope;
// The main init/deinit functions for global state.
pub const init = sentry.init;
pub const deinit = sentry.deinit;
test {
@import("std").testing.refAllDecls(@This());
}

279
src/crash/sentry.zig Normal file
View File

@ -0,0 +1,279 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const sentry = @import("sentry");
const internal_os = @import("../os/main.zig");
const crash = @import("main.zig");
const state = &@import("../global.zig").state;
const Surface = @import("../Surface.zig");
const log = std.log.scoped(.sentry);
/// The global state for the Sentry SDK. This is unavoidable since crash
/// handling is a global process-wide thing.
var init_thread: ?std.Thread = null;
/// Thread-local state that can be set by thread main functions so that
/// crashes have more context.
///
/// This is a hack over Sentry native SDK limitations. The native SDK has
/// one global scope for all threads and no support for thread-local scopes.
/// This means that if we want to set thread-specific data we have to do it
/// on our own in the on crash callback.
pub const ThreadState = struct {
/// Thread type, used to tag the crash
type: Type,
/// The surface that this thread is attached to.
surface: *Surface,
pub const Type = enum { main, renderer, io };
};
/// See ThreadState. This should only ever be set by the owner of the
/// thread entry function.
pub threadlocal var thread_state: ?ThreadState = null;
/// Process-wide initialization of our Sentry client.
///
/// This should only be called from one thread, and deinit should be called
/// from the same thread that calls init to avoid data races.
///
/// PRIVACY NOTE: I want to make it very clear that Ghostty by default does
/// NOT send any data over the network. We use the Sentry native SDK to collect
/// crash reports and logs, but we only store them locally (see Transport).
/// It is up to the user to grab the logs and manually send them to us
/// (or they own Sentry instance) if they want to.
pub fn init(gpa: Allocator) !void {
// Not supported on Windows currently, doesn't build.
if (comptime builtin.os.tag == .windows) return;
// const start = try std.time.Instant.now();
// const start_micro = std.time.microTimestamp();
// defer {
// const end = std.time.Instant.now() catch unreachable;
// // "[updateFrame critical time] <START us>\t<TIME_TAKEN us>"
// std.log.err("[sentry init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us });
// }
// Must only start once
assert(init_thread == null);
// We use a thread for initializing Sentry because initialization takes
// ~2k ns on my M3 Max. That's not a LOT of time but it's enough to be
// 90% of our pre-App startup time. Everything Sentry is doing initially
// is safe to do on a separate thread and fast enough that its very
// likely to be done before a crash occurs.
const thr = try std.Thread.spawn(
.{},
initThread,
.{gpa},
);
thr.setName("sentry-init") catch {};
init_thread = thr;
}
fn initThread(gpa: Allocator) !void {
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const alloc = arena.allocator();
const transport = sentry.Transport.init(&Transport.send);
errdefer transport.deinit();
const opts = sentry.c.sentry_options_new();
errdefer sentry.c.sentry_options_free(opts);
sentry.c.sentry_options_set_release_n(
opts,
build_config.version_string.ptr,
build_config.version_string.len,
);
sentry.c.sentry_options_set_transport(opts, @ptrCast(transport));
// Set our crash callback. See beforeSend for more details on what we
// do here and why we use this.
sentry.c.sentry_options_set_before_send(opts, beforeSend, null);
// Determine the Sentry cache directory.
const cache_dir = try internal_os.xdg.cache(alloc, .{ .subdir = "ghostty/sentry" });
sentry.c.sentry_options_set_database_path_n(
opts,
cache_dir.ptr,
cache_dir.len,
);
if (comptime builtin.mode == .Debug) {
// Debug logging for Sentry
sentry.c.sentry_options_set_debug(opts, @intFromBool(true));
}
// Initialize
if (sentry.c.sentry_init(opts) != 0) return error.SentryInitFailed;
// Setup some basic tags that we always want present
sentry.setTag("build-mode", build_config.mode_string);
sentry.setTag("app-runtime", @tagName(build_config.app_runtime));
sentry.setTag("font-backend", @tagName(build_config.font_backend));
sentry.setTag("renderer", @tagName(build_config.renderer));
// Log some information about sentry
log.debug("sentry initialized database={s}", .{cache_dir});
}
/// Process-wide deinitialization of our Sentry client. This ensures all
/// our data is flushed.
pub fn deinit() void {
if (comptime builtin.os.tag == .windows) return;
// If we're still initializing then wait for init to finish. This
// is highly unlikely since init is a very fast operation but we want
// to avoid the possibility.
const thr = init_thread orelse return;
thr.join();
_ = sentry.c.sentry_close();
}
fn beforeSend(
event_val: sentry.c.sentry_value_t,
_: ?*anyopaque,
_: ?*anyopaque,
) callconv(.C) sentry.c.sentry_value_t {
// The native SDK at the time of writing doesn't support thread-local
// scopes. The full SDK has one global scope. So we use the beforeSend
// handler to set thread-specific data such as window size, grid size,
// etc. that we can use to debug crashes.
// If we don't have thread state we can't reliably determine
// metadata such as surface dimensions. In the future we can probably
// drop full app state (all surfaces, all windows, etc.).
const thr_state = thread_state orelse {
log.debug("no thread state, skipping crash metadata", .{});
return event_val;
};
// Get our event contexts. At this point Sentry has already merged
// all the contexts so we should have this key. If not, we create it.
const event: sentry.Value = .{ .value = event_val };
const contexts = event.get("contexts") orelse contexts: {
const obj = sentry.Value.initObject();
event.set("contexts", obj);
break :contexts obj;
};
const tags = event.get("tags") orelse tags: {
const obj = sentry.Value.initObject();
event.set("tags", obj);
break :tags obj;
};
// Store our thread type
tags.set("thread-type", sentry.Value.initString(@tagName(thr_state.type)));
// Read the surface data. This is likely unsafe because on a crash
// other threads can continue running. We don't have race-safe way to
// access this data so this might be corrupted but it's most likely fine.
{
const obj = sentry.Value.initObject();
errdefer obj.decref();
const surface = thr_state.surface;
obj.set(
"screen-width",
sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.width) orelse -1),
);
obj.set(
"screen-height",
sentry.Value.initInt32(std.math.cast(i32, surface.screen_size.height) orelse -1),
);
obj.set(
"grid-columns",
sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.columns) orelse -1),
);
obj.set(
"grid-rows",
sentry.Value.initInt32(std.math.cast(i32, surface.grid_size.rows) orelse -1),
);
obj.set(
"cell-width",
sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.width) orelse -1),
);
obj.set(
"cell-height",
sentry.Value.initInt32(std.math.cast(i32, surface.cell_size.height) orelse -1),
);
contexts.set("Dimensions", obj);
}
return event_val;
}
pub const Transport = struct {
pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.C) void {
_ = ud;
defer envelope.deinit();
// Call our internal impl. If it fails there is nothing we can do
// but log to the user.
sendInternal(envelope) catch |err| {
log.warn("failed to persist crash report err={}", .{err});
};
}
/// Implementation of send but we can use Zig errors.
fn sendInternal(envelope: *sentry.Envelope) !void {
var arena = std.heap.ArenaAllocator.init(state.alloc);
defer arena.deinit();
const alloc = arena.allocator();
// Parse into an envelope structure
const json = envelope.serialize();
defer sentry.free(@ptrCast(json.ptr));
var parsed: crash.Envelope = parsed: {
var fbs = std.io.fixedBufferStream(json);
break :parsed try crash.Envelope.parse(alloc, fbs.reader());
};
defer parsed.deinit();
// If our envelope doesn't have an event then we don't do anything.
// To figure this out we first encode it into a string, parse it,
// and check if it has an event. Kind of wasteful but the best
// option we have at the time of writing this since the C API doesn't
// expose this information.
if (try shouldDiscard(&parsed)) {
log.info("sentry envelope does not contain crash, discarding", .{});
return;
}
// Generate a UUID for this envelope. The envelope DOES have an event_id
// header but I don't think there is any public API way to get it
// afaict so we generate a new UUID for the filename just so we don't
// conflict.
const uuid = sentry.UUID.init();
// 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);
// Build our final path and write to it.
const path = try std.fs.path.join(alloc, &.{
crash_dir,
try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}),
});
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writer().writeAll(json);
log.warn("crash report written to disk path={s}", .{path});
}
fn shouldDiscard(envelope: *const crash.Envelope) !bool {
// If we have an event item then we're good.
for (envelope.items) |item| {
if (item.type == .event) return false;
}
return true;
}
};

View File

@ -0,0 +1,284 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
/// The Sentry Envelope format: https://develop.sentry.dev/sdk/envelopes/
///
/// The envelope is our primary crash report format since use the Sentry
/// client. It is designed and created by Sentry but is an open format
/// in that it is publicly documented and can be used by any system. This
/// lets us utilize the Sentry client for crash capture but also gives us
/// the opportunity to migrate to another system if we need to, and doesn't
/// force any user or developer to use Sentry the SaaS if they don't want
/// to.
///
/// This struct implements reading the envelope format (writing is not needed
/// currently but can be added later). It is incomplete; I only implemented
/// what I needed at the time.
pub const Envelope = struct {
// Developer note: this struct is really geared towards decoding an
// already-encoded envelope vs. building up an envelope from rich
// data types. I think it can be used for both I just didn't have
// the latter need.
//
// If I were to make that ability more enjoyable I'd probably change
// Item below a tagged union of either an "EncodedItem" (which is the
// current Item type) or a "DecodedItem" which is a union(ItemType)
// to its rich data type. This would allow the user to cheaply append
// items to the envelope without paying the encoding cost until
// serialization time.
//
// The way it is now, the user has to encode every entry as they build
// the envelope, which is probably fine but I wanted to write this down
// for my future self or some future contributor since it is fresh
// in my mind. Cheers.
/// The arena that the envelope is allocated in.
arena: std.heap.ArenaAllocator,
/// The headers of the envelope decoded into a json ObjectMap.
headers: std.json.ObjectMap,
/// The items in the envelope in the order they're encoded.
items: []const Item,
/// An encoded item. It is "encoded" in the sense that the payload
/// is a byte slice. The headers are "decoded" into a json ObjectMap
/// but that's still a pretty low-level representation.
pub const Item = struct {
headers: std.json.ObjectMap,
type: ItemType,
payload: []const u8,
};
/// Parse an envelope from a reader.
///
/// The full envelope must fit in memory for this to succeed. This
/// will always copy the data from the reader into memory, even if the
/// reader is already in-memory (i.e. a FixedBufferStream). This
/// simplifies memory lifetimes at the expense of a copy, but envelope
/// parsing in our use case is not a hot path.
pub fn parse(
alloc_gpa: Allocator,
reader: anytype,
) !Envelope {
// We use an arena allocator to read from reader. We pair this
// with `alloc_if_needed` when parsing json to allow the json
// to reference the arena-allocated memory if it can. That way both
// our temp and perm memory is part of the same arena. This slightly
// bloats our memory requirements but reduces allocations.
var arena = std.heap.ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
// Parse our elements. We do this outside of the struct assignment
// below to avoid the issue where order matters in struct assignment.
const headers = try parseHeader(alloc, reader);
const items = try parseItems(alloc, reader);
return .{
.headers = headers,
.items = items,
.arena = arena,
};
}
fn parseHeader(
alloc: Allocator,
reader: anytype,
) !std.json.ObjectMap {
var buf: std.ArrayListUnmanaged(u8) = .{};
reader.streamUntilDelimiter(
buf.writer(alloc),
'\n',
1024 * 1024, // 1MB, arbitrary choice
) catch |err| switch (err) {
// Envelope can be header-only.
error.EndOfStream => {},
else => |v| return v,
};
const value = try std.json.parseFromSliceLeaky(
std.json.Value,
alloc,
buf.items,
.{ .allocate = .alloc_if_needed },
);
return switch (value) {
.object => |map| map,
else => error.EnvelopeMalformedHeaders,
};
}
fn parseItems(
alloc: Allocator,
reader: anytype,
) ![]const Item {
var items = std.ArrayList(Item).init(alloc);
defer items.deinit();
while (try parseOneItem(alloc, reader)) |item| try items.append(item);
return try items.toOwnedSlice();
}
fn parseOneItem(
alloc: Allocator,
reader: anytype,
) !?Item {
// Get the next item which must start with a header.
var buf: std.ArrayListUnmanaged(u8) = .{};
reader.streamUntilDelimiter(
buf.writer(alloc),
'\n',
1024 * 1024, // 1MB, arbitrary choice
) catch |err| switch (err) {
error.EndOfStream => return null,
else => |v| return v,
};
// Parse the header JSON
const headers: std.json.ObjectMap = headers: {
const line = std.mem.trim(u8, buf.items, " \t");
if (line.len == 0) return null;
const value = try std.json.parseFromSliceLeaky(
std.json.Value,
alloc,
line,
.{ .allocate = .alloc_if_needed },
);
break :headers switch (value) {
.object => |map| map,
else => return error.EnvelopeItemMalformedHeaders,
};
};
// Get the event type
const typ: ItemType = if (headers.get("type")) |v| switch (v) {
.string => |str| std.meta.stringToEnum(
ItemType,
str,
) orelse .unknown,
else => return error.EnvelopeItemTypeMissing,
} else return error.EnvelopeItemTypeMissing;
// Get the payload length. The length is not required. If the length
// is not specified then it is the next line ending in `\n`.
const len_: ?u64 = if (headers.get("length")) |v| switch (v) {
.integer => |int| std.math.cast(
u64,
int,
) orelse return error.EnvelopeItemLengthMalformed,
else => return error.EnvelopeItemLengthMalformed,
} else null;
// Get the payload
const payload: []const u8 = if (len_) |len| payload: {
// The payload length is specified so read the exact length.
var payload = std.ArrayList(u8).init(alloc);
defer payload.deinit();
for (0..len) |_| {
const byte = reader.readByte() catch |err| switch (err) {
error.EndOfStream => return error.EnvelopeItemPayloadTooShort,
else => return err,
};
try payload.append(byte);
}
break :payload try payload.toOwnedSlice();
} else payload: {
// The payload is the next line ending in `\n`. It is required.
var payload = std.ArrayList(u8).init(alloc);
defer payload.deinit();
reader.streamUntilDelimiter(
payload.writer(),
'\n',
1024 * 1024 * 50, // 50MB, arbitrary choice
) catch |err| switch (err) {
error.EndOfStream => return error.EnvelopeItemPayloadTooShort,
else => |v| return v,
};
break :payload try payload.toOwnedSlice();
};
return .{
.headers = headers,
.type = typ,
.payload = payload,
};
}
pub fn deinit(self: *Envelope) void {
self.arena.deinit();
}
};
/// The various item types that can be in an envelope. This is a point
/// in time snapshot of the types that are known whenever this is edited.
/// Event types can be introduced at any time and unknown types will
/// take the "unknown" enum value.
///
/// https://develop.sentry.dev/sdk/envelopes/#data-model
pub const ItemType = enum {
/// Special event type for when the item type is unknown.
unknown,
/// Documented event types
event,
transaction,
attachment,
session,
sessions,
statsd,
metric_meta,
user_feedback,
client_report,
replay_event,
replay_recording,
profile,
check_in,
};
test "Envelope parse" {
const testing = std.testing;
const alloc = testing.allocator;
var fbs = std.io.fixedBufferStream(
\\{}
);
var v = try Envelope.parse(alloc, fbs.reader());
defer v.deinit();
}
test "Envelope parse session" {
const testing = std.testing;
const alloc = testing.allocator;
var fbs = std.io.fixedBufferStream(
\\{}
\\{"type":"session","length":218}
\\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}}
);
var v = try Envelope.parse(alloc, fbs.reader());
defer v.deinit();
try testing.expectEqual(@as(usize, 1), v.items.len);
try testing.expectEqual(ItemType.session, v.items[0].type);
}
test "Envelope parse end in new line" {
const testing = std.testing;
const alloc = testing.allocator;
var fbs = std.io.fixedBufferStream(
\\{}
\\{"type":"session","length":218}
\\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}}
\\
);
var v = try Envelope.parse(alloc, fbs.reader());
defer v.deinit();
try testing.expectEqual(@as(usize, 1), v.items.len);
try testing.expectEqual(ItemType.session, v.items[0].type);
}

View File

@ -7,6 +7,7 @@ const fontconfig = @import("fontconfig");
const glslang = @import("glslang");
const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const xev = @import("xev");
@ -39,6 +40,14 @@ pub const GlobalState = struct {
/// Initialize the global state.
pub fn init(self: *GlobalState) !void {
// const start = try std.time.Instant.now();
// const start_micro = std.time.microTimestamp();
// defer {
// const end = std.time.Instant.now() catch unreachable;
// // "[updateFrame critical time] <START us>\t<TIME_TAKEN us>"
// std.log.err("[global init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us });
// }
// Initialize ourself to nothing so we don't have any extra state.
// IMPORTANT: this MUST be initialized before any log output because
// the log function uses the global state.
@ -117,6 +126,18 @@ pub const GlobalState = struct {
// First things first, we fix our file descriptors
internal_os.fixMaxFiles();
// Initialize our crash reporting.
try crash.init(self.alloc);
// const sentrylib = @import("sentry");
// if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent(
// .info,
// null,
// "hello, world",
// ))) |uuid| {
// std.log.warn("uuid={s}", .{uuid.string()});
// } else std.log.warn("failed to capture event", .{});
// We need to make sure the process locale is set properly. Locale
// affects a lot of behaviors in a shell.
try internal_os.ensureLocale(self.alloc);
@ -138,6 +159,9 @@ pub const GlobalState = struct {
pub fn deinit(self: *GlobalState) void {
if (self.resources_dir) |dir| self.alloc.free(dir);
// Flush our crash logs
crash.deinit();
if (self.gpa) |*value| {
// We want to ensure that we deinit the GPA because this is
// the point at which it will output if there were safety violations.

View File

@ -300,6 +300,28 @@ pub const Action = union(enum) {
/// Quit ghostty.
quit: void,
/// Crash ghostty in the desired thread for the focused surface.
///
/// WARNING: This is a hard crash (panic) and data can be lost.
///
/// The purpose of this action is to test crash handling. For some
/// users, it may be useful to test crash reporting functionality in
/// order to determine if it all works as expected.
///
/// The value determines the crash location:
///
/// - "main" - crash on the main (GUI) thread.
/// - "io" - crash on the IO thread for the focused surface.
/// - "render" - crash on the render thread for the focused surface.
///
crash: CrashThread,
pub const CrashThread = enum {
main,
io,
render,
};
pub const CursorKey = struct {
normal: []const u8,
application: []const u8,

View File

@ -181,6 +181,7 @@ test {
// Libraries
_ = @import("segmented_pool.zig");
_ = @import("crash/main.zig");
_ = @import("inspector/main.zig");
_ = @import("terminal/main.zig");
_ = @import("terminfo/main.zig");

View File

@ -21,17 +21,54 @@ pub const Options = struct {
/// Get the XDG user config directory. The returned value is allocated.
pub fn config(alloc: Allocator, opts: Options) ![]u8 {
return try dir(alloc, opts, .{
.env = "XDG_CONFIG_HOME",
.windows_env = "LOCALAPPDATA",
.default_subdir = ".config",
});
}
/// Get the XDG cache directory. The returned value is allocated.
pub fn cache(alloc: Allocator, opts: Options) ![]u8 {
return try dir(alloc, opts, .{
.env = "XDG_CACHE_HOME",
.windows_env = "LOCALAPPDATA",
.default_subdir = ".cache",
});
}
/// Get the XDG state directory. The returned value is allocated.
pub fn state(alloc: Allocator, opts: Options) ![]u8 {
return try dir(alloc, opts, .{
.env = "XDG_STATE_HOME",
.windows_env = "LOCALAPPDATA",
.default_subdir = ".local/state",
});
}
const InternalOptions = struct {
env: []const u8,
windows_env: []const u8,
default_subdir: []const u8,
};
/// Unified helper to get XDG directories that follow a common pattern.
fn dir(
alloc: Allocator,
opts: Options,
internal_opts: InternalOptions,
) ![]u8 {
// First check the env var. On Windows we have to allocate so this tracks
// both whether we have the env var and whether we own it.
// on Windows we treat `LOCALAPPDATA` as a fallback for `XDG_CONFIG_HOME`
const env_, const owned = switch (builtin.os.tag) {
else => .{ posix.getenv("XDG_CONFIG_HOME"), false },
else => .{ posix.getenv(internal_opts.env), false },
.windows => windows: {
if (std.process.getEnvVarOwned(alloc, "XDG_CONFIG_HOME")) |env| {
if (std.process.getEnvVarOwned(alloc, internal_opts.env)) |env| {
break :windows .{ env, true };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {
if (std.process.getEnvVarOwned(alloc, "LOCALAPPDATA")) |env| {
if (std.process.getEnvVarOwned(alloc, internal_opts.windows_env)) |env| {
break :windows .{ env, true };
} else |err2| switch (err2) {
error.EnvironmentVariableNotFound => break :windows .{ null, false },
@ -60,7 +97,7 @@ pub fn config(alloc: Allocator, opts: Options) ![]u8 {
if (opts.home) |home| {
return try std.fs.path.join(alloc, &[_][]const u8{
home,
".config",
internal_opts.default_subdir,
opts.subdir orelse "",
});
}
@ -70,7 +107,7 @@ pub fn config(alloc: Allocator, opts: Options) ![]u8 {
if (try homedir.home(&buf)) |home| {
return try std.fs.path.join(alloc, &[_][]const u8{
home,
".config",
internal_opts.default_subdir,
opts.subdir orelse "",
});
}

View File

@ -137,6 +137,25 @@ const PosixPty = struct {
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
pub fn childPreExec(self: Pty) !void {
// Reset our signals
var sa: posix.Sigaction = .{
.handler = .{ .handler = posix.SIG.DFL },
.mask = posix.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.ABRT, &sa, null);
try posix.sigaction(posix.SIG.ALRM, &sa, null);
try posix.sigaction(posix.SIG.BUS, &sa, null);
try posix.sigaction(posix.SIG.CHLD, &sa, null);
try posix.sigaction(posix.SIG.FPE, &sa, null);
try posix.sigaction(posix.SIG.HUP, &sa, null);
try posix.sigaction(posix.SIG.ILL, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.SEGV, &sa, null);
try posix.sigaction(posix.SIG.TRAP, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
try posix.sigaction(posix.SIG.QUIT, &sa, null);
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;

View File

@ -5,6 +5,7 @@ pub const Thread = @This();
const std = @import("std");
const builtin = @import("builtin");
const xev = @import("xev");
const crash = @import("../crash/main.zig");
const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@ -191,6 +192,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("renderer thread exited", .{});
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .renderer,
.surface = self.renderer.surface_mailbox.surface,
};
defer crash.sentry.thread_state = null;
// Run our loop start/end callbacks if the renderer cares.
const has_loop = @hasDecl(renderer.Renderer, "loopEnter");
if (has_loop) try self.renderer.loopEnter(self);
@ -263,6 +271,8 @@ fn drainMailbox(self: *Thread) !void {
while (self.mailbox.pop()) |message| {
log.debug("mailbox message={}", .{message});
switch (message) {
.crash => @panic("crash request, crashing intentionally"),
.visible => |v| {
// Set our visible state
self.flags.visible = v;

View File

@ -8,6 +8,10 @@ const terminal = @import("../terminal/main.zig");
/// The messages that can be sent to a renderer thread.
pub const Message = union(enum) {
/// Purposely crash the renderer. This is used for testing and debugging.
/// See the "crash" binding action.
crash: void,
/// A change in state in the window focus that this renderer is
/// rendering within. This is only sent when a change is detected so
/// the renderer is expected to handle all of these.

View File

@ -15,6 +15,7 @@ const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const builtin = @import("builtin");
const xev = @import("xev");
const crash = @import("../crash/main.zig");
const termio = @import("../termio.zig");
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
@ -200,6 +201,13 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
fn threadMain_(self: *Thread, io: *termio.Termio) !void {
defer log.debug("IO thread exited", .{});
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .io,
.surface = io.surface_mailbox.surface,
};
defer crash.sentry.thread_state = null;
// Get the mailbox. This must be an SPSC mailbox for threading.
const mailbox = switch (io.mailbox) {
.spsc => |*v| v,
@ -261,6 +269,7 @@ fn drainMailbox(
log.debug("mailbox message={}", .{message});
switch (message) {
.crash => @panic("crash request, crashing intentionally"),
.change_config => |config| {
defer config.alloc.destroy(config.ptr);
try io.changeConfig(data, config.ptr);

View File

@ -29,6 +29,10 @@ pub const Message = union(enum) {
padding: renderer.Padding,
};
/// Purposely crash the renderer. This is used for testing and debugging.
/// See the "crash" binding action.
crash: void,
/// The derived configuration to update the implementation with. This
/// is allocated via the allocator and is expected to be freed when done.
change_config: struct {