[education/rkward] rkward: Partial fix for command interruption mess

Thomas Friedrichsmeier null at kde.org
Sun Sep 14 19:26:16 BST 2025


Git commit 5b37381fbea6b482e1f56e1bc9938f952755d51c by Thomas Friedrichsmeier.
Committed on 11/09/2025 at 14:54.
Pushed by tfry into branch 'master'.

Partial fix for command interruption mess

M  +2    -1    rkward/autotests/core_test.cpp
M  +115  -79   rkward/rbackend/rkrbackend.cpp
M  +6    -7    rkward/rbackend/rkrbackend.h
M  +4    -0    rkward/rbackend/rkrbackendprotocol_backend.cpp
M  +1    -0    rkward/rbackend/rkrbackendprotocol_shared.cpp
M  +1    -0    rkward/rbackend/rkrbackendprotocol_shared.h
M  +5    -2    rkward/rbackend/rkreventloop.cpp
M  +1    -1    rkward/rbackend/rkrinterface.cpp
M  +1    -1    rkward/rbackend/rksignalsupport.cpp

https://invent.kde.org/education/rkward/-/commit/5b37381fbea6b482e1f56e1bc9938f952755d51c

diff --git a/rkward/autotests/core_test.cpp b/rkward/autotests/core_test.cpp
index 1a18131b0..4611b7225 100644
--- a/rkward/autotests/core_test.cpp
+++ b/rkward/autotests/core_test.cpp
@@ -197,6 +197,7 @@ class RKWardCoreTest : public QObject {
 		KAboutData::setApplicationData(about);
 		new RKCommandLineArgs(&about, qApp, true);
 		RK_Debug::RK_Debug_Level = DL_DEBUG;
+		// RK_Debug::RK_Debug_Flags = RBACKEND;
 		testLog(R_EXECUTABLE);
 		RKSessionVars::r_binary = QStringLiteral(R_EXECUTABLE);
 		main_win = new RKWardMainWindow();
@@ -479,7 +480,7 @@ class RKWardCoreTest : public QObject {
 		ScopeHandler sup([]() { RKSettingsModuleWatch::forTestingSuppressOutput(true); }, []() { RKSettingsModuleWatch::forTestingSuppressOutput(false); });
 		// NOTE: Test deliberately toned down until the next attempt to fully fix the issue. Still fails in corner-cases, typically 10-100 iterations are needed
 		//       to trigger. Not considered much of a real-world issue, though.
-		for (int i = 0; i < 1; ++i) {
+		for (int i = 0; i < 100; ++i) {
 			bool priority_command_done = false;
 			runCommandAsync(new RCommand(QStringLiteral("%1cat(\"sleeping\\n\");%1Sys.sleep(5);cat(\"BUG\")").arg(QString().fill(u'\n', i % 5)), RCommand::User), nullptr, [&priority_command_done](RCommand *command) {
 				QVERIFY(priority_command_done);
diff --git a/rkward/rbackend/rkrbackend.cpp b/rkward/rbackend/rkrbackend.cpp
index 2e3701e66..7f59f540e 100644
--- a/rkward/rbackend/rkrbackend.cpp
+++ b/rkward/rbackend/rkrbackend.cpp
@@ -75,9 +75,11 @@ static bool RK_IsRInterruptPending() {
 #endif
 }
 
+/// schedule an intterupt to happen at the next R_CheckUserInterrupt() (i.e. inside R code)
 void RK_scheduleIntr() {
-	RK_DEBUG(RBACKEND, DL_DEBUG, "interrupt scheduled");
+	// NOTE: signal handler code. Must not produce debug output, here (could cause mutex deadlocK)
 	RKRBackend::repl_status.interrupted = true;
+	RKRBackend::this_pointer->awaiting_sigint = false;
 #ifdef Q_OS_WIN
 	ROb(UserBreak) = 1;
 #else
@@ -85,11 +87,13 @@ void RK_scheduleIntr() {
 #endif
 }
 
+/// force an immediate intterupt (unless R_interrupts_suspended
 void RK_doIntr() {
 	RK_scheduleIntr();
 	RFn::R_CheckUserInterrupt();
 }
 
+/// thread-safe function to trigger an R interrupt, as if Ctrl+C had been hit in the R console
 void RKRBackend::scheduleInterrupt() {
 	if (RKRBackendProtocolBackend::inRThread()) {
 		RK_scheduleIntr();
@@ -106,51 +110,86 @@ void RKRBackend::scheduleInterrupt() {
 void RKRBackend::interruptCommand(int command_id) {
 	RK_TRACE(RBACKEND);
 	RK_ASSERT(command_id >= 0); // -1 would signify broken request
+	RK_ASSERT(!RKRBackendProtocolBackend::inRThread());
 	RK_DEBUG(RBACKEND, DL_DEBUG, "Received interrupt request for command id %d", command_id);
-	QMutexLocker lock(&all_current_commands_mutex);
+	QMutexLocker lock(&command_flow_mutex);
 
 	/** A request to interrupt a command may happen at very different points in the code:
 	 *  0) before it was even sent from the frontend -> frontend will not even send it to the backend code
 	 *  1) after it was sent from the frontend, but before we properly started handling it in the backend (deserializing the request, pushing it onto
-	 *     all_current_commands, feeding it into the REPL/eval) TODO: buggy!
+	 *     all_current_commands, feeding it into the REPL/eval) -> handled in beginAllowInterruptCommand()
 	 *  2) when it is properly running in the backend -> this is the typical case, we worry about
-	 *  2b) when it is properly running in the backend, but there is also an active sub-command, which we should allow to complete
-	 *  3) after it finished running in the backend, but the frontend wasn't aware of that, yet TODO: buggy! */
-	if (!all_current_commands.isEmpty() && (all_current_commands.last()->id == command_id)) {
-		if (too_late_to_interrupt) {
-			RK_DEBUG(RBACKEND, DL_DEBUG, "too late to interrupt command id %d (repl uc status: %d)", command_id, repl_status.user_command_status);
-		} else {
-			RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling interrupt for command id %d (repl uc status: %d)", command_id, repl_status.user_command_status);
-			lock.unlock();
-			scheduleInterrupt();
-			return;
-		}
+	 *  2b) when it is properly running in the backend, but there is also an active sub-command, which we should allow to complete -> TODO buggy
+	 *  3) after it has finished running in the backend, but the frontend wasn't aware of that, yet TODO: buggy! */
+	if (current_command && (current_command->id == command_id) && (current_command->interruptible_stage)) {
+		RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling immediate interrupt for command id %d", command_id);
+		awaiting_sigint = true;
+		scheduleInterrupt();
 	} else {
-		bool any_found = false;
-		// if the command to cancel is *not* the topmost command, then do not interrupt, yet.
-		for (RCommandProxy *candidate : std::as_const(all_current_commands)) {
-			if (candidate->id == command_id) {
-				if (!current_commands_to_cancel.contains(candidate)) {
-					RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling delayed interrupt for command id %d", command_id);
-					current_commands_to_cancel.append(candidate);
-					any_found = true;
-				}
-			}
-		}
-		if (!any_found) {
-			RK_DEBUG(RBACKEND, DL_ERROR, "interrupt scheduled for command id %d, but it is not current", command_id);
+		RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling delayed interrupt for command id %d", command_id);
+		commands_to_cancel_deferred.append(command_id);
+	}
+}
+
+void RKRBackend::beginAllowInterruptCommand(RCommandProxy *command) {
+	if (command->type & RCommand::Internal) return;
+	RK_TRACE(RBACKEND);
+
+	bool intr = false;
+	{
+		QMutexLocker m(&command_flow_mutex);
+		RK_ASSERT(!command->interruptible_stage);
+		command->interruptible_stage = true;
+
+		if (commands_to_cancel_deferred.contains(command->id)) {
+			RK_DEBUG(RBACKEND, DL_DEBUG, "triggering deferred interrupt for command %d", command->id);
+			commands_to_cancel_deferred.removeAll(command->id);
+			intr = true;
 		}
 	}
+	if (intr) {
+		// do not call while mutex is locked!
+		RK_doIntr();
+	}
 }
 
-void RKRBackend::clearPendingInterrupt() {
+void RKRBackend::endAllowInterruptCommand(RCommandProxy *command) {
+	if (command->type & RCommand::Internal) return;
 	RK_TRACE(RBACKEND);
-	if (!RK_IsRInterruptPending()) return;
-	RKRBackend::repl_status.interrupted = false;
 
-	bool passed = RFn::R_ToplevelExec([](void *) { RFn::R_CheckUserInterrupt(); }, nullptr);
-	if (!passed) RK_DEBUG(RBACKEND, DL_DEBUG, "pending interrupt cleared");
-	RK_ASSERT(!passed);
+	bool wait_interrupt;
+	{
+		QMutexLocker m(&command_flow_mutex);
+		RK_ASSERT(command->interruptible_stage);
+		command->interruptible_stage = false;
+		wait_interrupt = awaiting_sigint;
+	}
+	// wait for any pending interrupt to happen
+	while (wait_interrupt) {
+		// do not call while mutex is locked!
+		RFn::R_CheckUserInterrupt();
+		RKRBackendProtocolBackend::msleep(10); // TODO: timeout after a while? In case interrupts are blocked, or something?
+		{
+			QMutexLocker m(&command_flow_mutex);
+			wait_interrupt = awaiting_sigint;
+		}
+	}
+}
+
+void RKRBackend::handleDeferredInterrupts() {
+	RK_TRACE(RBACKEND);
+	bool intr = false;
+	{
+		QMutexLocker m(&command_flow_mutex);
+		if (current_command && commands_to_cancel_deferred.contains(current_command->id) && current_command->interruptible_stage) {
+			RK_DEBUG(RBACKEND, DL_DEBUG, "triggering deferred interrupt for command %d", current_command->id);
+			commands_to_cancel_deferred.removeAll(current_command->id);
+			intr = true;
+		}
+	}
+	if (intr) {
+		RK_scheduleIntr();
+	}
 }
 
 static void markLastWarningAsErrorMessage() {
@@ -195,6 +234,7 @@ static void RKTransmitNextUserCommandChunk(unsigned char *buf, int buflen) {
 	}
 	buf[++pos] = '\0';
 	RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandTransmitted;
+	RK_DEBUG(RBACKEND, DL_DEBUG, "transmitted chunk %s", buf);
 
 	if (reached_newline || reached_eof) {
 		// feed a marker into the output stream that a source line of the command shall be interleaved into the output, at this point (if running in the console)
@@ -206,6 +246,12 @@ static void RKTransmitNextUserCommandChunk(unsigned char *buf, int buflen) {
 
 void RBusy(int);
 
+static void replCommandFinished() {
+	RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::NoUserCommand;
+	RKRBackend::this_pointer->commandFinished();
+	RKRBackend::this_pointer->handleDeferredInterrupts();
+}
+
 int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
 	RK_TRACE(RBACKEND);
 
@@ -231,6 +277,7 @@ int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
 				if (!(command->type & RCommand::User)) {
 					RKRBackend::this_pointer->runCommand(command);
 					RKRBackend::this_pointer->commandFinished();
+					RKRBackend::this_pointer->handleDeferredInterrupts();
 				} else {
 					// so, we are about to transmit a new user command, which is quite a complex endeavor...
 					/* Some words about running user commands:
@@ -264,11 +311,6 @@ int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
 					RKTransmitNextUserCommandChunk(buf, buflen);
 					return 1;
 				}
-			} else if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::UserCommandSyntaxError) {
-				RBusy(1); // properly init command, so commandFinished() can end it
-				RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorSyntax;
-				RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::NoUserCommand;
-				RKRBackend::this_pointer->commandFinished();
 			} else if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::UserCommandRunning) {
 				// This can mean three different things:
 				// 1) User called readline ()
@@ -289,21 +331,20 @@ int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
 				if (n_frames < 1) {
 					// No active frames? This can't be a call to readline(), so the previous command must have finished.
 					if (RKRBackend::repl_status.user_command_completely_transmitted) {
-						RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::NoUserCommand;
-						RKRBackend::this_pointer->commandFinished();
+						replCommandFinished();
 					} else RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandTransmitted;
 				} else {
 					// A call to readline(). Will be handled below
 					break;
 				}
 			} else if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::UserCommandFailed) {
-				RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorOther;
-				RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::NoUserCommand;
-				RKRBackend::this_pointer->commandFinished();
+				if (!(RKRBackend::this_pointer->current_command->status & RCommand::ErrorSyntax)) {
+					RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorOther;
+				}
+				replCommandFinished();
 			} else {
 				RK_ASSERT(RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::ReplIterationKilled);
-				RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::NoUserCommand;
-				RKRBackend::this_pointer->commandFinished();
+				replCommandFinished();
 			}
 		}
 	}
@@ -349,7 +390,7 @@ int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
 	if (request.params[QStringLiteral("cancelled")].toBool()) {
 		if (RKRBackend::this_pointer->current_command) RKRBackend::this_pointer->current_command->status |= RCommand::Canceled;
 		RFn::Rf_error("cancelled"); // TODO: probably a memleak, as d'tors (request, params) won't be called
-		RK_ASSERT(false); // should not reach this point.
+		RK_ASSERT(false);           // should not reach this point.
 	}
 
 	QByteArray localres = RKTextCodec::toNative(request.params[QStringLiteral("result")].toString());
@@ -394,10 +435,9 @@ void RWriteConsoleEx(const char *buf, int buflen, int type) {
 	// output while nothing else is running (including handlers?) -> This may be a syntax error.
 	if ((RKRBackend::repl_status.eval_depth == 0) && (!RKRBackend::repl_status.browser_context) && (!RKRBackend::this_pointer->isKilled())) {
 		if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::UserCommandTransmitted) {
-			// status UserCommandTransmitted might have been set from RKToplevelStatementFinishedHandler, too, in which case all is fine
-			// (we're probably inside another task handler at this point, then)
-			if (RKRBackend::repl_status.user_command_parsed_up_to < RKRBackend::repl_status.user_command_transmitted_up_to) {
-				RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandSyntaxError;
+			if (RKRBackend::repl_status.user_command_parsed_up_to < RKRBackend::repl_status.user_command_transmitted_up_to && !RKRBackend::repl_status.interrupted) {
+				RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorSyntax;
+				RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandFailed;
 			}
 		} else if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::ReplIterationKilled) {
 			// purge superfluous newlines and empty output
@@ -549,12 +589,14 @@ The interesting bit is in rresetconsole_called(). */
 struct RKUserCommandErrorDetection {
 	bool reset_signals_error = true;
 	void rbusy_called(int busy) {
-		reset_signals_error = busy;
+		if (busy) reset_signals_error = true;
+		RK_DEBUG(RBACKEND, DL_DEBUG, "RBusy %d", busy);
 	}
 	void reditfiles_called() {
 		reset_signals_error = false;
 	}
 	void rresetconsole_called() {
+		RK_DEBUG(RBACKEND, DL_WARNING, "reset console called, repl status %d, reset_signals_error %d, wasintr %d, awaiting_sigint %d", RKRBackend::repl_status.user_command_status, reset_signals_error, RKRBackend::repl_status.interrupted, RKRBackend::this_pointer->awaiting_sigint);
 		if (!reset_signals_error) {
 			reset_signals_error = true;
 			return;
@@ -566,9 +608,7 @@ struct RKUserCommandErrorDetection {
 			RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandFailed;
 		}
 
-		// it is unlikely, but possible, that an interrupt signal was received, but the current command failed for some other reason, before processing was actually interrupted. In this case, R_interrupts_pending is not yet cleared.
-		const bool command_was_interrupted = RKRBackend::repl_status.interrupted && !RK_IsRInterruptPending();
-		if (!command_was_interrupted) {
+		if (!RKRBackend::repl_status.interrupted) {
 			markLastWarningAsErrorMessage();
 			RK_DEBUG(RBACKEND, DL_DEBUG, "error in user command");
 		}
@@ -718,6 +758,11 @@ void RBusy(int busy) {
 			}
 			RKRBackend::repl_status.user_command_parsed_up_to = RKRBackend::repl_status.user_command_transmitted_up_to;
 			RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandRunning;
+			RKRBackend::this_pointer->beginAllowInterruptCommand(RKRBackend::this_pointer->current_command);
+		}
+	} else if (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::NoUserCommand && RKRBackend::repl_status.eval_depth == 0) {
+		if (RKRBackend::this_pointer->current_command->interruptible_stage) {
+			RKRBackend::this_pointer->endAllowInterruptCommand(RKRBackend::this_pointer->current_command);
 		}
 	}
 }
@@ -949,7 +994,7 @@ bool RKRBackend::startR() {
 
 	RKSignalSupport::saveDefaultSignalHandlers();
 
-	too_late_to_interrupt = false;
+	awaiting_sigint = false;
 	r_running = true;
 	int argc = 3;
 	char *argv[3] = {qstrdup("rkward"), qstrdup("--no-save"), qstrdup("--no-restore")};
@@ -1188,6 +1233,7 @@ static void parseAndRunWorker(void *_data) {
 	}
 
 	// evaluate the actual command - also, if it consists of multiple expressions, internally
+	RKRBackend::this_pointer->beginAllowInterruptCommand(data->command);
 	if (RFn::TYPEOF(parsed) == EXPRSXP && RFn::LENGTH(parsed) > 0) {
 		const int len = RFn::LENGTH(parsed);
 		for (int bi = 0; bi < len; bi++) {
@@ -1198,6 +1244,7 @@ static void parseAndRunWorker(void *_data) {
 		exp = RFn::Rf_eval(parsed, ROb(R_GlobalEnv));
 	}
 	RFn::Rf_protect(exp);
+	RKRBackend::this_pointer->endAllowInterruptCommand(data->command);
 	data->status = RKParseAndRunData::Evalled;
 
 	if (data->warn_level != 1) setWarnOption(data->warn_level);
@@ -1276,7 +1323,7 @@ void doPendingPriorityCommands() {
 	if (command) {
 		RK_DEBUG(RBACKEND, DL_DEBUG, "running priority command %s", qPrintable(command->command));
 		{
-			QMutexLocker lock(&RKRBackend::this_pointer->all_current_commands_mutex);
+			QMutexLocker lock(&RKRBackend::this_pointer->command_flow_mutex);
 			RKRBackend::this_pointer->all_current_commands.append(command);
 			RKRBackend::this_pointer->current_command = command;
 		}
@@ -1286,10 +1333,13 @@ void doPendingPriorityCommands() {
 		RCommandProxy *previous_command_backup = RKRBackend::this_pointer->previous_command;
 		RKRBackend::this_pointer->commandFinished(false);
 		RKRBackend::this_pointer->previous_command = previous_command_backup;
-		RBackendRequest req(false, RBackendRequest::CommandOut); // NOTE: We do *NOT* want a reply to this one, and in particular, we do *NOT* want to do
-		                                                         // (recursive) event processing while handling this.
-		req.command = command;
-		RKRBackend::this_pointer->handleRequest(&req);
+		{
+			RBackendRequest req(false, RBackendRequest::CommandOut); // NOTE: We do *NOT* want a reply to this one, and in particular, we do *NOT* want to do
+			                                                         // (recursive) event processing while handling this.
+			req.command = command;
+			RKRBackend::this_pointer->handleRequest(&req);
+		}
+		RKRBackend::this_pointer->handleDeferredInterrupts();
 	}
 }
 
@@ -1347,13 +1397,7 @@ void RKRBackend::run(const QString &locale_dir, bool setup) {
 
 void RKRBackend::commandFinished(bool check_object_updates_needed) {
 	RK_TRACE(RBACKEND);
-	RK_DEBUG(RBACKEND, DL_DEBUG, "done running command %s", qPrintable(current_command->command));
-
-	{
-		QMutexLocker lock(&all_current_commands_mutex);
-		too_late_to_interrupt = true;
-	}
-	clearPendingInterrupt(); // Mutex must be unlocked for this!
+	RK_DEBUG(RBACKEND, DL_DEBUG, "done running command %s, status: %d", qPrintable(current_command->command), current_command->status);
 
 	fetchStdoutStderr(true);
 	if (current_command->type & RCommand::CCOutput) printAndClearCapturedMessages(current_command->type & RCommand::Plugin);
@@ -1380,15 +1424,13 @@ void RKRBackend::commandFinished(bool check_object_updates_needed) {
 	previous_command = current_command;
 
 	{
-		QMutexLocker lock(&all_current_commands_mutex);
+		QMutexLocker lock(&command_flow_mutex);
 		if (repl_status.interrupted) {
 			repl_status.interrupted = false;
 			current_command->status |= RCommand::Canceled;
-			clearPendingInterrupt(); // might not have been handled, yet
 		}
 		all_current_commands.pop_back();
 		if (!all_current_commands.isEmpty()) current_command = all_current_commands.last();
-		too_late_to_interrupt = false;
 	}
 }
 
@@ -1434,7 +1476,7 @@ RCommandProxy *RKRBackend::handleRequest2(RBackendRequest *request, bool mayHand
 	if (!command) return nullptr;
 
 	{
-		QMutexLocker lock(&all_current_commands_mutex);
+		QMutexLocker lock(&command_flow_mutex);
 		RK_ASSERT(command != current_command);
 		all_current_commands.append(command);
 		current_command = command;
@@ -1445,18 +1487,12 @@ RCommandProxy *RKRBackend::handleRequest2(RBackendRequest *request, bool mayHand
 	while (command) {
 		runCommand(command);
 		commandFinished(false);
+		handleDeferredInterrupts();
 
 		command = fetchNextCommand();
 	};
 
-	{
-		QMutexLocker lock(&all_current_commands_mutex);
-		if (current_commands_to_cancel.contains(current_command)) {
-			RK_DEBUG(RBACKEND, DL_DEBUG, "will now interrupt parent command");
-			current_commands_to_cancel.removeAll(current_command);
-			scheduleInterrupt();
-		}
-	}
+	handleDeferredInterrupts();
 
 	return nullptr;
 }
diff --git a/rkward/rbackend/rkrbackend.h b/rkward/rbackend/rkrbackend.h
index d5ca3ff4d..a7f683bce 100644
--- a/rkward/rbackend/rkrbackend.h
+++ b/rkward/rbackend/rkrbackend.h
@@ -133,7 +133,6 @@ class RKRBackend : public RKROutputBuffer {
 		enum {
 			NoUserCommand,
 			UserCommandTransmitted,
-			UserCommandSyntaxError,
 			UserCommandRunning,
 			UserCommandFailed,
 			ReplIterationKilled
@@ -162,10 +161,13 @@ class RKRBackend : public RKROutputBuffer {
 	void printCommand(const QString &command);
 	void catToOutputFile(const QString &out);
 
-	QMutex all_current_commands_mutex;
-	QList<RCommandProxy *> current_commands_to_cancel;
-	bool too_late_to_interrupt;
+	QMutex command_flow_mutex;
+	QList<int> commands_to_cancel_deferred;
 	void interruptCommand(int command_id);
+	void beginAllowInterruptCommand(RCommandProxy *command);
+	void endAllowInterruptCommand(RCommandProxy *command);
+	void handleDeferredInterrupts();
+	volatile bool awaiting_sigint;
 
 	/** check stdout and stderr for new output (from sub-processes). Since this function is called from both threads, it is protected by a mutex.
 	 *  @param forcibly: if false, and the other thread currently has a lock on the mutex, do nothing, and return false.
@@ -181,9 +183,6 @@ class RKRBackend : public RKROutputBuffer {
 
 	bool graphicsEngineMismatchMessage(int compiled_version, int runtime_version);
 
-  private:
-	void clearPendingInterrupt();
-
   protected:
 	RCommandProxy *handleRequest(RBackendRequest *request, bool mayHandleSubstack);
 	RCommandProxy *handleRequest2(RBackendRequest *request, bool mayHandleSubstack);
diff --git a/rkward/rbackend/rkrbackendprotocol_backend.cpp b/rkward/rbackend/rkrbackendprotocol_backend.cpp
index b69a4bde7..37369b982 100644
--- a/rkward/rbackend/rkrbackendprotocol_backend.cpp
+++ b/rkward/rbackend/rkrbackendprotocol_backend.cpp
@@ -16,6 +16,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QLocalSocket>
 #include <QMutex>
 #include <QThread>
+#include <QTime>
 #include <QUrl>
 #include <QUuid> // mis-used as a random-string generator
 
@@ -36,6 +37,9 @@ void RKDebugMessageOutput(QtMsgType type, const QMessageLogContext &, const QStr
 	if (type == QtFatalMsg) {
 		fprintf(stderr, "%s\n", qPrintable(msg));
 	}
+	RK_Debug::debug_file->write("B ");
+	RK_Debug::debug_file->write(qPrintable(QString::number(QTime::currentTime().msecsSinceStartOfDay())));
+	RK_Debug::debug_file->write(": ");
 	RK_Debug::debug_file->write(qPrintable(msg));
 	RK_Debug::debug_file->write("\n");
 	RK_Debug::debug_file->flush();
diff --git a/rkward/rbackend/rkrbackendprotocol_shared.cpp b/rkward/rbackend/rkrbackendprotocol_shared.cpp
index 31710194c..237f1a4d2 100644
--- a/rkward/rbackend/rkrbackendprotocol_shared.cpp
+++ b/rkward/rbackend/rkrbackendprotocol_shared.cpp
@@ -16,6 +16,7 @@ RCommandProxy::RCommandProxy(const QString &command, int type) {
 	RCommandProxy::type = type;
 	id = -1;
 	status = 0;
+	interruptible_stage = false;
 }
 
 RCommandProxy::~RCommandProxy() {
diff --git a/rkward/rbackend/rkrbackendprotocol_shared.h b/rkward/rbackend/rkrbackendprotocol_shared.h
index 7e1dacf17..7aea84cae 100644
--- a/rkward/rbackend/rkrbackendprotocol_shared.h
+++ b/rkward/rbackend/rkrbackendprotocol_shared.h
@@ -127,6 +127,7 @@ class RCommandProxy : public RData {
 	int id;
 	int status;
 	int has_been_run_up_to;
+	bool interruptible_stage;
 };
 
 class RKROutputBuffer {
diff --git a/rkward/rbackend/rkreventloop.cpp b/rkward/rbackend/rkreventloop.cpp
index d2daabfd3..813af0cbe 100644
--- a/rkward/rbackend/rkreventloop.cpp
+++ b/rkward/rbackend/rkreventloop.cpp
@@ -14,7 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 
 void RK_doIntr();
 
-static void processX11EventsWorker(void *) {
+static void processX11EventsWorker(void *ok) {
 // this basically copied from R's unix/sys-std.c (Rstd_ReadConsole)
 #ifndef Q_OS_WIN
 	for (;;) {
@@ -44,6 +44,7 @@ TODO: verify we really need this. */
 		timeout_counter = 0;
 	}
 #endif
+	*static_cast<bool *>(ok) = true;
 }
 
 void RKREventLoop::processX11Events() {
@@ -51,10 +52,12 @@ void RKREventLoop::processX11Events() {
 	if (!RKRBackend::this_pointer->r_running) return;
 	if (RKRBackend::this_pointer->isKilled()) return;
 
+	bool ok = false;
 	RKRBackend::repl_status.eval_depth++;
 	// In case an error (or user interrupt) is caught inside processX11EventsWorker, we don't want to long-jump out.
-	RFn::R_ToplevelExec(processX11EventsWorker, nullptr);
+	RFn::R_ToplevelExec(processX11EventsWorker, &ok);
 	RKRBackend::repl_status.eval_depth--;
+	if (!ok) RK_DEBUG(RBACKEND, DL_WARNING, "Error in process events");
 }
 
 static void (*RK_eventHandlerFunction)() = nullptr;
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index e37b17811..ec2241e98 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -473,7 +473,7 @@ void RInterface::flushOutput(bool forced) {
 	for (const ROutput &output : list) {
 		if (output.type == ROutput::CommandLineIn) {
 			RCommand *command = all_current_commands.value(0, nullptr); // User command will always be the first.
-			if ((command == nullptr) || (!command->type() & RCommand::User)) {
+			if ((command == nullptr) || !(command->type() & RCommand::User)) {
 				RK_ASSERT(false);
 			} else {
 				command->commandLineIn();
diff --git a/rkward/rbackend/rksignalsupport.cpp b/rkward/rbackend/rksignalsupport.cpp
index 012add4eb..54ff91f2c 100644
--- a/rkward/rbackend/rksignalsupport.cpp
+++ b/rkward/rbackend/rksignalsupport.cpp
@@ -36,7 +36,7 @@ struct sigaction default_sigabrt_handler;
 rk_sighandler_t r_sigint_handler = nullptr;
 void (*new_sigint_handler)(void) = nullptr;
 void internal_sigint_handler(int num) {
-	RK_DEBUG(RBACKEND, DL_DEBUG, "calling internal sigint handler");
+	// NOTE: signal handler code. Must not produce debug output, here (could cause mutex deadlocK)
 	new_sigint_handler();
 	signal(num, internal_sigint_handler);
 }



More information about the rkward-tracker mailing list