Skip to content

Commit 5c1933b

Browse files
committed
Harden resumable dasHerd sessions
1 parent f185b70 commit 5c1933b

13 files changed

Lines changed: 942 additions & 96 deletions

modules/dasHV/dashv/dashv_boost.das

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ class HvWebSocketClient {
6464
//! Sends a text message to the server.
6565
send(client, text)
6666
}
67+
def send_result(text : string) : int {
68+
//! Sends a text message and returns the native transport result.
69+
return send(client, text)
70+
}
6771
def close : int {
6872
//! Closes the WebSocket connection. Returns 0 on success.
6973
return close(client)

modules/dasTerminal/daslib/terminal.das

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ struct public TerminalBufferSnapshot {
118118
scrollback : array<TerminalCell>
119119
scrollback_rows : int
120120
cursor : TerminalCursor = TerminalCursor()
121+
saved_cursor : TerminalCursor = TerminalCursor()
121122
}
122123

123124
struct public TerminalModes {
@@ -1184,6 +1185,7 @@ def private collect_buffer(buffer : TerminalBuffer const; columns, rows : int;
11841185
}
11851186
snapshot.scrollback_rows = length(buffer.history)
11861187
snapshot.cursor = buffer.cursor
1188+
snapshot.saved_cursor = buffer.saved_cursor
11871189
}
11881190

11891191
def public terminal_snapshot(terminal : Terminal const; var snapshot : TerminalSnapshot) {
@@ -1365,6 +1367,175 @@ def private same_color(left, right : TerminalColor const) : bool {
13651367
left.red == right.red && left.green == right.green && left.blue == right.blue)
13661368
}
13671369

1370+
def private checkpoint_same_style(left, right : TerminalCell const) : bool {
1371+
return (left.attributes == right.attributes &&
1372+
same_color(left.foreground, right.foreground) &&
1373+
same_color(left.background, right.background))
1374+
}
1375+
1376+
def private checkpoint_write_color(var writer : StringBuilderWriter;
1377+
color : TerminalColor const;
1378+
foreground : bool) {
1379+
let base = foreground ? 38 : 48
1380+
if (color.kind == TerminalColorKind.indexed) {
1381+
writer |> write(";{base};5;{color.index}")
1382+
} elif (color.kind == TerminalColorKind.rgb) {
1383+
writer |> write(";{base};2;{color.red};{color.green};{color.blue}")
1384+
}
1385+
}
1386+
1387+
def private checkpoint_write_style(var writer : StringBuilderWriter;
1388+
cell : TerminalCell const) {
1389+
let esc = to_char(27)
1390+
writer |> write("{esc}[0")
1391+
if ((cell.attributes & TERMINAL_ATTR_BOLD) != 0) writer |> write(";1")
1392+
if ((cell.attributes & TERMINAL_ATTR_FAINT) != 0) writer |> write(";2")
1393+
if ((cell.attributes & TERMINAL_ATTR_ITALIC) != 0) writer |> write(";3")
1394+
if ((cell.attributes & TERMINAL_ATTR_UNDERLINE) != 0) writer |> write(";4")
1395+
if ((cell.attributes & TERMINAL_ATTR_BLINK) != 0) writer |> write(";5")
1396+
if ((cell.attributes & TERMINAL_ATTR_INVERSE) != 0) writer |> write(";7")
1397+
if ((cell.attributes & TERMINAL_ATTR_HIDDEN) != 0) writer |> write(";8")
1398+
if ((cell.attributes & TERMINAL_ATTR_STRIKE) != 0) writer |> write(";9")
1399+
checkpoint_write_color(writer, cell.foreground, true)
1400+
checkpoint_write_color(writer, cell.background, false)
1401+
writer |> write("m")
1402+
}
1403+
1404+
def private checkpoint_write_hyperlink(var writer : StringBuilderWriter;
1405+
hyperlink : string) {
1406+
let esc = to_char(27)
1407+
writer |> write("{esc}]8;;{hyperlink}{esc}\\")
1408+
}
1409+
1410+
def private checkpoint_cell_visible(cell : TerminalCell const) : bool {
1411+
let has_text = cell.width != 0 && cell.grapheme != "" && cell.grapheme != " "
1412+
let has_style = (cell.attributes != 0 ||
1413+
cell.foreground.kind != TerminalColorKind.default_color ||
1414+
cell.background.kind != TerminalColorKind.default_color)
1415+
return has_text || has_style || !empty(cell.hyperlink)
1416+
}
1417+
1418+
def private checkpoint_write_row(var writer : StringBuilderWriter;
1419+
row : TerminalRow const;
1420+
var active_style : TerminalCell;
1421+
var active_hyperlink : string) {
1422+
var last_column = -1
1423+
for (column in range(length(row.cells))) {
1424+
if (checkpoint_cell_visible(row.cells[column])) {
1425+
last_column = column
1426+
}
1427+
}
1428+
for (column in range(last_column + 1)) {
1429+
let cell = row.cells[column]
1430+
if (cell.width == 0) continue
1431+
if (!checkpoint_same_style(active_style, cell)) {
1432+
checkpoint_write_style(writer, cell)
1433+
active_style = cell
1434+
}
1435+
if (active_hyperlink != cell.hyperlink) {
1436+
checkpoint_write_hyperlink(writer, cell.hyperlink)
1437+
active_hyperlink = cell.hyperlink
1438+
}
1439+
writer |> write(empty(cell.grapheme) ? " " : cell.grapheme)
1440+
}
1441+
}
1442+
1443+
def private checkpoint_write_buffer(var writer : StringBuilderWriter;
1444+
buffer : TerminalBuffer const;
1445+
include_history : bool;
1446+
var active_style : TerminalCell;
1447+
var active_hyperlink : string) {
1448+
var line = 0
1449+
let line_count = (include_history ? length(buffer.history) : 0) + length(buffer.screen)
1450+
if (include_history) {
1451+
for (row in range(length(buffer.history))) {
1452+
if (line > 0) writer |> write("\r\n")
1453+
checkpoint_write_row(writer, buffer.history[history_index(buffer, row)],
1454+
active_style, active_hyperlink)
1455+
line++
1456+
}
1457+
}
1458+
for (row in range(length(buffer.screen))) {
1459+
if (line > 0) writer |> write("\r\n")
1460+
checkpoint_write_row(writer, buffer.screen[screen_index(buffer, row)],
1461+
active_style, active_hyperlink)
1462+
line++
1463+
}
1464+
assert(line == line_count)
1465+
}
1466+
1467+
def private checkpoint_write_mode(var writer : StringBuilderWriter;
1468+
mode : int; enabled : bool) {
1469+
let esc = to_char(27)
1470+
let suffix = enabled ? "h" : "l"
1471+
writer |> write("{esc}[?{mode}{suffix}")
1472+
}
1473+
1474+
def private checkpoint_write_cursor_state(var writer : StringBuilderWriter;
1475+
cursor : TerminalCursor const) {
1476+
let esc = to_char(27)
1477+
writer |> write("{esc}[{cursor.style}q")
1478+
checkpoint_write_mode(writer, 25, cursor.visible)
1479+
}
1480+
1481+
def private checkpoint_restore_buffer_state(var writer : StringBuilderWriter;
1482+
buffer : TerminalBuffer const;
1483+
var active_style : TerminalCell;
1484+
var active_hyperlink : string) {
1485+
let esc = to_char(27)
1486+
writer |> write("{esc}[{buffer.scroll_top + 1};{buffer.scroll_bottom + 1}r")
1487+
writer |> write("{esc}[{buffer.saved_cursor.row + 1};{buffer.saved_cursor.column + 1}H")
1488+
checkpoint_write_cursor_state(writer, buffer.saved_cursor)
1489+
writer |> write("{esc}[s")
1490+
writer |> write("{esc}[{buffer.cursor.row + 1};{buffer.cursor.column + 1}H")
1491+
checkpoint_write_cursor_state(writer, buffer.cursor)
1492+
checkpoint_write_style(writer, buffer.pen)
1493+
active_style = buffer.pen
1494+
if (active_hyperlink != buffer.pen.hyperlink) {
1495+
checkpoint_write_hyperlink(writer, buffer.pen.hyperlink)
1496+
active_hyperlink = buffer.pen.hyperlink
1497+
}
1498+
}
1499+
1500+
//! Serializes authoritative terminal state so a fresh emulator can resume without replaying TUI history.
1501+
def public terminal_checkpoint_ansi(terminal : Terminal const) : string {
1502+
let esc = to_char(27)
1503+
return build_string() $(var writer) {
1504+
var active_style = TerminalCell()
1505+
var active_hyperlink = ""
1506+
writer |> write("{esc}c{esc}[?7l{esc}[H")
1507+
checkpoint_write_buffer(writer, terminal.normal, true,
1508+
active_style, active_hyperlink)
1509+
checkpoint_restore_buffer_state(writer, terminal.normal,
1510+
active_style, active_hyperlink)
1511+
if (terminal.alternate_active) {
1512+
checkpoint_write_mode(writer, 1049, true)
1513+
active_style = TerminalCell()
1514+
active_hyperlink = ""
1515+
checkpoint_write_mode(writer, 7, false)
1516+
writer |> write("{esc}[H")
1517+
checkpoint_write_buffer(writer, terminal.alternate, false,
1518+
active_style, active_hyperlink)
1519+
checkpoint_restore_buffer_state(writer, terminal.alternate,
1520+
active_style, active_hyperlink)
1521+
}
1522+
checkpoint_write_mode(writer, 1, terminal.modes.application_cursor_keys)
1523+
checkpoint_write_mode(writer, 7, terminal.modes.auto_wrap)
1524+
checkpoint_write_mode(writer, 1000, terminal.modes.mouse_button_reporting)
1525+
checkpoint_write_mode(writer, 1002, terminal.modes.mouse_drag_reporting)
1526+
checkpoint_write_mode(writer, 1003, terminal.modes.mouse_any_reporting)
1527+
checkpoint_write_mode(writer, 1004, terminal.modes.focus_reporting)
1528+
checkpoint_write_mode(writer, 1006, terminal.modes.sgr_mouse_encoding)
1529+
checkpoint_write_mode(writer, 2004, terminal.modes.bracketed_paste)
1530+
let buffer & = terminal.alternate_active ? terminal.alternate : terminal.normal
1531+
checkpoint_write_cursor_state(writer, buffer.cursor)
1532+
if (!empty(terminal.title)) writer |> write("{esc}]2;{terminal.title}{esc}\\")
1533+
if (!empty(terminal.current_directory)) {
1534+
writer |> write("{esc}]7;{terminal.current_directory}{esc}\\")
1535+
}
1536+
}
1537+
}
1538+
13681539
def private batchable_ascii(cell : TerminalCell const) : bool {
13691540
if (cell.width != 1 || cell.attributes != 0 || cell.hyperlink != "" ||
13701541
length(cell.grapheme) != 1) return false
@@ -1541,6 +1712,7 @@ def public terminal_viewport_snapshot(terminal : Terminal const; scroll_offset :
15411712
snapshot.text_run_projection_us = get_time_usec(runs_started)
15421713
snapshot.buffer.scrollback_rows = history_rows
15431714
snapshot.buffer.cursor = buffer.cursor
1715+
snapshot.buffer.saved_cursor = buffer.saved_cursor
15441716
}
15451717

15461718
def public terminal_viewport_snapshot_dispose(var snapshot : TerminalViewportSnapshot) {

modules/dasTerminal/src/pty.cpp

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class ConPtyProcess final : public PtyProcess {
105105
HANDLE input_write_ = nullptr;
106106
HANDLE output_read_ = nullptr;
107107
HANDLE process_ = nullptr;
108+
HANDLE job_ = nullptr;
108109
uint32_t process_id_ = 0;
109110
};
110111

@@ -155,11 +156,12 @@ std::string buildWindowsCommandLine(const std::vector<std::string> & arguments)
155156

156157
ConPtyProcess::~ConPtyProcess() {
157158
closePty();
159+
closeHandle(job_);
158160
closeHandle(process_);
159161
}
160162

161163
bool ConPtyProcess::launch(const PtyProcessOptions & options, std::string & error) {
162-
if (pseudo_console_ || input_write_ || output_read_ || process_) {
164+
if (pseudo_console_ || input_write_ || output_read_ || process_ || job_) {
163165
error = "ConPTY process is already launched";
164166
return false;
165167
}
@@ -248,10 +250,34 @@ bool ConPtyProcess::launch(const PtyProcessOptions & options, std::string & erro
248250
}
249251
std::vector<wchar_t> mutable_command(command.begin(), command.end());
250252
mutable_command.push_back(L'\0');
253+
254+
job_ = CreateJobObjectW(nullptr, nullptr);
255+
if (!job_) {
256+
error = windowsError("CreateJobObjectW");
257+
DeleteProcThreadAttributeList(startup.lpAttributeList);
258+
closeHandle(input_read);
259+
closeHandle(output_write);
260+
closePty();
261+
return false;
262+
}
263+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits = {};
264+
job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
265+
if (!SetInformationJobObject(
266+
job_, JobObjectExtendedLimitInformation, &job_limits,
267+
sizeof(job_limits))) {
268+
error = windowsError("SetInformationJobObject");
269+
DeleteProcThreadAttributeList(startup.lpAttributeList);
270+
closeHandle(input_read);
271+
closeHandle(output_write);
272+
closePty();
273+
closeHandle(job_);
274+
return false;
275+
}
276+
251277
PROCESS_INFORMATION process = {};
252278
const BOOL created = CreateProcessW(
253279
nullptr, mutable_command.data(), nullptr, nullptr, FALSE,
254-
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
280+
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED,
255281
nullptr, directory.empty() ? nullptr : directory.c_str(),
256282
&startup.StartupInfo, &process);
257283
const DWORD create_error = GetLastError();
@@ -263,8 +289,33 @@ bool ConPtyProcess::launch(const PtyProcessOptions & options, std::string & erro
263289
if (!created) {
264290
error = windowsError("CreateProcessW", create_error);
265291
closePty();
292+
closeHandle(job_);
266293
return false;
267294
}
295+
296+
if (!AssignProcessToJobObject(job_, process.hProcess)) {
297+
const DWORD assign_error = GetLastError();
298+
TerminateProcess(process.hProcess, 1);
299+
WaitForSingleObject(process.hProcess, INFINITE);
300+
CloseHandle(process.hThread);
301+
CloseHandle(process.hProcess);
302+
closePty();
303+
closeHandle(job_);
304+
error = windowsError("AssignProcessToJobObject", assign_error);
305+
return false;
306+
}
307+
if (ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
308+
const DWORD resume_error = GetLastError();
309+
TerminateJobObject(job_, 1);
310+
WaitForSingleObject(process.hProcess, INFINITE);
311+
CloseHandle(process.hThread);
312+
CloseHandle(process.hProcess);
313+
closePty();
314+
closeHandle(job_);
315+
error = windowsError("ResumeThread", resume_error);
316+
return false;
317+
}
318+
268319
CloseHandle(process.hThread);
269320
process_ = process.hProcess;
270321
process_id_ = process.dwProcessId;
@@ -368,11 +419,12 @@ bool ConPtyProcess::wait(
368419
}
369420

370421
bool ConPtyProcess::terminate(uint32_t exit_code, std::string & error) {
371-
if (TerminateProcess(process_, exit_code)) return true;
422+
if (job_ && TerminateJobObject(job_, exit_code)) return true;
423+
if (!job_ && TerminateProcess(process_, exit_code)) return true;
372424
const DWORD code = GetLastError();
373425
if (code == ERROR_ACCESS_DENIED && WaitForSingleObject(process_, 0) == WAIT_OBJECT_0)
374426
return true;
375-
error = windowsError("TerminateProcess", code);
427+
error = windowsError(job_ ? "TerminateJobObject" : "TerminateProcess", code);
376428
return false;
377429
}
378430

0 commit comments

Comments
 (0)