Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac779e2435 | ||
|
|
ac3559a102 | ||
|
|
77a0228d6e | ||
|
|
9397edf463 | ||
|
|
3e2f716d07 | ||
|
|
a5e3a2f817 |
+27
-6
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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[]);
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user