#pragma once /* * confirm.h -- destructive-command confirmation gate: prompts the operator * on stderr before a destructive command runs, offers a session-scoped * toggle to skip that prompt, and treats a Ctrl-C during the prompt the * same as declining it (xce-test-cli-ux spec, task 2.1; Requirements * 9.1-9.4, 9.6-9.9. Requirement 9.5 -- the five-command enumeration and * each cmd_*'s own `--yes` bypass flag -- is task 3.3, NOT this module; * this module is the generic, command-agnostic gate mechanism only). * * THE MECHANISM (see design.md's "Confirmation Gate" component and its * "Destructive Command Confirmation Gate" flowchart): * - confirm_destructive() is the gate itself. It internally checks, in * order: the session-scoped skip toggle (confirm_is_skip_set()), then * whether the top-level session is even interactive * (xcetest_is_interactive(), see below). If either says "don't * prompt", it returns 1 (proceed) immediately WITHOUT touching stdin * or printing anything. Otherwise it prints a yes/no prompt to * stderr and performs one blocking read of the answer from stdin. * - confirm_destructive() deliberately has NO parameter for a * per-command `--yes` bypass flag -- that flag is per-INVOCATION, not * session state, so it cannot be folded into confirm_is_skip_set()'s * persistent toggle without leaking into the next command. Task 3.3's * five call sites (cmd_jobkill, cmd_shutdown, cmd_qctrl, cmd_hostctrl, * cmd_upgradeconfig) are responsible for short-circuiting around * confirm_destructive() entirely when their OWN --yes flag was given, * e.g.: * if (!opts.yes && !confirm_destructive("kill job 1234")) * return NOT_RUN; * (design.md's Implementation Notes describes this same three-way * bypass -- toggle, TTY, --yes -- as one boolean expression each call * site evaluates; confirm_destructive() itself owns the first two, * the call site owns the third because only it knows its own flag.) * * RETURN CONTRACT (confirm_destructive): exactly two outcomes, matching * design.md's Service Interface postcondition -- no third "error" case is * defined for this function. * 1 -- proceed: skip-toggle set, OR non-interactive session, OR the * operator answered affirmatively ("y"/"yes", case-insensitive, * surrounding whitespace ignored). * 0 -- not run: the operator declined (anything else, including a bare * Enter or unrecognized text), OR stdin hit EOF, OR the prompt's * blocking read was interrupted by a real Ctrl-C/SIGINT (EINTR; * Requirement 9.9 -- treated identically to a decline, never * retried). The caller must report this as "not run", distinct * from "failed" (Requirement 9.2/9.9). * * WHY THE PROMPT READ IS A PLAIN fgets(), NOT libedit's readline(): a * y/n answer routed through readline() would pollute the session's * command history and behave inconsistently when stdin isn't a TTY * (design.md's Implementation Notes / research.md). It also would not * reliably surface EINTR the way a bare blocking libc read does. * * WHY EINTR IS RELIABLE HERE: task 1.3 (xcetest_interrupt.h, * xcetest_install_sigint()) replaced test.c's old `signal(SIGINT, * sigbreak)` installation with an explicit sigaction() call with * SA_RESTART cleared, once, at process startup. That is a PRECONDITION * of this module, not something confirm_destructive() re-checks on every * call -- see design.md's Service Interface preconditions. */ /* Prompts the operator (on stderr) to confirm a destructive action * described by `action_description` (a short phrase completing a * sentence like "About to . Continue? [y/N] " -- * e.g. "kill job 1234"), unless the skip toggle is set or the session * isn't interactive, in which case it returns 1 without printing * anything or reading stdin. See the return contract above. */ int confirm_destructive(const char* action_description); /* Sets/gets the session-scoped confirmation-skip toggle (Requirement * 9.6/9.7), in the exact same style as test.c's existing g_continue_on_error * `-e`/`+e` toggle: a plain `static int`, defaulting to 0 (confirmation * required -- NOT skipped) at process start, flipped by `cmd_set` (task * 3.3 wires the `set` command's new branch; this module only owns the * flag itself). While set, confirm_destructive() proceeds without * prompting regardless of TTY state, so a `source`d script (Requirement * 13) can opt in to unattended destructive commands explicitly. */ void confirm_set_skip(int skip); int confirm_is_skip_set(void); /* Read-only accessor over test.c's private `g_interactive` flag (set once * in main() from isatty(STDIN_FILENO), never changed after that) -- * mirrors xcetest_interrupted()'s accessor pattern over test.c's private * `break_out` flag (xcetest_interrupt.h, task 1.3). Declared here, not in * xcetest_interrupt.h, because confirm.c is its only consumer; g_interactive * itself is unrelated to that header's SIGINT/EINTR concern. * * Per design.md's Confirmation Gate invariant, this is deliberately the * TOP-LEVEL session's own stdin, not "is this line currently being read * from a sourced file" -- Requirement 13.7 (a later task) depends on that * distinction: a destructive command inside a `source`d file in an * otherwise-interactive session must still prompt unless the skip toggle * is set. * * Implemented in test.c. test.c itself is never linked into * controller_unit_test (it pulls in libedit/cJSON/libxce/Cap'n Proto/ * OpenSSL and defines its own main() -- see xcetest_interrupt.h's header * comment for the same reasoning applied to xcetest_interrupted()), so * test_confirm.c supplies its own local fake definition, exactly as * test_interruptcall.c already does for xcetest_interrupted(). */ int xcetest_is_interactive(void);