diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index a03326c79..49b28d86f 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -170,6 +170,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "alt_sends_escape", .value = 1039 }, .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, .{ .name = "bracketed_paste", .value = 2004 }, + .{ .name = "synchronized_output", .value = 2026 }, }; test { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 16a90a87e..384029a5d 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -296,6 +296,15 @@ pub fn resize( } } +/// Reset the synchronized output mode. This is usually called by timer +/// expiration from the termio thread. +pub fn resetSynchronizedOutput(self: *Exec) void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.terminal.modes.set(.synchronized_output, false); + self.renderer_wakeup.notify() catch {}; +} + /// Clear the screen. pub fn clearScreen(self: *Exec, history: bool) !void { { @@ -1350,6 +1359,13 @@ const StreamHandler = struct { if (enabled) .@"132_cols" else .@"80_cols", ), + // We need to start a timer to prevent the emulator being hung + // forever. + .synchronized_output => { + if (enabled) self.messageWriter(.{ .start_synchronized_output = {} }); + try self.queueRender(); + }, + .mouse_event_x10 => self.terminal.flags.mouse_event = if (enabled) .x10 else .none, .mouse_event_normal => self.terminal.flags.mouse_event = if (enabled) .normal else .none, .mouse_event_button => self.terminal.flags.mouse_event = if (enabled) .button else .none, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 2c32d459f..6f2a4af3f 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -27,6 +27,10 @@ const Coalesce = struct { resize: ?termio.Message.Resize = null, }; +/// The number of milliseconds before we reset the synchronized output flag +/// if the running program hasn't already. +const sync_reset_ms = 5000; + /// Allocator used for some state alloc: std.mem.Allocator, @@ -49,6 +53,12 @@ coalesce_c: xev.Completion = .{}, coalesce_cancel_c: xev.Completion = .{}, coalesce_data: Coalesce = .{}, +/// This timer is used to reset synchronized output modes so that +/// the terminal doesn't freeze with a bad actor. +sync_reset: xev.Timer, +sync_reset_c: xev.Completion = .{}, +sync_reset_cancel_c: xev.Completion = .{}, + /// The underlying IO implementation. impl: *termio.Impl, @@ -79,6 +89,10 @@ pub fn init( var coalesce_h = try xev.Timer.init(); errdefer coalesce_h.deinit(); + // This timer is used to reset synchronized output modes. + var sync_reset_h = try xev.Timer.init(); + errdefer sync_reset_h.deinit(); + // The mailbox for messaging this thread var mailbox = try Mailbox.create(alloc); errdefer mailbox.destroy(alloc); @@ -89,6 +103,7 @@ pub fn init( .wakeup = wakeup_h, .stop = stop_h, .coalesce = coalesce_h, + .sync_reset = sync_reset_h, .impl = impl, .mailbox = mailbox, }; @@ -98,6 +113,7 @@ pub fn init( /// completes executing; the caller must join prior to this. pub fn deinit(self: *Thread) void { self.coalesce.deinit(); + self.sync_reset.deinit(); self.stop.deinit(); self.wakeup.deinit(); self.loop.deinit(); @@ -158,6 +174,7 @@ fn drainMailbox(self: *Thread) !void { .clear_screen => |v| try self.impl.clearScreen(v.history), .scroll_viewport => |v| try self.impl.scrollViewport(v), .jump_to_prompt => |v| try self.impl.jumpToPrompt(v), + .start_synchronized_output => self.startSynchronizedOutput(), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_stable => |v| try self.impl.queueWrite(v), .write_alloc => |v| { @@ -174,6 +191,18 @@ fn drainMailbox(self: *Thread) !void { } } +fn startSynchronizedOutput(self: *Thread) void { + self.sync_reset.reset( + &self.loop, + &self.sync_reset_c, + &self.sync_reset_cancel_c, + sync_reset_ms, + Thread, + self, + syncResetCallback, + ); +} + fn handleResize(self: *Thread, resize: termio.Message.Resize) void { self.coalesce_data.resize = resize; @@ -193,6 +222,25 @@ fn handleResize(self: *Thread, resize: termio.Message.Resize) void { ); } +fn syncResetCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch |err| switch (err) { + error.Canceled => {}, + else => { + log.warn("error during sync reset callback err={}", .{err}); + return .disarm; + }, + }; + + const self = self_ orelse return .disarm; + self.impl.resetSynchronizedOutput(); + return .disarm; +} + fn coalesceCallback( self_: ?*Thread, _: *xev.Loop, diff --git a/src/termio/message.zig b/src/termio/message.zig index d0fc9f7c3..06e8aae85 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -51,6 +51,11 @@ pub const Message = union(enum) { /// Jump forward/backward n prompts. jump_to_prompt: isize, + /// Send this when a synchronized output mode is started. This will + /// start the timer so that the output mode is disabled after a + /// period of time so that a bad actor can't hang the terminal. + start_synchronized_output: void, + /// Write where the data fits in the union. write_small: WriteReq.Small,