wdye: optionally produce asciicast v2 logs
Alpine 3.20 Success
OpenBSD 7.5 Success

I've been fairly disappointed with asciinema,
but it's slightly better than nothing.
This commit is contained in:
2025-01-06 17:01:14 +01:00
parent 914e743dc4
commit 6c47e384f5
2 changed files with 97 additions and 1 deletions
+6
View File
@@ -126,6 +126,12 @@ Example
rot13:send "Hello\r"
expect(rot13:exact {"Uryyb\r"})
Environment
-----------
*WDYE_LOGGING*::
When this environment variable is present, *wdye* produces asciicast v2
files for every spawned program, in the current working directory.
Reporting bugs
--------------
Use https://git.janouch.name/p/liberty to report bugs, request features,
+91 -1
View File
@@ -222,6 +222,45 @@ pty_fork (int *ptrfdm, char **slave_name,
return pid;
}
// --- JSON --------------------------------------------------------------------
static void
write_json_string (FILE *output, const char *s, size_t len)
{
fputc ('"', output);
for (const char *last = s, *end = s + len; s != end; last = s)
{
// Here is where you realize the asciicast format is retarded for using
// JSON at all. (Consider multibyte characters at read() boundaries.)
int32_t codepoint = utf8_decode (&s, end - s);
if (codepoint < 0)
{
s++;
fprintf (output, "\\uFFFD");
continue;
}
switch (codepoint)
{
break; case '"': fprintf (output, "\\\"");
break; case '\\': fprintf (output, "\\\\");
break; case '\b': fprintf (output, "\\b");
break; case '\f': fprintf (output, "\\f");
break; case '\n': fprintf (output, "\\n");
break; case '\r': fprintf (output, "\\r");
break; case '\t': fprintf (output, "\\t");
break; default:
if (!utf8_validate_cp (codepoint))
fprintf (output, "\\uFFFD");
else if (codepoint < 32)
fprintf (output, "\\u%04X", codepoint);
else
fwrite (last, 1, s - last, output);
}
}
fputc ('"', output);
}
// --- Global state ------------------------------------------------------------
static struct
@@ -435,6 +474,9 @@ struct process
int ref_term; ///< Terminal information
struct str buffer; ///< Terminal input buffer
int status; ///< Process status iff pid is -1
int64_t start; ///< Start timestamp (Unix msec)
FILE *asciicast; ///< asciicast script dump
};
static struct process *
@@ -462,6 +504,8 @@ xlua_process_gc (lua_State *L)
kill (-self->pid, SIGKILL);
luaL_unref (L, LUA_REGISTRYINDEX, self->ref_term);
str_free (&self->buffer);
if (self->asciicast)
fclose (self->asciicast);
return 0;
}
@@ -500,6 +544,14 @@ xlua_process_send (lua_State *L)
return luaL_error (L, "write failed: %s", strerror (errno));
else if (written != len)
return luaL_error (L, "write failed: %s", "short write");
if (self->asciicast)
{
double timestamp = (clock_msec () - self->start) / 1000.;
fprintf (self->asciicast, "[%f, \"i\", ", timestamp);
write_json_string (self->asciicast, arg, len);
fprintf (self->asciicast, "]\n");
}
}
lua_pushvalue (L, 1);
return 1;
@@ -667,6 +719,14 @@ process_feed (struct process *self)
return false;
}
if (self->asciicast)
{
double timestamp = (clock_msec () - self->start) / 1000.;
fprintf (self->asciicast, "[%f, \"o\", ", timestamp);
write_json_string (self->asciicast, buf, n);
fprintf (self->asciicast, "]\n");
}
// TODO(p): Add match_max processing, limiting the buffer size.
str_append_data (&self->buffer, buf, n);
return n > 0;
@@ -888,9 +948,17 @@ spawn_protected (lua_State *L)
}
process->ref_term = luaL_ref (L, LUA_REGISTRYINDEX);
struct winsize ws = { .ws_row = 24, .ws_col = 80 };
if ((entry = str_map_find (&ctx->term, "lines"))
&& entry->kind == TERMINFO_NUMERIC)
ws.ws_row = entry->numeric;
if ((entry = str_map_find (&ctx->term, "columns"))
&& entry->kind == TERMINFO_NUMERIC)
ws.ws_col = entry->numeric;
// Step 5: Spawn the process, which gets a new process group.
process->pid =
pty_fork (&process->terminal_fd, NULL, NULL, NULL, &ctx->error);
pty_fork (&process->terminal_fd, NULL, NULL, &ws, &ctx->error);
if (process->pid < 0)
{
return luaL_error (L, "failed to spawn %s: %s",
@@ -905,6 +973,28 @@ spawn_protected (lua_State *L)
_exit (EXIT_FAILURE);
}
// Step 6: Create a log file.
if (getenv ("WDYE_LOGGING"))
{
const char *name = ctx->argv.vector[0];
const char *last_slash = strrchr (name, '/');
if (last_slash)
name = last_slash + 1;
char *path = xstrdup_printf ("%s-%s.%d.cast",
PROGRAM_NAME, name, (int) process->pid);
if (!(process->asciicast = fopen (path, "w")))
print_warning ("%s: %s", path, strerror (errno));
free (path);
}
process->start = clock_msec ();
if (process->asciicast)
{
fprintf (process->asciicast, "{\"version\": 2, "
"\"width\": %u, \"height\": %u, \"env\": {\"TERM\": \"%s\"}}\n",
ws.ws_col, ws.ws_row, term);
}
set_cloexec (process->terminal_fd);
return 1;
}