termio: handle termio thread failure by showing a message in window

Fixes #1301
This commit is contained in:
Mitchell Hashimoto
2024-01-15 19:58:29 -08:00
parent e4aa17a2dc
commit b87bbd55c5
2 changed files with 101 additions and 9 deletions

View File

@ -706,13 +706,20 @@ pub fn invokeCharset(
} }
} }
/// Print UTF-8 encoded string to the terminal. This string must be /// Print UTF-8 encoded string to the terminal.
/// a single line, newlines and carriage returns and other control
/// characters are not processed.
pub fn printString(self: *Terminal, str: []const u8) !void { pub fn printString(self: *Terminal, str: []const u8) !void {
const view = try std.unicode.Utf8View.init(str); const view = try std.unicode.Utf8View.init(str);
var it = view.iterator(); var it = view.iterator();
while (it.nextCodepoint()) |cp| try self.print(cp); while (it.nextCodepoint()) |cp| {
switch (cp) {
'\n' => {
self.carriageReturn();
try self.linefeed();
},
else => try self.print(cp),
}
}
} }
pub fn print(self: *Terminal, c: u21) !void { pub fn print(self: *Terminal, c: u21) !void {

View File

@ -3,6 +3,7 @@
pub const Thread = @This(); pub const Thread = @This();
const std = @import("std"); const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const builtin = @import("builtin"); const builtin = @import("builtin");
const xev = @import("xev"); const xev = @import("xev");
const termio = @import("../termio.zig"); const termio = @import("../termio.zig");
@ -65,6 +66,10 @@ impl: *termio.Impl,
mailbox: *Mailbox, mailbox: *Mailbox,
flags: packed struct { flags: packed struct {
/// This is set to true only when an abnormal exit is detected. It
/// tells our mailbox system to drain and ignore all messages.
drain: bool = false,
/// True if linefeed mode is enabled. This is duplicated here so that the /// True if linefeed mode is enabled. This is duplicated here so that the
/// write thread doesn't need to grab a lock to check this on every write. /// write thread doesn't need to grab a lock to check this on every write.
linefeed_mode: bool = false, linefeed_mode: bool = false,
@ -133,24 +138,98 @@ pub fn deinit(self: *Thread) void {
pub fn threadMain(self: *Thread) void { pub fn threadMain(self: *Thread) void {
// Call child function so we can use errors... // Call child function so we can use errors...
self.threadMain_() catch |err| { self.threadMain_() catch |err| {
// In the future, we should expose this on the thread struct.
log.warn("error in io thread err={}", .{err}); log.warn("error in io thread err={}", .{err});
// Use an arena to simplify memory management below
var arena = ArenaAllocator.init(self.alloc);
defer arena.deinit();
const alloc = arena.allocator();
// If there is an error, we replace our terminal screen with
// the error message. It might be better in the future to send
// the error to the surface thread and let the apprt deal with it
// in some way but this works for now. Without this, the user would
// just see a blank terminal window.
self.impl.renderer_state.mutex.lock();
defer self.impl.renderer_state.mutex.unlock();
const t = self.impl.renderer_state.terminal;
// Hide the cursor
t.modes.set(.cursor_visible, false);
// This is weird but just ensures that no matter what our underlying
// implementation we have the errors below. For example, Windows doesn't
// have "OpenptyFailed".
const Err = @TypeOf(err) || error{
OpenptyFailed,
};
switch (@as(Err, @errorCast(err))) {
error.OpenptyFailed => {
const str =
\\Your system cannot allocate any more pty devices.
\\
\\Ghostty requires a pty device to launch a new terminal.
\\This error is usually due to having too many terminal
\\windows open or having another program that is using too
\\many pty devices.
\\
\\Please free up some pty devices and try again.
;
t.eraseDisplay(alloc, .complete, false);
t.printString(str) catch {};
},
else => {
const str = std.fmt.allocPrint(
alloc,
\\error starting IO thread: {}
\\
\\The underlying shell or command was unable to be started.
\\This error is usually due to exhausting a system resource.
\\If this looks like a bug, please report it.
\\
\\This terminal is non-functional. Please close it and try again.
,
.{err},
) catch
\\Out of memory. This terminal is non-functional. Please close it and try again.
;
t.eraseDisplay(alloc, .complete, false);
t.printString(str) catch {};
},
}
}; };
// If our loop is not stopped, then we need to keep running so that
// messages are drained and we can wait for the surface to send a stop
// message.
if (!self.loop.flags.stopped) {
log.warn("abrupt io thread exit detected, starting xev to drain mailbox", .{});
defer log.debug("io thread fully exiting after abnormal failure", .{});
self.flags.drain = true;
self.loop.run(.until_done) catch |err| {
log.err("failed to start xev loop for draining err={}", .{err});
};
}
} }
fn threadMain_(self: *Thread) !void { fn threadMain_(self: *Thread) !void {
defer log.debug("IO thread exited", .{}); defer log.debug("IO thread exited", .{});
// Start the async handlers. We start these first so that they're
// registered even if anything below fails so we can drain the mailbox.
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
// Run our thread start/end callbacks. This allows the implementation // Run our thread start/end callbacks. This allows the implementation
// to hook into the event loop as needed. // to hook into the event loop as needed.
var data = try self.impl.threadEnter(self); var data = try self.impl.threadEnter(self);
defer data.deinit(); defer data.deinit();
defer self.impl.threadExit(data); defer self.impl.threadExit(data);
// Start the async handlers
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
self.stop.wait(&self.loop, &self.stop_c, Thread, self, stopCallback);
// Run // Run
log.debug("starting IO thread", .{}); log.debug("starting IO thread", .{});
defer log.debug("starting IO thread shutdown", .{}); defer log.debug("starting IO thread shutdown", .{});
@ -159,6 +238,12 @@ fn threadMain_(self: *Thread) !void {
/// Drain the mailbox, handling all the messages in our terminal implementation. /// Drain the mailbox, handling all the messages in our terminal implementation.
fn drainMailbox(self: *Thread) !void { fn drainMailbox(self: *Thread) !void {
// If we're draining, we just drain the mailbox and return.
if (self.flags.drain) {
while (self.mailbox.pop()) |_| {}
return;
}
// This holds the mailbox lock for the duration of the drain. The // This holds the mailbox lock for the duration of the drain. The
// expectation is that all our message handlers will be non-blocking // expectation is that all our message handlers will be non-blocking
// ENOUGH to not mess up throughput on producers. // ENOUGH to not mess up throughput on producers.