Compare commits

...
Author SHA1 Message Date
f4alt ac779e2435 add subprocess tests 2026-08-07 11:05:26 -04:00
f4alt ac3559a102 consistent setup
make sure containers are consistently setup (form container -> ptbl ins
-> create handlers) to ensure mid-flight terminates would see a fully
formed entry
2026-08-07 11:05:26 -04:00
f4alt 77a0228d6e harden subprocess_terminate abort cleanup 2026-08-07 11:05:26 -04:00
f4alt 9397edf463 wire in the interrupt handler
on <Escape>, <Ctrl-C> we want to fire our interrupt handler. All the
signaling is done through our self-rolled service. The actually handling
is only done through the mged_heartbeat (for now, this ONLY handles all
running subprocesses via ged_subprocesses_terminate).
2026-07-30 14:38:03 -04:00
f4alt 3e2f716d07 rename log_drain to a more general "heartbeat" 2026-07-30 14:33:43 -04:00
f4alt a5e3a2f817 begin wiring in a ged interrupt path 2026-07-30 14:33:43 -04:00
21 changed files with 972 additions and 57 deletions
+27 -6
View File
@@ -62,12 +62,13 @@
/* Sequence starts after BRLCAD_ERROR, to be compatible with
* BU return codes */
#define GED_HELP 0x0002 /**< invalid specification, result contains usage */
#define GED_MORE 0x0004 /**< incomplete specification, can specify again interactively */
#define GED_QUIET 0x0008 /**< don't set or modify the result string */
#define GED_UNKNOWN 0x0010 /**< argv[0] was not a known command */
#define GED_EXIT 0x0020 /**< command is requesting a clean application shutdown */
#define GED_OVERRIDE 0x0040 /**< used to indicate settings have been overridden */
#define GED_HELP 0x0002 /**< invalid specification, result contains usage */
#define GED_MORE 0x0004 /**< incomplete specification, can specify again interactively */
#define GED_QUIET 0x0008 /**< don't set or modify the result string */
#define GED_UNKNOWN 0x0010 /**< argv[0] was not a known command */
#define GED_EXIT 0x0020 /**< command is requesting a clean application shutdown */
#define GED_OVERRIDE 0x0040 /**< used to indicate settings have been overridden */
#define GED_INTERRUPTED 0x0080 /**< command stopped early at user request */
/* Check the internal GED magic */
#define GED_CK_MAGIC(_p) BU_CKMAG(_p->i, GED_MAGIC, "ged")
@@ -397,6 +398,26 @@ GED_EXPORT extern void ged_close(struct ged *gedp);
*/
GED_EXPORT extern void ged_subprocesses_terminate(struct ged *gedp);
/* "interrupt" API
* An interrupt source calls ged_interrupt_request() to ask the currently
* running command to stop (request is async-signal-safe and may be called
* from a signal handler).
*
* Cancellation is cooperative: the flag is ONLY observed by commands that
* poll it (via ged_interrupt_pending() or the GED_CHECK_INTERRUPT() macro);
* untaught commands run to completion - this API will not forcibly kill a
* thread or process.
*
* ged_exec() should handle the flag clears and will & GED_INTERRUPTED
* into the result code (read via ged_results_ret(gedp->ged_results))
*/
GED_EXPORT extern void ged_interrupt_request(struct ged *gedp);
GED_EXPORT extern int ged_interrupt_pending(struct ged *gedp);
GED_EXPORT extern void ged_interrupt_clear(struct ged *gedp);
/* convenience macro for checking interrupt requested. if set -> goto label */
#define GED_CHECK_INTERRUPT(_gedp, _label) \
do { if (ged_interrupt_pending(_gedp)) goto _label; } while (0)
/**
* make sure there is a command name given
+31
View File
@@ -32,6 +32,27 @@
extern "C" void libged_init(void);
extern "C" void
ged_interrupt_request(struct ged *gedp)
{
/* must stay async-signal-safe: nothing but a store */
if (gedp && gedp->i)
gedp->i->interrupt_requested = 1;
}
extern "C" int
ged_interrupt_pending(struct ged *gedp)
{
return (gedp && gedp->i) ? (int)gedp->i->interrupt_requested : 0;
}
extern "C" void
ged_interrupt_clear(struct ged *gedp)
{
if (gedp && gedp->i)
gedp->i->interrupt_requested = 0;
}
extern "C" int
ged_exec(struct ged *gedp, int argc, const char *argv[])
{
@@ -82,6 +103,12 @@ ged_exec(struct ged *gedp, int argc, const char *argv[])
GED_CK_MAGIC(gedp);
Ged_Internal *gedip = gedp->i->i;
/* clear interrupt flag BEFORE command is pushed onto the exec stack. otherwise
* nested/recursive commands could do weird things */
if (gedip->exec_stack.empty())
ged_interrupt_clear(gedp);
gedip->exec_stack.push(cmdname);
gedip->cmd_recursion_depth_cnt[cmdname]++;
@@ -135,6 +162,10 @@ ged_exec(struct ged *gedp, int argc, const char *argv[])
gedip->cmd_recursion_depth_cnt[cmdname]--;
gedip->exec_stack.pop();
if (ged_interrupt_pending(gedp))
gedp->ged_results->ret |= GED_INTERRUPTED;
return gedp->ged_results->ret;
}
+10
View File
@@ -81,6 +81,16 @@ ged_subprocesses_terminate(struct ged *gedp)
rrp->aborted = 1;
}
(void)bu_process_wait_n(&rrp->p, 0);
/* Mirror the natural-EOF completion of src/libged/ged_util.cpp -
* notify the drawable subsystem AND fire the caller-supplied end callback
* with aborted so UI/render-done hooks don't stall on interrupt teardown. */
int aborted_flag = 1;
if (gedp->i && gedp->i->ged_gdp && gedp->i->ged_gdp->gd_rtCmdNotify != (void (*)(int))0)
gedp->i->ged_gdp->gd_rtCmdNotify(aborted_flag);
if (rrp->end_clbk)
rrp->end_clbk(0, NULL, &aborted_flag, rrp->end_clbk_data);
bu_ptbl_rm(&gedp->ged_subp, (long *)rrp);
BU_PUT(rrp, struct ged_subprocess);
}
+4
View File
@@ -28,6 +28,7 @@
#include "common.h"
#include <signal.h>
#include <time.h>
#include "bu/avs.h"
@@ -143,6 +144,9 @@ struct ged_impl {
Ged_Internal *i;
struct ged_drawable *ged_gdp;
/* volatile sig_atomic_t so the store is async-signal-safe */
volatile sig_atomic_t interrupt_requested;
};
__BEGIN_DECLS
+3 -4
View File
@@ -2060,6 +2060,9 @@ _ged_run_rt(struct ged *gedp, int cmd_len, const char **gd_rt_cmd, int argc, con
BU_GET(run_rtp, struct ged_subprocess);
run_rtp->magic = GED_CMD_MAGIC;
run_rtp->p = p;
run_rtp->aborted = 0;
run_rtp->gedp = gedp;
run_rtp->stdin_active = 0;
run_rtp->stdout_active = 0;
run_rtp->stderr_active = 0;
@@ -2067,10 +2070,6 @@ _ged_run_rt(struct ged *gedp, int cmd_len, const char **gd_rt_cmd, int argc, con
run_rtp->end_clbk_data = end_clbk_data;
bu_ptbl_ins(&gedp->ged_subp, (long *)run_rtp);
run_rtp->p = p;
run_rtp->aborted = 0;
run_rtp->gedp = gedp;
/* If we know how, set up hooks so the parent process knows to watch for output. */
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(run_rtp, BU_PROCESS_STDERR, _ged_rt_output_handler, (void *)run_rtp);
+3 -3
View File
@@ -307,13 +307,13 @@ ged_rtcheck_core(struct ged *gedp, int argc, const char *argv[])
rtcp->rrtp->stdin_active = 0;
rtcp->rrtp->stdout_active = 0;
rtcp->rrtp->stderr_active = 0;
rtcp->rrtp->end_clbk = NULL;
rtcp->rrtp->end_clbk_data = NULL;
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDOUT, rtcheck_vector_handler, (void *)rtcp);
}
bu_ptbl_ins(&gedp->ged_subp, (long *)rtcp->rrtp);
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDOUT, rtcheck_vector_handler, (void *)rtcp);
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDERR, rtcheck_output_handler, (void *)rtcp);
}
+3 -3
View File
@@ -308,13 +308,13 @@ ged_rtcheck2_core(struct ged *gedp, int argc, const char *argv[])
rtcp->rrtp->stdin_active = 0;
rtcp->rrtp->stdout_active = 0;
rtcp->rrtp->stderr_active = 0;
rtcp->rrtp->end_clbk = NULL;
rtcp->rrtp->end_clbk_data = NULL;
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDOUT, rtcheck_vector_handler, (void *)rtcp);
}
bu_ptbl_ins(&gedp->ged_subp, (long *)rtcp->rrtp);
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDOUT, rtcheck_vector_handler, (void *)rtcp);
(*gedp->ged_create_io_handler)(rtcp->rrtp, BU_PROCESS_STDERR, rtcheck_output_handler, (void *)rtcp);
}
+7 -5
View File
@@ -57,14 +57,16 @@ _ged_run_rtwizard(struct ged *gedp, int cmd_len, const char **gd_rt_cmd)
BU_GET(run_rtp, struct ged_subprocess);
run_rtp->magic = GED_CMD_MAGIC;
run_rtp->stdin_active = 0;
run_rtp->stdout_active = 0;
run_rtp->stderr_active = 0;
bu_ptbl_ins(&gedp->ged_subp, (long *)run_rtp);
run_rtp->p = p;
run_rtp->aborted = 0;
run_rtp->gedp = gedp;
run_rtp->stdin_active = 0;
run_rtp->stdout_active = 0;
run_rtp->stderr_active = 0;
run_rtp->end_clbk = NULL;
run_rtp->end_clbk_data = NULL;
bu_ptbl_ins(&gedp->ged_subp, (long *)run_rtp);
if (gedp->ged_create_io_handler) {
(*gedp->ged_create_io_handler)(run_rtp, BU_PROCESS_STDERR, _ged_rt_output_handler, (void *)run_rtp);
+18
View File
@@ -4,6 +4,24 @@ if(TARGET ged_test_tops)
endif(TARGET ged_test_tops)
brlcad_add_test(NAME ged_test_tops_moss COMMAND ged_test_tops "${CMAKE_CURRENT_SOURCE_DIR}/ged_tests.g")
brlcad_addexec(ged_test_interrupt ged_test_interrupt.cpp libged TEST)
if(TARGET ged_test_interrupt)
add_dependencies(ged_test_interrupt ged_plugins)
endif(TARGET ged_test_interrupt)
brlcad_add_test(NAME ged_test_interrupt COMMAND ged_test_interrupt "${CMAKE_CURRENT_SOURCE_DIR}/ged_tests.g")
if(TARGET ged_test_interrupt)
set_property(TEST ged_test_interrupt APPEND PROPERTY ENVIRONMENT BRLCAD_ROOT=${CMAKE_BINARY_DIR})
endif(TARGET ged_test_interrupt)
brlcad_addexec(ged_test_subp ged_test_subp.cpp libged TEST)
if(TARGET ged_test_subp)
add_dependencies(ged_test_subp ged_plugins)
endif(TARGET ged_test_subp)
brlcad_add_test(NAME ged_test_subp COMMAND ged_test_subp "${CMAKE_CURRENT_SOURCE_DIR}/ged_tests.g")
if(TARGET ged_test_subp)
set_property(TEST ged_test_subp APPEND PROPERTY ENVIRONMENT BRLCAD_ROOT=${CMAKE_BINARY_DIR})
endif(TARGET ged_test_subp)
brlcad_addexec(ged_test_rm ged_test_rm.c "libged;libwdb;librt;libbu" TEST)
if(TARGET ged_test_rm)
add_dependencies(ged_test_rm ged_plugins)
+183
View File
@@ -0,0 +1,183 @@
/* G E D _ T E S T _ I N T E R R U P T . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file ged_test_interrupt.cpp
*
* tests for libged interrupt infrastructure:
*
* 1. Accessor round-trip: request -> pending()==1, clear -> 0.
* 2. Cross-thread visibility: a second thread sets the flag; the main
* thread observes it by polling (models the worker/GUI split).
* 3. Clear-at-entry: a stale flag is cleared when a top-level command
* starts, so its result does NOT carry GED_INTERRUPTED.
* 4. Auto-tag: a command that ends with the flag set has GED_INTERRUPTED
* OR'd into its result by ged_exec. Two variants exercise the PRE
* path and the POST-callback-error path (the latter guards against
* the auto-tag being placed before the callback error assignment,
* which would drop the bit).
*/
#include "common.h"
#include <atomic>
#include <chrono>
#include <thread>
#include <stdio.h>
#include <bu.h>
#include <ged.h>
static int failures = 0;
static void
check(int cond, const char *msg)
{
if (!cond) {
printf("FAIL: %s\n", msg);
failures++;
} else {
printf("ok: %s\n", msg);
}
}
/* Callback that requests an interrupt and reports success (PRE variant). */
static int
clbk_request_ok(int UNUSED(ac), const char **UNUSED(av), void *u1, void *UNUSED(u2))
{
ged_interrupt_request((struct ged *)u1);
return BRLCAD_OK;
}
/* Callback that requests an interrupt and reports failure (POST-error
* variant). A non-OK return makes ged_exec assign the callback's code to
* ged_results->ret; the auto-tag must still survive that overwrite. */
static int
clbk_request_err(int UNUSED(ac), const char **UNUSED(av), void *u1, void *UNUSED(u2))
{
ged_interrupt_request((struct ged *)u1);
return BRLCAD_ERROR;
}
int
main(int ac, char *av[])
{
struct ged *gedp;
const char *ls[2] = {"ls", NULL};
bu_setprogname(av[0]);
if (ac != 2) {
printf("Usage: %s file.g\n", av[0]);
return 1;
}
if (!bu_file_exists(av[1], NULL)) {
printf("ERROR: [%s] does not exist, expecting .g file\n", av[1]);
return 2;
}
gedp = ged_open("db", av[1], 1);
if (!gedp) {
printf("ERROR: unable to open [%s]\n", av[1]);
return 2;
}
/* 1. Accessor round-trip. */
ged_interrupt_clear(gedp);
check(ged_interrupt_pending(gedp) == 0, "clear -> pending()==0");
ged_interrupt_request(gedp);
check(ged_interrupt_pending(gedp) == 1, "request -> pending()==1");
ged_interrupt_clear(gedp);
check(ged_interrupt_pending(gedp) == 0, "clear -> pending()==0 (again)");
/* NULL-safety of the accessors. */
ged_interrupt_request(NULL);
check(ged_interrupt_pending(NULL) == 0, "pending(NULL)==0");
/* 2. Cross-thread visibility. A helper thread raises the flag; the
* main thread observes it by polling, exactly as a worker thread would
* observe a Stop-button/signal-driven request.
* NOTE: This is an intentional cross-thread access of a volatile sig_atomic_t;
* ThreadSanitizer, if enabled, will report it. */
ged_interrupt_clear(gedp);
{
std::thread setter([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
ged_interrupt_request(gedp);
});
int observed = 0;
for (int i = 0; i < 10000 && !observed; i++) {
observed = ged_interrupt_pending(gedp);
if (!observed)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
setter.join();
check(observed == 1, "flag set on another thread is observed by poller");
}
ged_interrupt_clear(gedp);
/* 3. Clear-at-entry: a stale flag must not leak into a fresh top-level
* command's result. Set the flag, run a benign command, and confirm the
* result lacks GED_INTERRUPTED (ged_exec cleared it at depth 0). */
ged_interrupt_request(gedp);
{
int r = ged_exec(gedp, 1, ls);
check(!(r & GED_INTERRUPTED), "clear-at-entry: stale flag not tagged onto next command");
check(ged_interrupt_pending(gedp) == 0, "clear-at-entry: flag is clear after command");
}
/* 4a. Auto-tag, PRE path: a PRE callback raises the flag (after the
* entry clear), the command runs, and ged_exec tags the result. */
ged_interrupt_clear(gedp);
(void)ged_clbk_set(gedp, "ls", BU_CLBK_PRE, &clbk_request_ok, NULL);
{
int r = ged_exec(gedp, 1, ls);
check((r & GED_INTERRUPTED) != 0, "auto-tag: PRE-set flag OR'd into result");
}
(void)ged_clbk_set(gedp, "ls", BU_CLBK_PRE, NULL, NULL); /* unregister */
/* 4b. Auto-tag, POST-error path: a POST callback raises the flag and
* returns non-OK, forcing ged_exec to assign its error code to the
* result. The auto-tag runs as the last mutation of ret, so
* GED_INTERRUPTED must still be present. This fails if the auto-tag is
* placed before the callback-error assignment. */
ged_interrupt_clear(gedp);
(void)ged_clbk_set(gedp, "ls", BU_CLBK_POST, &clbk_request_err, NULL);
{
int r = ged_exec(gedp, 1, ls);
check((r & GED_INTERRUPTED) != 0, "auto-tag: survives POST-callback-error overwrite");
check((r & BRLCAD_ERROR) != 0, "auto-tag: POST error still reported as error");
}
(void)ged_clbk_set(gedp, "ls", BU_CLBK_POST, NULL, NULL); /* unregister */
ged_close(gedp);
printf("\n%s: %d failure(s)\n", av[0], failures);
return (failures == 0) ? 0 : 1;
}
/*
* Local Variables:
* tab-width: 8
* mode: C++
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+211
View File
@@ -0,0 +1,211 @@
/* G E D _ T E S T _ S U B P . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file ged_test_subp.cpp
*
* Tests for Increment 2 subprocess hardening (mged interruptibility):
*
* A. ged_subprocesses_terminate fires end_clbk (with aborted=1) and
* invokes gedp->ged_delete_io_handler on every registered stream,
* restoring the completion contract that the natural-EOF path
* already honored (see src/libged/ged_util.cpp:1786-1795 /
* :1881-1890). This closes the Increment-1 regression where the
* heartbeat interrupt path silently dropped end_clbk.
*
* B. Ordering invariant: a re-entrant io-handler stub called during
* registration observes gedp->ged_subp already populated, proving
* the "init → publish → register" ordering enforced in the four
* spawn helpers (src/libged/ged_util.cpp:_ged_run_rt, rtcheck.c,
* rtcheck2.cpp, rtwizard.c).
*
* Both tests are fully headless — no libtcl, no real rt process. The
* synthetic ged_subprocess in Test A carries p=NULL with aborted
* pre-set so ged_subprocesses_terminate takes the fast path (skips
* bu_pid_terminate; bu_process_wait_n(&NULL,...) is a documented no-op).
* Test B publishes the synthetic entry directly and then invokes the
* gedp->ged_create_io_handler slot to model the ordering hazard.
*/
#include "common.h"
#include <atomic>
#include <stdio.h>
#include <string.h>
#include <bu.h>
#include <ged.h>
static int failures = 0;
static void
check(int cond, const char *msg)
{
if (!cond) {
printf("FAIL: %s\n", msg);
failures++;
} else {
printf("ok: %s\n", msg);
}
}
/* -------- Test A: end_clbk fires on interrupt teardown -------- */
static std::atomic<int> end_clbk_calls{0};
static std::atomic<int> end_clbk_last_aborted{-1};
static std::atomic<int> delete_io_calls{0};
static int
test_end_clbk(int UNUSED(ac), const char **UNUSED(av), void *u1, void *UNUSED(u2))
{
/* u1 is &aborted per the ged_util.cpp natural-EOF contract. */
int aborted = u1 ? *(int *)u1 : -2;
end_clbk_last_aborted.store(aborted);
end_clbk_calls.fetch_add(1);
return BRLCAD_OK;
}
static void
test_delete_io_handler(struct ged_subprocess *UNUSED(p), bu_process_io_t UNUSED(t))
{
delete_io_calls.fetch_add(1);
}
/* -------- Test B: ordering-invariant probe -------- */
static struct ged *g_probe_gedp = NULL;
static std::atomic<int> observed_subp_len{-1};
static void
test_create_io_handler_probe(struct ged_subprocess *UNUSED(p),
bu_process_io_t UNUSED(t),
ged_io_func_t UNUSED(cb),
void *UNUSED(data))
{
/* Mimic the visibility hazard: any observer that inspects ged_subp
* during handler registration must see the entry already published. */
if (g_probe_gedp)
observed_subp_len.store((int)BU_PTBL_LEN(&g_probe_gedp->ged_subp));
}
int
main(int ac, char *av[])
{
struct ged *gedp;
bu_setprogname(av[0]);
if (ac != 2) {
printf("Usage: %s file.g\n", av[0]);
return 1;
}
if (!bu_file_exists(av[1], NULL)) {
printf("ERROR: [%s] does not exist, expecting .g file\n", av[1]);
return 2;
}
gedp = ged_open("db", av[1], 1);
if (!gedp) {
printf("ERROR: unable to open [%s]\n", av[1]);
return 2;
}
/* ---- Test A: end_clbk + delete_io_handler fire on interrupt teardown ---- */
gedp->ged_delete_io_handler = &test_delete_io_handler;
end_clbk_calls.store(0);
end_clbk_last_aborted.store(-1);
delete_io_calls.store(0);
{
struct ged_subprocess *rrp;
BU_GET(rrp, struct ged_subprocess);
rrp->magic = GED_CMD_MAGIC;
rrp->p = NULL; /* synthetic — safe with aborted=1 (see file header) */
rrp->aborted = 1; /* skip bu_pid_terminate path */
rrp->gedp = gedp;
rrp->stdin_active = 0;
rrp->stdout_active = 0;
rrp->stderr_active = 0;
rrp->end_clbk = &test_end_clbk;
rrp->end_clbk_data = NULL;
bu_ptbl_ins(&gedp->ged_subp, (long *)rrp);
}
check((int)BU_PTBL_LEN(&gedp->ged_subp) == 1, "A: ged_subp populated pre-terminate");
ged_subprocesses_terminate(gedp);
check((int)BU_PTBL_LEN(&gedp->ged_subp) == 0, "A: ged_subp emptied by terminate");
check(end_clbk_calls.load() == 1, "A: end_clbk fired exactly once");
check(end_clbk_last_aborted.load() == 1, "A: end_clbk received aborted=1");
check(delete_io_calls.load() == 3, "A: delete_io_handler called for STDIN+STDOUT+STDERR");
gedp->ged_delete_io_handler = NULL;
/* ---- Test B: publish-before-register ordering invariant ---- */
gedp->ged_create_io_handler = &test_create_io_handler_probe;
g_probe_gedp = gedp;
observed_subp_len.store(-1);
{
/* Reproduce the pattern the four spawn helpers now use: fully init the
* ged_subprocess, publish, then register. The probe callback records
* BU_PTBL_LEN(&gedp->ged_subp) at the moment of registration; it MUST
* see the entry already present (>= 1). */
struct ged_subprocess *rrp;
BU_GET(rrp, struct ged_subprocess);
rrp->magic = GED_CMD_MAGIC;
rrp->p = NULL;
rrp->aborted = 1; /* safe for the cleanup call below */
rrp->gedp = gedp;
rrp->stdin_active = 0;
rrp->stdout_active = 0;
rrp->stderr_active = 0;
rrp->end_clbk = NULL;
rrp->end_clbk_data = NULL;
bu_ptbl_ins(&gedp->ged_subp, (long *)rrp);
/* now register — the probe reads back ged_subp length */
(*gedp->ged_create_io_handler)(rrp, BU_PROCESS_STDERR, NULL, NULL);
}
check(observed_subp_len.load() >= 1,
"B: io-handler registration observes published ged_subp entry");
/* Clean up the synthetic entry from Test B without needing terminate's
* full path (we've cleared the delete_io_handler slot already). */
ged_subprocesses_terminate(gedp);
g_probe_gedp = NULL;
gedp->ged_create_io_handler = NULL;
ged_close(gedp);
printf("\n%s: %d failure(s)\n", av[0], failures);
return (failures == 0) ? 0 : 1;
}
/*
* Local Variables:
* tab-width: 8
* mode: C++
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+4
View File
@@ -28,6 +28,7 @@ set(
fbserv.c
grid.c
history.c
interrupt.c
mater.c
menu.c
mged.c
@@ -105,6 +106,8 @@ brlcad_addexec(cad_boundp cad_boundp.c "libbu;${M_LIBRARY}")
brlcad_addexec(cad_parea cad_parea.c libbu)
add_subdirectory(tests)
set(
mged_ignore_files
CMakeLists.txt
@@ -116,6 +119,7 @@ set(
mged.ico
mged.rc
mged_dm.h
mged_interrupt.h
mged_impl.h
mged_wdb.h
sedit.h
+48 -24
View File
@@ -67,6 +67,7 @@
#include "./cmd.h"
#include "./mged_dm.h"
#include "./mged_interrupt.h"
#include "./sedit.h"
extern "C" void mged_finish(struct mged_state *s, int exitcode); /* in mged.c */
@@ -95,7 +96,7 @@ static int gui_output_hook_active = 0;
/* Thread-safe buffer: bu_log output from any thread is accumulated here under
* MGED_SEM_LOG protection. The main thread drains it to the Tcl interp via
* mged_pr_output(), which is called from refresh() and the log_drain timer. */
* mged_pr_output(), which is called from refresh() and the heartbeat timer. */
static struct bu_vls tcl_log_str = BU_VLS_INIT_ZERO;
/* Dedicated semaphore for the log buffer.
@@ -154,13 +155,13 @@ run_ged_async(struct mged_state *s, std::function<int()> func)
s->cmd_running = 1;
/* must suppress SIGINT for the duration of the worker pump. Otherwise,
* SIGINT would longjmp to the outer frame, skipping worker.join(). SIG_IGN
* is just for this window and is be restored after .join()
* TODO/FIXME: this should be replaced with an intentional interrupt flag
* that can cooperate with callers for graceful handling
*/
void (*prev_sigint)(int) = signal(SIGINT, SIG_IGN);
/* Route SIGINT to the cooperative interrupt handler for the duration of
* the worker pump. sig_interrupt only raises the ged interrupt flag (it
* never longjmps), so worker.join() below is always reached; a terminal
* Ctrl-C while an in-process command is parked here sets the flag, and the
* main-thread mged_heartbeat (which also fires inside this pump) reacts.
* This override is just for this window and is restored after .join(). */
void (*prev_sigint)(int) = signal(SIGINT, sig_interrupt);
std::thread worker([&]() {
result.store(func(), std::memory_order_release);
@@ -175,7 +176,7 @@ run_ged_async(struct mged_state *s, std::function<int()> func)
/* Block until woken. The worker's queued wake event is the primary wakeup
* (Tcl services the event queue before it ever blocks, so there is no
* lost-wakeup race); log_drain_callback timer is the liveness backstop and
* lost-wakeup race); the mged_heartbeat timer is the liveness backstop and
* continues to stream intermediate bu_log output to the command prompt */
while (!done.load(std::memory_order_acquire) && !mged_shutting_down(s)) {
Tcl_DoOneEvent(TCL_ALL_EVENTS);
@@ -209,13 +210,19 @@ mged_sem_log_init(void)
MGED_SEM_LOG = bu_semaphore_register("MGED_SEM_LOG");
}
/* Tcl timer callback: drain accumulated log output to the command prompt and
* reschedule itself so that intermediate bu_log output produced during a long
* ged_exec call (running in a worker thread) reaches the user in
* near-real-time.
/* Tcl timer callback: the single main-thread periodic service ("heartbeat").
* It
* (1) drains accumulated log output to the command prompt so intermediate
* bu_log output produced during a long ged_exec call (running in a worker
* thread) reaches the user in near-real-time, and
* (2) reacts to a raised interrupt flag on its rising edge by terminating
* any registered subprocesses. (the reaction never clears the flag - this
* is left to ged_exec's clear-at-entry when the next top-level command
* starts)
* (then) reschedules itself.
*/
static void
log_drain_callback(ClientData clientData)
mged_heartbeat(ClientData clientData)
{
struct mged_state *s = (struct mged_state *)clientData;
MGED_CK_STATE(s);
@@ -223,40 +230,57 @@ log_drain_callback(ClientData clientData)
* quiescence deletes the pending timer token. */
if (mged_shutting_down(s))
return;
mged_pr_output(s->interp);
/* Interrupt reaction (rising-edge, main thread only). */
if (s->gedp) {
int should_log = 0;
int pending = ged_interrupt_pending(s->gedp);
int subp_len = (int)BU_PTBL_LEN(&s->gedp->ged_subp);
if (mged_interrupt_service(s->interrupt_prev, pending, s->cmd_running, subp_len, &should_log)) {
if (should_log)
bu_log("interrupt requested — stopping...\n");
/* Broad, intentional: kill ALL registered subprocesses (rt family,
* facetize children). A no-op when ged_subp is empty. */
ged_subprocesses_terminate(s->gedp);
}
s->interrupt_prev = pending;
}
/* Reschedule: 50 ms gives good responsiveness without unnecessary CPU use. */
s->log_drain_timer = Tcl_CreateTimerHandler(50, log_drain_callback, clientData);
s->heartbeat_timer = Tcl_CreateTimerHandler(50, mged_heartbeat, clientData);
}
/**
* Start the recurring log-drain timer. Call once from mged_setup().
* Start the recurring heartbeat timer. Call once from mged_setup().
*/
void
mged_start_log_drain_timer(struct mged_state *s)
mged_start_heartbeat(struct mged_state *s)
{
MGED_CK_STATE(s);
s->log_drain_timer = Tcl_CreateTimerHandler(50, log_drain_callback, (ClientData)s);
s->heartbeat_timer = Tcl_CreateTimerHandler(50, mged_heartbeat, (ClientData)s);
}
/**
* Cancel the log-drain timer. Call from mged_finish() before teardown.
* Cancel the heartbeat timer. Call from mged_finish() before teardown.
*/
void
mged_stop_log_drain_timer(struct mged_state *s)
mged_stop_heartbeat(struct mged_state *s)
{
MGED_CK_STATE(s);
if (s->log_drain_timer) {
Tcl_DeleteTimerHandler(s->log_drain_timer);
s->log_drain_timer = NULL;
if (s->heartbeat_timer) {
Tcl_DeleteTimerHandler(s->heartbeat_timer);
s->heartbeat_timer = NULL;
}
}
/**
* Release process-lifetime output buffers once no log hook or worker can use
* them. This is separate from mged_stop_log_drain_timer() because shutdown
* them. This is separate from mged_stop_heartbeat() because shutdown
* must first remove the bu_log hook.
*/
void
+56
View File
@@ -0,0 +1,56 @@
/* I N T E R R U P T . C
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file mged/interrupt.c
*
* Pure decision helpers for MGED's cooperative interrupt handling. See
* mged_interrupt.h for the contract. Kept dependency-free so the
* interrupt policy is unit-testable without an MGED/Tcl/GED context.
*/
#include "common.h"
#include "./mged_interrupt.h"
int
mged_interrupt_service(int prev, int pending, int cmd_running, int subp_len, int *should_log)
{
int rising = (pending && !prev);
if (should_log)
*should_log = (rising && (cmd_running || subp_len > 0));
return rising;
}
int
mged_interrupt_should_act(int cmd_running, int subp_len)
{
return (cmd_running || subp_len > 0);
}
/*
* Local Variables:
* tab-width: 8
* mode: C
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+91 -6
View File
@@ -79,6 +79,7 @@
#include "./menu.h"
#include "./mged_dm.h"
#include "./cmd.h"
#include "./mged_interrupt.h"
#include "./f_cmd.h" // for f_opendb
#include "brlcad_ident.h"
@@ -186,7 +187,7 @@ mged_quiesce_tcl(struct mged_state *s)
(void)Tcl_Eval(s->interp, "catch {foreach id [after info] {after cancel $id}}");
}
mged_stop_log_drain_timer(s);
mged_stop_heartbeat(s);
#ifdef HAVE_TK
/* This handler is process-global in Tk and retains s as ClientData. It
@@ -469,6 +470,68 @@ sig3(int UNUSED(sig))
}
void
sig_interrupt(int UNUSED(sig))
{
/* if we have valid state, raise the interrupt */
if (MGED_STATE && MGED_STATE->gedp)
ged_interrupt_request(MGED_STATE->gedp);
/* rearm */
(void)signal(SIGINT, sig_interrupt);
}
/*
* "mged_interrupt" Tcl command, bound to <Escape> in the command window.
*
* Guarded: if something is running (an in-process command, or a registered
* subprocess such as a runaway rt) it raises the interrupt flag and returns
* "1" so the binding swallows the key (the heartbeat then reacts); when idle
* it returns "0" and does nothing, so the key falls through to the widget's
* normal <Escape> handling (VI reset, etc.).
*/
int
cmd_mged_interrupt(ClientData clientData, Tcl_Interp *interp, int UNUSED(argc), const char *UNUSED(argv)[])
{
struct mged_state *s = (struct mged_state *)clientData;
int subp_len;
int act;
if (!s || !s->gedp) {
Tcl_SetResult(interp, (char *)"0", TCL_STATIC);
return TCL_OK;
}
subp_len = (int)BU_PTBL_LEN(&s->gedp->ged_subp);
act = mged_interrupt_should_act(s->cmd_running, subp_len);
if (act)
ged_interrupt_request(s->gedp);
Tcl_SetResult(interp, (char *)(act ? "1" : "0"), TCL_STATIC);
return TCL_OK;
}
#ifdef HAVE_WINDOWS_H
/*
* Windows console Ctrl-C / Ctrl-Break handler. Like sig_interrupt, it only
* raises the interrupt flag and returns TRUE to mark the event handled; the
* heartbeat (which polls every tick) does the rest, so no wake event is
* needed.
*/
static BOOL WINAPI
mged_console_ctrl_handler(DWORD ctrl_type)
{
if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) {
if (MGED_STATE && MGED_STATE->gedp)
ged_interrupt_request(MGED_STATE->gedp);
return TRUE;
}
return FALSE;
}
#endif /* HAVE_WINDOWS_H */
void
new_edit_mats(struct mged_state *s)
{
@@ -1022,7 +1085,9 @@ mged_process_char(struct mged_state *s, char ch)
set_curr_dm(s, curr_cmd_list->cl_tie);
reset_Tty(fileno(stdin)); /* Backwards compatibility */
(void)signal(SIGINT, SIG_IGN);
/* Cooperative interrupt: route Ctrl-C during command
* execution to the flag-setting handler */
(void)signal(SIGINT, sig_interrupt);
if (cmdline(s, &s->input_str_prefix, 1) == CMD_MORE) {
/* Remove newline */
bu_vls_trunc(&s->input_str_prefix,
@@ -1037,7 +1102,9 @@ mged_process_char(struct mged_state *s, char ch)
/* All done; clear all strings. */
bu_vls_trunc(&s->input_str_prefix, 0);
bu_vls_trunc(&s->input_str, 0);
(void)signal(SIGINT, SIG_IGN);
/* Idle disposition for this input path: a terminal Ctrl-C
* sets the flag so the heartbeat can kill a background rt. */
(void)signal(SIGINT, sig_interrupt);
}
set_Cbreak(fileno(stdin)); /* Back to single-character mode */
clr_Echo(fileno(stdin));
@@ -1824,6 +1891,12 @@ stdin_input(ClientData clientData, int UNUSED(mask))
if (curr_cmd_list->cl_tie)
set_curr_dm(s, curr_cmd_list->cl_tie);
/* Cooperative interrupt: route Ctrl-C during command execution to
* the flag-setting handler (mirrors the cbreak path before its own
* cmdline call). This is the classic/pipe/non-tty completion path,
* so without it a terminal Ctrl-C in classic -c mode would be
* ignored after the first command. */
(void)signal(SIGINT, sig_interrupt);
cmd_status = cmdline(s, &s->input_str_prefix, 1);
if (cmd_status == CMD_MORE) {
/* Remove newline */
@@ -1835,7 +1908,9 @@ stdin_input(ClientData clientData, int UNUSED(mask))
} else {
bu_vls_trunc(&s->input_str_prefix, 0);
bu_vls_trunc(&s->input_str, 0);
(void)signal(SIGINT, SIG_IGN);
/* Idle disposition: keep Ctrl-C wired to the flag so the
* heartbeat can still kill a background rt between commands. */
(void)signal(SIGINT, sig_interrupt);
}
pr_prompt(s);
s->input_str_index = 0;
@@ -2539,7 +2614,8 @@ main(int argc, char *argv[])
bu_vls_init(&s->mged_prompt);
s->dpy_string = NULL;
s->cmd_running = 0;
s->log_drain_timer = NULL;
s->heartbeat_timer = NULL;
s->interrupt_prev = 0;
s->shutdown_state = MGED_SHUTDOWN_RUNNING;
s->shutdown_exitcode = 0;
s->stdin_chan = NULL;
@@ -2845,6 +2921,13 @@ main(int argc, char *argv[])
(void)signal(SIGINT, cur_sigint); /* restore */
#endif /* SIGPIPE && SIGINT */
#ifdef HAVE_WINDOWS_H
/* Cooperative interrupt: a Windows console Ctrl-C/Ctrl-Break raises the
* flag (the heartbeat reacts). Registered here at startup; the handler
* reads gedp only at event time, by which point it is open. */
(void)SetConsoleCtrlHandler(mged_console_ctrl_handler, TRUE);
#endif /* HAVE_WINDOWS_H */
#if defined(HAVE_PIPE) && !defined(HAVE_WINDOWS_H)
/* fork()-based background detach: POSIX only. fork() and select() on
* pipe file descriptors are not available on Windows. */
@@ -3303,7 +3386,9 @@ main(int argc, char *argv[])
Tcl_CreateChannelHandler(sd->chan, TCL_READABLE, stdin_input, sd);
#ifdef SIGINT
(void)signal(SIGINT, SIG_IGN);
/* Interactive idle base disposition: a terminal Ctrl-C at an idle GUI
* prompt raises the flag so the heartbeat can kill a background rt. */
(void)signal(SIGINT, sig_interrupt);
#endif
bu_vls_strcpy(&s->mged_prompt, MGED_PROMPT);
+11 -5
View File
@@ -261,10 +261,14 @@ struct mged_state {
/* Asynchronous ged_exec state (cmd.cpp).
* cmd_running is set to 1 while ged_exec runs in a worker thread so
* that re-entrant command dispatch (e.g. from stdin_input) is blocked.
* log_drain_timer is the recurring Tcl timer token used to flush
* accumulated bu_log output to the Tcl command prompt. */
* heartbeat_timer is the recurring Tcl timer token for mged_heartbeat,
* the single main-thread periodic service that flushes accumulated
* bu_log output and reacts to a raised interrupt flag.
* interrupt_prev latches the previous pending value so the heartbeat
* reacts exactly once on the 0->1 rising edge. */
int cmd_running;
Tcl_TimerToken log_drain_timer;
Tcl_TimerToken heartbeat_timer;
int interrupt_prev;
/* Staged shutdown state. Tcl callbacks request shutdown and return; the
* outer MGED event loop performs final teardown after Tcl has unwound. */
@@ -361,6 +365,8 @@ extern void get_attached(struct mged_state *s);
extern void (*cur_sigint)(int); /* Current SIGINT status */
extern void sig2(int);
extern void sig3(int);
extern void sig_interrupt(int);
extern int cmd_mged_interrupt(ClientData clientData, Tcl_Interp *interp, int argc, const char *argv[]);
/* mged.c */
extern void mged_view_callback(struct bview *gvp, void *clientData);
@@ -501,8 +507,8 @@ int mged_get_filename(int, const char **, void *, void*);
int gui_output(void *clientData, void *str);
void mged_pr_output(Tcl_Interp *interp);
void mged_sem_log_init(void);
void mged_start_log_drain_timer(struct mged_state *s);
void mged_stop_log_drain_timer(struct mged_state *s);
void mged_start_heartbeat(struct mged_state *s);
void mged_stop_heartbeat(struct mged_state *s);
void mged_output_cleanup(void);
int mged_ged_exec_async(struct mged_state *s, int argc, const char *argv[]);
+79
View File
@@ -0,0 +1,79 @@
/* M G E D _ I N T E R R U P T . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file mged/mged_interrupt.h
*
* Pure decision helpers for MGED's cooperative interrupt handling
* These functions contain no MGED/Tcl/GED state and no side effects,
* so the interrupt policy can be exercised headlessly.
*
* The rest of the wiring (signal handlers, the mged_interrupt Tcl
* command, the mged_heartbeat reaction) lives in mged.c / cmd.c and
* merely feeds these predicates and acts on their answers.
*/
#ifndef MGED_INTERRUPT_H
#define MGED_INTERRUPT_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* Rising-edge reactor decision for the mged_heartbeat timer.
*
* Given the previous pending value (@p prev) and the current one
* (@p pending), returns 1 exactly on a 0->1 transition (react once),
* and 0 otherwise (including while the flag stays raised). When a
* rising edge is detected and @p should_log is non-NULL, *should_log
* is set to 1 iff there is something visibly running to report
* ("interrupt requested — stopping..."), i.e. an in-process command
* (@p cmd_running) or one or more live subprocesses (@p subp_len > 0).
* On a non-edge, *should_log is set to 0.
*
* The reaction itself (ged_subprocesses_terminate) is unconditional on
* the edge; only the human-facing log line is gated by should_log.
*/
int mged_interrupt_service(int prev, int pending, int cmd_running, int subp_len, int *should_log);
/**
* Guard predicate for the mged_interrupt Tcl command / <Escape>.
*
* Returns 1 iff there is something to interrupt (an in-process command
* is running, or at least one subprocess is registered), meaning the
* caller should raise the interrupt flag and swallow the key; returns 0
* when idle so the key event falls through to its normal binding.
*/
int mged_interrupt_should_act(int cmd_running, int subp_len);
#ifdef __cplusplus
}
#endif
#endif /* MGED_INTERRUPT_H */
/*
* Local Variables:
* tab-width: 8
* mode: C
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+6 -1
View File
@@ -530,6 +530,11 @@ cmd_setup(struct mged_state *s)
(ClientData)ctp, (Tcl_CmdDeleteProc *)NULL);
}
/* the <Escape> binding in the command window calls this to raise the interrupt flag when something is
* running (returns 1) or fall through when idle (returns 0). */
(void)Tcl_CreateCommand(s->interp, "mged_interrupt", (Tcl_CmdProc *)cmd_mged_interrupt,
(ClientData)s, (Tcl_CmdDeleteProc *)NULL);
/* Init mged's Tcl interface to libwdb */
Wdb_Init(s->interp);
@@ -678,7 +683,7 @@ mged_setup(struct mged_state *s)
* any bu_log output accumulated in the thread-safe buffer to the command
* prompt. This enables live streaming of intermediate output during long
* GED commands that run in a worker thread. */
mged_start_log_drain_timer(s);
mged_start_heartbeat(s);
/* Tcl needs to write nulls onto subscripted variable names */
bu_vls_printf(&str, "%s(state)", MGED_DISPLAY_VAR);
+20
View File
@@ -0,0 +1,20 @@
# Headless unit tests for MGED's cooperative interrupt policy (Increment 1).
# The pure decision helpers in ../interrupt.c carry no MGED/Tcl/GED state, so
# they compile directly into a small self-contained test executable.
brlcad_addexec(mged_test_interrupt "mged_test_interrupt.cpp;../interrupt.c" libbu TEST)
target_include_directories(mged_test_interrupt BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..)
brlcad_add_test(NAME mged_test_interrupt COMMAND mged_test_interrupt)
cmakefiles(
CMakeLists.txt
mged_test_interrupt.cpp
)
# Local Variables:
# tab-width: 8
# mode: cmake
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
+146
View File
@@ -0,0 +1,146 @@
/* M G E D _ T E S T _ I N T E R R U P T . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file mged_test_interrupt.cpp
*
* Headless unit tests for MGED's cooperative interrupt policy (Increment 1):
*
* 1. mged_interrupt_service -- the mged_heartbeat rising-edge reactor:
* fires exactly once on a 0->1 transition, not while the flag stays
* raised, re-arms after the flag returns to 0, and gates the human-facing
* "stopping" log on whether anything is actually running.
* 2. mged_interrupt_should_act -- the <Escape>/mged_interrupt guard: acts
* only when an in-process command is running or a subprocess exists.
*
* These functions encode the whole interrupt decision; the surrounding signal
* handlers and Tcl glue are thin wrappers verified by inspection (async-signal
* safety) and by the GUI manual checklist.
*/
#include "common.h"
#include <stdio.h>
#include "bu.h"
#include "mged_interrupt.h"
static int failures = 0;
static void
check(int cond, const char *msg)
{
if (!cond) {
printf("FAIL: %s\n", msg);
failures++;
} else {
printf("ok: %s\n", msg);
}
}
int
main(int UNUSED(ac), char *av[])
{
bu_setprogname(av[0]);
/* ------------------------------------------------------------------ *
* 1. mged_interrupt_service: rising-edge reactor.
* ------------------------------------------------------------------ */
/* No request pending: never reacts, never logs. */
{
int log = 7; /* sentinel: must be overwritten to 0 */
check(mged_interrupt_service(0, 0, 0, 0, &log) == 0, "service: idle 0->0 does not react");
check(log == 0, "service: idle 0->0 sets should_log=0");
}
/* 0->1 with an in-process command running: react + log. */
{
int log = 0;
check(mged_interrupt_service(0, 1, 1, 0, &log) == 1, "service: 0->1 (cmd running) reacts");
check(log == 1, "service: 0->1 (cmd running) logs");
}
/* 0->1 with a subprocess (rt) but no in-process command: react + log. */
{
int log = 0;
check(mged_interrupt_service(0, 1, 0, 2, &log) == 1, "service: 0->1 (subproc) reacts");
check(log == 1, "service: 0->1 (subproc) logs");
}
/* 0->1 with nothing running (flag raised while idle): still reacts once
* (terminate is a no-op on empty ged_subp) but does NOT log. */
{
int log = 1; /* sentinel: must be overwritten to 0 */
check(mged_interrupt_service(0, 1, 0, 0, &log) == 1, "service: 0->1 (idle) still reacts");
check(log == 0, "service: 0->1 (idle) does not log");
}
/* 1->1: flag stays raised -- must NOT react again (react exactly once). */
{
int log = 9; /* sentinel: must be overwritten to 0 on a non-edge */
check(mged_interrupt_service(1, 1, 1, 3, &log) == 0, "service: 1->1 does not re-react");
check(log == 0, "service: 1->1 sets should_log=0");
}
/* 1->0: flag cleared -- no reaction, re-arms for the next edge. */
{
int log = 5;
check(mged_interrupt_service(1, 0, 0, 0, &log) == 0, "service: 1->0 does not react");
check(log == 0, "service: 1->0 sets should_log=0");
}
/* Full cycle: 0->1 (once) then 1->1 (silent) then re-arm 1->0 then 0->1. */
{
int prev = 0;
int reacts = 0;
int pendings[] = {1, 1, 1, 0, 1}; /* raise, hold, hold, clear, raise */
for (int i = 0; i < 5; i++) {
if (mged_interrupt_service(prev, pendings[i], 1, 0, NULL))
reacts++;
prev = pendings[i];
}
check(reacts == 2, "service: two distinct rising edges over a full cycle react twice");
}
/* should_log NULL is tolerated. */
check(mged_interrupt_service(0, 1, 1, 0, NULL) == 1, "service: NULL should_log tolerated on edge");
/* ------------------------------------------------------------------ *
* 2. mged_interrupt_should_act: <Escape> / mged_interrupt guard.
* ------------------------------------------------------------------ */
check(mged_interrupt_should_act(0, 0) == 0, "should_act: idle + no subproc -> no-op");
check(mged_interrupt_should_act(1, 0) != 0, "should_act: cmd running -> act");
check(mged_interrupt_should_act(0, 1) != 0, "should_act: subproc present -> act");
check(mged_interrupt_should_act(1, 5) != 0, "should_act: both -> act");
printf("\n%s: %d failure(s)\n", av[0], failures);
return (failures == 0) ? 0 : 1;
}
/*
* Local Variables:
* tab-width: 8
* mode: C++
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+11
View File
@@ -1919,6 +1919,17 @@ proc set_text_key_bindings { id } {
global tcl_platform
set w .$id.t
# give the command-window text widget a dedicated MgedInterrupt bindtag,
# ordered before its own bindings, so <Escape> defers to the C-backed
# "mged_interrupt". If it acted, "break" swallows the key so the
# per-mode <Escape> (VI reset, etc.) does not also fire; when idle it
# returns 0 and the key falls through untouched.
bind MgedInterrupt <Escape> { if {[mged_interrupt]} break }
if {[lsearch [bindtags $w] MgedInterrupt] < 0} {
bindtags $w [linsert [bindtags $w] 0 MgedInterrupt]
}
switch $mged_gui($id,edit_style) {
vi {
vi_insert_mode $w