Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/vmsdcl/dcl_cmd_set.c
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,12 @@ static int cmd_set_control(struct dcl_command *cmd)
if (strcasecmp(tok, "Y") == 0) {
ctx->ctrl_y_enabled = enable;
}
/* T = Ctrl-T (broadcast) — track but not implemented beyond flag */
/* T = Ctrl-T reflexive status line. The DCL input layer's Ctrl-T
* (0x14) handler (dcl_main.c) gates the reflexive one-liner on this
* flag; SET NOCONTROL=T disables it. Default off (VMS convention). */
else if (strcasecmp(tok, "T") == 0) {
ctx->ctrl_t_enabled = enable;
}
tok = strtok_r(NULL, ",", &saveptr);
}

Expand Down
76 changes: 76 additions & 0 deletions src/vmsdcl/dcl_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,79 @@ static void ctrl_c_handler(int sig)
}
}

/*
* dcl_emit_ctrl_t_status - the reflexive Ctrl/T handler.
*
* VMS prints a one-line process status when the user presses Ctrl/T, if
* Ctrl/T is enabled (SET CONTROL=T; disabled by default). This is the
* handler the input layer invokes on a Ctrl/T (0x14) keystroke.
*
* The whole line is sourced from the executive's $GETJPI row --
* vms_kif_getjpi_self(), the SAME reader SHOW PROCESS uses -- and rendered
* by dcl_format_ctrl_t_status() (dcl_terminal.c), which fabricates nothing:
* see its citation and per-field sourcing. If the executive is absent (no
* /dev/vms) $GETJPI fails and we emit NOTHING rather than a faked line
* (INV-6 / CLAUDE.md Rule 9).
*
* DEFERRED GAP (vms-0d75 follow-up): the image-name field is empty because
* OVMX does not yet source JPI$_IMAGNAME for a running image, and the "IO="
* token is omitted because JPI$_DIRIO/BUFIO carry no faithful OVMX source
* (their fields_valid bits are never set). Both are honest omissions, not
* fabrications; filed as a follow-up rather than filled with plausible
* numbers.
*
* Returns 1 if a status line was emitted, 0 otherwise (disabled or no
* executive) so the caller knows whether the display needs a refresh.
*/
static int dcl_emit_ctrl_t_status(void)
{
struct dcl_context *ctx = dcl_get_context();
if (!ctx->ctrl_t_enabled)
return 0; /* SET NOCONTROL=T (VMS default): Ctrl/T is inert */

struct vms_procinfo info;
memset(&info, 0, sizeof(info));
if (!(vms_kif_getjpi_self(&info) & 1))
return 0; /* no executive -> no line, never a fabricated one */

char node[64];
ovmx_node_name(node, sizeof(node));

char line[256];
/* No image name: none is activated at the DCL prompt (see the gap note
* above). JPI$_IMAGNAME would be sourced here once OVMX tracks it. */
if (dcl_format_ctrl_t_status(&info, node, "", time(NULL),
line, sizeof(line)) & 1) {
fputs("\r\n", stdout);
fputs(line, stdout);
fputs("\r\n", stdout);
fflush(stdout);
return 1;
}
return 0;
}

#ifdef HAVE_READLINE
/*
* Readline binding for Ctrl/T (0x14). Readline's emacs default maps 0x14 to
* transpose-chars; this rebind gives it VMS Ctrl/T semantics instead. The
* status line is emitted between the current input line and a fresh redraw
* of it, so the reflexive line "momentarily interrupts" without disturbing
* what the user has typed (OpenVMS User's Manual). When Ctrl/T is disabled
* the keystroke is simply swallowed.
*/
static int dcl_ctrl_t_rl_handler(int count, int key)
{
(void)count;
(void)key;
if (dcl_emit_ctrl_t_status()) {
rl_on_new_line();
rl_forced_update_display();
}
return 0;
}
#endif

/*
* Terminal configuration for VMS signal/EOF model.
* - VEOF = 26 (Ctrl+Z is EOF, not Ctrl+D)
Expand Down Expand Up @@ -594,6 +667,9 @@ int main(int argc, char *argv[])
using_history();
/* Bind Ctrl-B to previous-history (VMS-style recall key) */
rl_bind_key(2, rl_get_previous_history);
/* Bind Ctrl-T (0x14) to the reflexive status-line handler, replacing
* readline's default transpose-chars. Gated on SET CONTROL=T. */
rl_bind_key(20, dcl_ctrl_t_rl_handler);
#endif
}

Expand Down
120 changes: 120 additions & 0 deletions src/vmsdcl/dcl_terminal.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@
#include <errno.h>
#include <termios.h>
#include <ctype.h>
#include <time.h>
#include <inttypes.h>

#include "dcl/terminal.h"
#include "ssdef.h"
/* struct vms_procinfo + VMS_PI_V_* valid-bit mask (the executive's $GETJPI
* contract) -- src/libvmssys/vms_kif.h pulls src/kernel/vms_ioctl.h. */
#include "vms_kif.h"

/* Path to the shared terminal device table */
#include "ovmx_layout.h"
Expand Down Expand Up @@ -258,3 +264,117 @@ void vms_term_deallocate(const char *device_name)
fclose(fp);
}

/*
* dcl_format_ctrl_t_status - render the reflexive Ctrl/T status line.
*
* FORMAT (CLEAN-ROOM, Rule 8). The one-line layout and every field are from
* PUBLIC OpenVMS documentation -- the OpenVMS User's Manual, "Interrupting
* Command Execution" / "Ctrl/T", which documents:
*
* GREEN::MCCARTHY 13:45:02 EVE CPU=00:00:03.33 PF=778 IO=295 MEM=315
*
* and defines the fields as: node and user name; current time of day; the
* name of the image (program) executing; charged CPU time in
* hours:minutes:seconds.hundredths; the accumulated page-fault count; the
* level of I/O activity; and memory usage listed in CPU-specific pages.
* (No VSI/HPE binary was disassembled; format taken from the manual text.)
*
* DATA SOURCE (INV-6 / Rule 9 / Rule 11). Every measured field is READ from
* the executive's $GETJPI row (struct vms_procinfo) the caller already
* fetched with vms_kif_getjpi_self() -- the SAME source SHOW PROCESS reads
* (src/vmsdcl/dcl_cmd_show.c). This function fabricates NOTHING: a field
* whose fields_valid bit is CLEAR is OMITTED, never printed as a plausible
* zero.
*
* node caller-supplied (ovmx_node_name() -> SCSNODE, the VMS node
* identity, NOT the Linux hostname -- INV-4).
* user info->username (executive-stamped identity, "" if none).
* time now, formatted as local HH:MM:SS.
* image caller-supplied JPI$_IMAGNAME. At the DCL prompt no image is
* activated, so this is empty and the field collapses -- which is
* what VMS shows there (the field names the executing image, and
* none is). OVMX does not yet source JPI$_IMAGNAME while an image
* runs; see the gap note in dcl_main.c's Ctrl-T handler.
* CPU info->cputim (JPI$_CPUTIM, 10ms units) if VMS_PI_V_CPUTIM.
* PF info->pageflts (JPI$_PAGEFLTS) if VMS_PI_V_PAGEFLTS.
* IO JPI$_DIRIO+JPI$_BUFIO. OVMX's executive carries these
* STRUCTURAL, valid bit CLEAR (no faithful direct/buffered I/O
* split exists -- src/kernel/vms_ioctl.h), so the field is OMITTED
* rather than fabricated (INV-6). Sourcing it is deferred work.
* MEM info->pages (JPI$_PPGCNT, resident pages) if VMS_PI_V_PAGES.
*
* Returns SS$_NORMAL on success, SS$_BADPARAM on a bad argument. The line is
* written WITHOUT a trailing newline; the caller frames it.
*/
uint32_t dcl_format_ctrl_t_status(const struct vms_procinfo *info,
const char *node,
const char *image,
time_t now,
char *out, size_t outlen)
{
if (!info || !out || outlen == 0)
return SS$_BADPARAM;

const char *nodestr = (node && node[0]) ? node : "";
const char *userstr = info->username; /* executive identity; may be "" */

struct tm tmv;
localtime_r(&now, &tmv);

/* Leading identity + time + (optional) image, VMS spacing. */
int n = snprintf(out, outlen, "%s::%s %02d:%02d:%02d ",
nodestr, userstr,
tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
if (n < 0 || (size_t)n >= outlen)
return SS$_BADPARAM;
size_t len = (size_t)n;

if (image && image[0]) {
n = snprintf(out + len, outlen - len, "%s ", image);
if (n < 0 || (size_t)n >= outlen - len)
return SS$_BADPARAM;
len += (size_t)n;
}

/* CPU=hh:mm:ss.cc -- JPI$_CPUTIM is in 10ms units (centiseconds). */
if (info->fields_valid & VMS_PI_V_CPUTIM) {
unsigned long cs = info->cputim;
unsigned long total_sec = cs / 100UL;
unsigned long cc = cs % 100UL;
unsigned long hh = total_sec / 3600UL;
unsigned long mm = (total_sec % 3600UL) / 60UL;
unsigned long ss = total_sec % 60UL;
n = snprintf(out + len, outlen - len,
"CPU=%02lu:%02lu:%02lu.%02lu", hh, mm, ss, cc);
if (n < 0 || (size_t)n >= outlen - len)
return SS$_BADPARAM;
len += (size_t)n;
}

if (info->fields_valid & VMS_PI_V_PAGEFLTS) {
n = snprintf(out + len, outlen - len, " PF=%" PRIu32, info->pageflts);
if (n < 0 || (size_t)n >= outlen - len)
return SS$_BADPARAM;
len += (size_t)n;
}

/* IO deliberately omitted: JPI$_DIRIO/BUFIO are unsourced in OVMX
* (valid bit never set). Never fabricate it (INV-6). */
if (info->fields_valid & (VMS_PI_V_DIRIO | VMS_PI_V_BUFIO)) {
n = snprintf(out + len, outlen - len, " IO=%" PRIu32,
info->dirio + info->bufio);
if (n < 0 || (size_t)n >= outlen - len)
return SS$_BADPARAM;
len += (size_t)n;
}

if (info->fields_valid & VMS_PI_V_PAGES) {
n = snprintf(out + len, outlen - len, " MEM=%" PRIu32, info->pages);
if (n < 0 || (size_t)n >= outlen - len)
return SS$_BADPARAM;
len += (size_t)n;
}

return SS$_NORMAL;
}

5 changes: 5 additions & 0 deletions src/vmsdcl/include/dcl/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ struct dcl_context {
int ctrl_y_enabled;
pid_t interrupted_pid; /* PID of Ctrl-Y stopped child (0 = none) */

/* Control-T handling (SET CONTROL=T / SET NOCONTROL=T). VMS default is
* DISABLED (OpenVMS User's Manual, "Interrupting Command Execution":
* "Ctrl/T is disabled by default"), so 0 == off until SET CONTROL=T. */
int ctrl_t_enabled;

/* User info */
char username[64];
char process_name[16];
Expand Down
18 changes: 18 additions & 0 deletions src/vmsdcl/include/dcl/terminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,22 @@ struct terminal_device {
*/
void vms_term_deallocate(const char *device_name);

/*
* dcl_format_ctrl_t_status - render the reflexive Ctrl/T status line into
* `out` from an executive $GETJPI row. Pure function (no I/O, no executive
* call): the caller supplies the vms_procinfo it already read with
* vms_kif_getjpi_self(), the VMS node name, the executing image name (""
* when none), and the wall-clock time. See the definition in
* dcl_terminal.c for the format citation and per-field data sourcing.
* Returns SS$_NORMAL on success. `info` is const void* so this header
* does not have to pull the kernel ioctl structures.
*/
#include <time.h>
struct vms_procinfo;
uint32_t dcl_format_ctrl_t_status(const struct vms_procinfo *info,
const char *node,
const char *image,
time_t now,
char *out, size_t outlen);

#endif /* __DCL_TERMINAL_H */
23 changes: 23 additions & 0 deletions tests/libvms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,26 @@ target_include_directories(test_libvms_conformance_constants PRIVATE
)
add_test(NAME test_libvms_conformance_constants
COMMAND test_libvms_conformance_constants)

# vms-0d75 -- reflexive Ctrl/T status line. Compiles the PRODUCTION renderer
# (src/vmsdcl/dcl_terminal.c) directly and drives dcl_format_ctrl_t_status()
# against a struct vms_procinfo -- the executive's real $GETJPI contract --
# so it proves the format and real-field consumption at the true data
# boundary, no monkeypatch. dcl_terminal.c needs the DCL, libvms and
# libvmssys (vms_kif.h -> vms_ioctl.h) include dirs; the vms lib supplies its
# incidental vms_kif_procscan() reference. _DEFAULT_SOURCE for localtime_r.
add_executable(test_libvms_ctrl_t_status
${CMAKE_CURRENT_SOURCE_DIR}/test_ctrl_t_status.c
${CMAKE_SOURCE_DIR}/src/vmsdcl/dcl_terminal.c)
target_include_directories(test_libvms_ctrl_t_status PRIVATE
${CMAKE_SOURCE_DIR}/src/vmsdcl/include
${VMS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/src/libvmssys
)
target_compile_definitions(test_libvms_ctrl_t_status PRIVATE
_POSIX_C_SOURCE=200809L
_DEFAULT_SOURCE
)
target_link_libraries(test_libvms_ctrl_t_status PRIVATE vms pthread m)
add_test(NAME test_libvms_ctrl_t_status
COMMAND test_libvms_ctrl_t_status)
Loading
Loading