1 /***************************************************************************
2                           rkrinterface.cpp  -  description
3                              -------------------
4     begin                : Fri Nov 1 2002
5     copyright            : (C) 2002-2019 by Thomas Friedrichsmeier
6     email                : thomas.friedrichsmeier@kdemail.net
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "rkrinterface.h"
19 
20 #include "rcommandstack.h"
21 #include "rkrbackendprotocol_frontend.h"
22 #include "../rkward.h"
23 #include "../rkconsole.h"
24 #include "../settings/rksettingsmoduler.h"
25 #include "../settings/rksettingsmodulegeneral.h"
26 #include "../settings/rksettingsmoduleoutput.h"
27 #include "../settings/rksettingsmodulegraphics.h"
28 #include "../settings/rksettingsmoduleplugins.h"
29 #include "../core/robjectlist.h"
30 #include "../core/renvironmentobject.h"
31 #include "../core/rkmodificationtracker.h"
32 #include "../dialogs/rkloadlibsdialog.h"
33 #include "../dialogs/rkselectlistdialog.h"
34 #include "../dialogs/rkreadlinedialog.h"
35 #include "../dialogs/rkerrordialog.h"
36 #include "../agents/showedittextfileagent.h"
37 #include "../agents/rkeditobjectagent.h"
38 #include "../agents/rkprintagent.h"
39 #include "../agents/rkdebughandler.h"
40 #include "../windows/rcontrolwindow.h"
41 #include "../windows/rkworkplace.h"
42 #include "../windows/rkcommandlog.h"
43 #include "../windows/rkhtmlwindow.h"
44 #include "../plugin/rkcomponentmap.h"
45 #include "../misc/rkcommonfunctions.h"
46 #include "../misc/rkmessagecatalog.h"
47 #include "rksessionvars.h"
48 
49 #include "../windows/rkwindowcatcher.h"
50 
51 #include "../rkglobals.h"
52 #include "../version.h"
53 #include "../debug.h"
54 
55 #include <kmessagebox.h>
56 #include <KLocalizedString>
57 
58 #include <qdir.h>
59 #include <qvalidator.h>
60 
61 #include <stdlib.h>
62 #include <QFileDialog>
63 #include <QApplication>
64 #include <QPushButton>
65 
66 // flush new pieces of output after this period of time:
67 #define FLUSH_INTERVAL 100
68 
69 #define GET_LIB_PATHS 1
70 #define GET_HELP_BASE 2
71 #define SET_RUNTIME_OPTS 3
72 #define STARTUP_PHASE2_COMPLETE 4
73 #define GET_R_VERSION 5
74 #define RSTARTUP_COMPLETE 6
75 
76 // statics
77 double RInterface::na_real;
78 int RInterface::na_int;
79 
RInterface()80 RInterface::RInterface () {
81 	RK_TRACE (RBACKEND);
82 
83 	new RCommandStackModel (this);
84 	RCommandStack::regular_stack = new RCommandStack ();
85 	startup_phase2_error = false;
86 	command_logfile_mode = NotRecordingCommands;
87 	previously_idle = false;
88 	locked = 0;
89 	backend_dead = false;
90 	flush_timer_id = 0;
91 	dummy_command_on_stack = 0;
92 
93 	// create a fake init command
94 	RCommand *fake = new RCommand (i18n ("R Startup"), RCommand::App | RCommand::Sync | RCommand::ObjectListUpdate, i18n ("R Startup"), this, STARTUP_PHASE2_COMPLETE);
95 	issueCommand (fake);
96 
97 	new RKSessionVars (this);
98 	new RKDebugHandler (this);
99 	new RKRBackendProtocolFrontend (this);
100 	RKRBackendProtocolFrontend::instance ()->setupBackend ();
101 
102 	/////// Further initialization commands, which do not necessarily have to run before everything else can be queued, here. ///////
103 	// NOTE: will receive the list as a call plain generic request from the backend ("updateInstalledPackagesList")
104 	issueCommand (".rk.get.installed.packages()", RCommand::App | RCommand::Sync);
105 
106 	issueCommand (new RCommand (QString (), RCommand::App | RCommand::Sync | RCommand::EmptyCommand, QString (), this, RSTARTUP_COMPLETE));
107 }
108 
issueCommand(const QString & command,int type,const QString & rk_equiv,RCommandReceiver * receiver,int flags,RCommandChain * chain)109 void RInterface::issueCommand (const QString &command, int type, const QString &rk_equiv, RCommandReceiver *receiver, int flags, RCommandChain *chain) {
110 	RK_TRACE (RBACKEND);
111 	issueCommand (new RCommand (command, type, rk_equiv, receiver, flags), chain);
112 }
113 
~RInterface()114 RInterface::~RInterface(){
115 	RK_TRACE (RBACKEND);
116 
117 	RKWindowCatcher::discardInstance ();
118 }
119 
backendIsIdle()120 bool RInterface::backendIsIdle () {
121 	RK_TRACE (RBACKEND);
122 
123 	return (RCommandStack::regular_stack->isEmpty() && (!runningCommand()));
124 }
125 
popPreviousCommand(int id)126 RCommand *RInterface::popPreviousCommand (int id) {
127 	RK_TRACE (RBACKEND);
128 
129 	RK_ASSERT (!all_current_commands.isEmpty ());
130 	for (int i = all_current_commands.size () - 1; i >= 0; --i) {
131 		RCommand *ret = all_current_commands[i];
132 		if (ret->id () == id) {
133 			RCommandStack::pop (ret);
134 			all_current_commands.removeAt (i);
135 			return ret;
136 		}
137 	}
138 	RK_ASSERT (false);
139 	return 0;
140 }
141 
openSubcommandChain(RCommand * parent_command)142 RCommandChain* RInterface::openSubcommandChain (RCommand* parent_command) {
143 	RK_TRACE (RBACKEND);
144 
145 	current_commands_with_subcommands.append (parent_command);
146 	return RCommandStack::startChain (parent_command);
147 }
148 
closeSubcommandChain(RCommand * parent_command)149 void RInterface::closeSubcommandChain (RCommand* parent_command) {
150 	RK_TRACE (RBACKEND);
151 
152 	if (current_commands_with_subcommands.contains (parent_command)) {
153 		current_commands_with_subcommands.removeAll (parent_command);
154 		doNextCommand (0);
155 	}
156 	if (parent_command && (parent_command == dummy_command_on_stack)) {
157 		all_current_commands.removeAll (dummy_command_on_stack);
158 		RCommandStack::pop (dummy_command_on_stack);
159 		handleCommandOut (dummy_command_on_stack);
160 		dummy_command_on_stack = 0;
161 	}
162 }
163 
tryNextCommand()164 void RInterface::tryNextCommand () {
165 	RK_TRACE (RBACKEND);
166 	RCommand *command = RCommandStack::currentCommand ();
167 	if (command_requests.isEmpty ()) {
168 		// if the backend is not requesting anything, only priority commands will be pushed
169 		if (!command) return;
170 		if (!(command->type () & RCommand::PriorityCommand)) return;
171 		if (all_current_commands.contains (command)) return;
172 	}
173 
174 	bool priority = command && (command->type () & RCommand::PriorityCommand);
175 	bool on_top_level = all_current_commands.isEmpty ();
176 	if (!(on_top_level && locked && !(priority))) {                                     // do not respect locks for sub-commands
177 		if ((!on_top_level) && all_current_commands.contains (command)) {  // all sub-commands of the current command have finished, it became the top-most item of the RCommandStack, again
178 			closeSubcommandChain (command);
179 			return;
180 		}
181 
182 		if (command) {
183 			all_current_commands.append (command);
184 
185 			if (command->status & RCommand::Canceled) {
186 				// avoid passing cancelled commands to R
187 				command->status |= RCommand::Failed;
188 
189 				// notify ourselves...
190 				RCommand* dummy = popPreviousCommand (command->id ());
191 				RK_ASSERT (dummy == command);
192 				handleCommandOut (command);
193 				return;
194 			}
195 
196 			if (previously_idle) RKWardMainWindow::getMain ()->setRStatus (RKWardMainWindow::Busy);
197 			previously_idle = false;
198 
199 			doNextCommand (command);
200 			return;
201 		}
202 	}
203 
204 	if (on_top_level) {
205 		if (!previously_idle) RKWardMainWindow::getMain ()->setRStatus (RKWardMainWindow::Idle);
206 		previously_idle = true;
207 	}
208 }
209 
handleCommandOut(RCommand * command)210 void RInterface::handleCommandOut (RCommand *command) {
211 	RK_TRACE (RBACKEND);
212 
213 	RK_ASSERT (command);
214 
215 	#ifdef RKWARD_DEBUG
216 		int dl = DL_WARNING;		// failed application commands are an issue worth reporting, failed user commands are not
217 		if (command->type () & RCommand::User) dl = DL_DEBUG;
218 		if (command->failed ()) {
219 			command->status |= RCommand::WasTried | RCommand::Failed;
220 			if (command->status & RCommand::ErrorIncomplete) {
221 				RK_DEBUG (RBACKEND, dl, "Command failed (incomplete)");
222 			} else if (command->status & RCommand::ErrorSyntax) {
223 				RK_DEBUG (RBACKEND, dl, "Command failed (syntax)");
224 			} else if (command->status & RCommand::Canceled) {
225 				RK_DEBUG (RBACKEND, dl, "Command failed (interrupted)");
226 			} else {
227 				RK_DEBUG (RBACKEND, dl, "Command failed (other)");
228 			}
229 			RK_DEBUG (RBACKEND, dl, "failed command was: '%s'", qPrintable (command->command ()));
230 			RK_DEBUG (RBACKEND, dl, "- error message was: '%s'", qPrintable (command->error ()));
231 		}
232 	#endif
233 
234 	if (command->status & RCommand::Canceled) {
235 		command->status |= RCommand::HasError;
236 		ROutput *out = new ROutput;
237 		out->type = ROutput::Error;
238 		out->output = ("--- interrupted ---");
239 		command->output_list.append (out);
240 		command->newOutput (out);
241 	}
242 	command->finished ();
243 	delete command;
244 }
245 
doNextCommand(RCommand * command)246 void RInterface::doNextCommand (RCommand *command) {
247 	RK_TRACE (RBACKEND);
248 	RBackendRequest* command_request = currentCommandRequest ();
249 	if (!command_request) {
250 		if (!(command && (command->type () & RCommand::PriorityCommand))) return;
251 	}
252 	// importantly, this point is not reached for the fake startup command
253 
254 	if (RK_Debug::RK_Debug_CommandStep) {
255 		QTime t;
256 		t.start ();
257 		while (t.elapsed () < RK_Debug::RK_Debug_CommandStep) {}
258 	}
259 
260 	flushOutput (true);
261 	RCommandProxy *proxy = 0;
262 	if (command) {
263 		RKWardMainWindow::getMain ()->setWorkspaceMightBeModified (true);
264 		proxy = command->makeProxy ();
265 
266 		RK_DEBUG (RBACKEND, DL_DEBUG, "running command: %s", command->command ().toLatin1().data ());
267 		command->status |= RCommand::Running;
268 		RCommandStackModel::getModel ()->itemChange (command);
269 
270 		RKCommandLog::getLog ()->addInput (command);
271 
272 		if (command_logfile_mode != NotRecordingCommands) {
273 			bool record = true;
274 			if (command_logfile_mode != RecordingCommandsUnfiltered) {
275 				if (command->type () & (RCommand::Silent | RCommand::Sync)) record = false;
276 			}
277 			if (record) {
278 				command_logfile.write (command->command ().toUtf8 ());
279 				command_logfile.write ("\n");
280 			}
281 		}
282 	}
283 
284 	if (command && (command->type () & RCommand::PriorityCommand)) {
285 		RKRBackendProtocolFrontend::sendPriorityCommand (proxy);
286 	} else {
287 		RK_ASSERT (command_request);
288 		command_request->command = proxy;
289 		RKRBackendProtocolFrontend::setRequestCompleted (command_request);
290 		command_requests.pop_back ();
291 	}
292 }
293 
rCommandDone(RCommand * command)294 void RInterface::rCommandDone (RCommand *command) {
295 	RK_TRACE (RBACKEND);
296 
297 	if (command->failed ()) {
298 		startup_phase2_error = true;
299 		return;
300 	}
301 
302 	if (command->getFlags () == GET_LIB_PATHS) {
303 		RK_ASSERT (command->getDataType () == RData::StringVector);
304 		RKSettingsModuleRPackages::r_libs_user = command->stringVector ().value (0);
305 		RKSettingsModuleRPackages::defaultliblocs += command->stringVector ().mid (1);
306 
307 		RCommandChain *chain = command->parent;
308 		RK_ASSERT (chain);
309 		RK_ASSERT (!chain->isClosed ());
310 
311 		// apply user configurable run time options
312 		QStringList commands = RKSettingsModuleR::makeRRunTimeOptionCommands () + RKSettingsModuleRPackages::makeRRunTimeOptionCommands () + RKSettingsModuleOutput::makeRRunTimeOptionCommands () + RKSettingsModuleGraphics::makeRRunTimeOptionCommands ();
313 		for (QStringList::const_iterator it = commands.begin (); it != commands.end (); ++it) {
314 			issueCommand (*it, RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
315 		}
316 		// initialize output file
317 		issueCommand ("rk.set.output.html.file (\"" + RKSettingsModuleGeneral::filesPath () + "/rk_out.html\")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
318 #ifdef Q_OS_MACOS
319 		// On MacOS, the backend is started from inside R home to allow resolution of dynamic libs. Re-set to frontend wd, here.
320 		issueCommand ("setwd (" + RKRSharedFunctionality::quote (QDir::currentPath ()) + ")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
321 #endif
322 		closeChain (chain);
323 	} else if (command->getFlags () == GET_R_VERSION) {
324 		RK_ASSERT (command->getDataType () == RData::StringVector);
325 		RK_ASSERT (command->getDataLength () == 1);
326 		RKSessionVars::setRVersion (command->stringVector ().value (0));
327 	} else if (command->getFlags () == GET_HELP_BASE) {
328 		RK_ASSERT (command->getDataType () == RData::StringVector);
329 		RK_ASSERT (command->getDataLength () == 1);
330 		RKSettingsModuleR::help_base_url = command->stringVector ().value (0);
331 	} else if (command->getFlags () == SET_RUNTIME_OPTS) {
332 		// no special handling. In case of failures, staturt_fail was set to true, above.
333 	} else if (command->getFlags () == STARTUP_PHASE2_COMPLETE) {
334 		QString message = startup_errors;
335 		if (startup_phase2_error) message.append (i18n ("<p>\t-An unspecified error occurred that is not yet handled by RKWard. Likely RKWard will not function properly. Please check your setup.</p>\n"));
336 		if (!message.isEmpty ()) {
337 			message.prepend (i18n ("<p>There was a problem starting the R backend. The following error(s) occurred:</p>\n"));
338 
339 			QString details = command->fullOutput().replace('<', "&lt;").replace('\n', "<br>");
340 			if (!details.isEmpty ()) {
341 				// WORKAROUND for stupid KMessageBox behavior. (kdelibs 4.2.3)
342 				// If length of details <= 512, it tries to show the details as a QLabel.
343 				details = details.leftJustified (513);
344 			}
345 			KMessageBox::detailedError (0, message, details, i18n ("Error starting R"), KMessageBox::Notify | KMessageBox::AllowLink);
346 		}
347 
348 		startup_errors.clear ();
349 	} else if (command->getFlags () == RSTARTUP_COMPLETE) {
350 		RKSettings::validateSettingsInteractive ();
351 	}
352 }
353 
handleRequest(RBackendRequest * request)354 void RInterface::handleRequest (RBackendRequest* request) {
355 	RK_TRACE (RBACKEND);
356 
357 	if (request->type == RBackendRequest::OutputStartedNotification) {
358 		RK_ASSERT (flush_timer_id == 0);
359 		flush_timer_id = startTimer (FLUSH_INTERVAL);	// calls flushOutput (false); see timerEvent ()
360 		RKRBackendProtocolFrontend::setRequestCompleted (request);
361 		return;
362 	}
363 
364 	flushOutput (true);
365 	if (request->type == RBackendRequest::CommandOut) {
366 		RCommandProxy *cproxy = request->takeCommand ();
367 		if (cproxy) {
368 			RK_DEBUG (RBACKEND, DL_DEBUG, "Command out \"%s\", id %d", qPrintable (cproxy->command), cproxy->id);
369 		} else {
370 			RK_DEBUG (RBACKEND, DL_DEBUG, "Fake command out");
371 		}
372 		RCommand *command = 0;
373 		// NOTE: the order of processing is: first try to submit the next command, then handle the old command.
374 		// The reason for doing it this way, instead of the reverse, is that this allows the backend thread / process to continue working, concurrently
375 		// NOTE: cproxy should only ever be 0 in the very first cycle
376 		if (cproxy) command = popPreviousCommand (cproxy->id);
377 		if (request->synchronous) command_requests.append (request);
378 		tryNextCommand ();
379 		if (cproxy) {
380 			RK_ASSERT (command);
381 			command->mergeAndDeleteProxy (cproxy);
382 			handleCommandOut (command);
383 		}
384 		tryNextCommand ();
385 	} else if (request->type == RBackendRequest::HistoricalSubstackRequest) {
386 		RCommandProxy *cproxy = request->command;
387 		RCommand *parent = 0;
388 		for (int i = all_current_commands.size () - 1; i >= 0; --i) {
389 			if (all_current_commands[i]->id () == cproxy->id) {
390 				parent = all_current_commands[i];
391 				break;
392 			}
393 		}
394 		command_requests.append (request);
395 		processHistoricalSubstackRequest (request->params["call"].toStringList (), parent);
396 	} else if (request->type == RBackendRequest::PlainGenericRequest) {
397 		request->params["return"] = QVariant (processPlainGenericRequest (request->params["call"].toStringList ()));
398 		RKRBackendProtocolFrontend::setRequestCompleted (request);
399 	} else if (request->type == RBackendRequest::Started) {
400 		// The backend thread has finished basic initialization, but we still have more to do...
401 		startup_errors = request->params["message"].toString ();
402 
403 		command_requests.append (request);
404 		RCommandChain *chain = openSubcommandChain (runningCommand ());
405 
406 		issueCommand ("paste (R.version[c (\"major\", \"minor\")], collapse=\".\")\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_R_VERSION, chain);
407 		// find out about standard library locations
408 		issueCommand ("c(path.expand(Sys.getenv(\"R_LIBS_USER\")), .libPaths())\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_LIB_PATHS, chain);
409 		// start help server / determined help base url
410 		issueCommand (".rk.getHelpBaseUrl ()\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_HELP_BASE, chain);
411 
412 		// NOTE: more initialization commands get run *after* we have determined the standard library locations (see rCommandDone())
413 	} else {
414 		processRBackendRequest (request);
415 	}
416 }
417 
timerEvent(QTimerEvent *)418 void RInterface::timerEvent (QTimerEvent *) {
419 // do not trace. called periodically
420 	flushOutput (false);
421 }
422 
flushOutput(bool forced)423 void RInterface::flushOutput (bool forced) {
424 // do not trace. called periodically
425 //	RK_TRACE (RBACKEND);
426 	ROutputList list = RKRBackendProtocolFrontend::instance ()->flushOutput (forced);
427 
428 	// this must come _after_ the output has been flushed.
429 	if (forced || !list.isEmpty ()) {
430 		if (flush_timer_id != 0) {
431 			killTimer (flush_timer_id);
432 			flush_timer_id = 0;
433 		}
434 	}
435 
436 	foreach (ROutput *output, list) {
437 		if (all_current_commands.isEmpty ()) {
438 			RK_DEBUG (RBACKEND, DL_WARNING, "output without receiver'%s'", qPrintable (output->output));
439 			delete output;
440 			continue;	// to delete the other output pointers, too
441 		} else {
442 			RK_DEBUG (RBACKEND, DL_DEBUG, "output '%s'", qPrintable (output->output));
443 		}
444 
445 		bool first = true;
446 		foreach (RCommand* command, all_current_commands) {
447 			ROutput *coutput = output;
448 			if (!first) {		// this output belongs to several commands at once. So we need to copy it.
449 				coutput = new ROutput;
450 				coutput->type = output->type;
451 				coutput->output = output->output;
452 			}
453 			first = false;
454 
455 			if (coutput->type == ROutput::Output) {
456 				command->status |= RCommand::HasOutput;
457 				command->output_list.append (coutput);
458 			} else if (coutput->type == ROutput::Warning) {
459 				command->status |= RCommand::HasWarnings;
460 				command->output_list.append (coutput);
461 			} else if (coutput->type == ROutput::Error) {
462 				command->status |= RCommand::HasError;
463 				// An error output is typically just the copy of the previous output, so merge if possible
464 				if (command->output_list.isEmpty ()) {
465 					command->output_list.append (coutput);
466 				}
467 				if (command->output_list.last ()->output == coutput->output) {
468 					command->output_list.last ()->type = ROutput::Error;
469 					continue;	// don't call command->newOutput(), again!
470 				}
471 			}
472 			command->newOutput (coutput);
473 		}
474 	}
475 }
476 
issueCommand(RCommand * command,RCommandChain * chain)477 void RInterface::issueCommand (RCommand *command, RCommandChain *chain) {
478 	RK_TRACE (RBACKEND);
479 
480 	if (command->command ().isEmpty ()) command->_type |= RCommand::EmptyCommand;
481 	if (RKCarbonCopySettings::shouldCarbonCopyCommand (command)) {
482 		command->_type |= RCommand::CCCommand;
483 		if (RKCarbonCopySettings::includeOutputInCarbonCopy ()) command->_type |= RCommand::CCOutput;
484 	}
485 	RCommandStack::issueCommand (command, chain);
486 	tryNextCommand ();
487 }
488 
startChain(RCommandChain * parent)489 RCommandChain *RInterface::startChain (RCommandChain *parent) {
490 	RK_TRACE (RBACKEND);
491 
492 	return RCommandStack::startChain (parent);
493 };
494 
closeChain(RCommandChain * chain)495 void RInterface::closeChain (RCommandChain *chain) {
496 	RK_TRACE (RBACKEND);
497 
498 	RCommandStack::closeChain (chain);
499 	tryNextCommand ();
500 };
501 
cancelAll()502 void RInterface::cancelAll () {
503 	RK_TRACE (RBACKEND);
504 
505 	QList<RCommand*> all_commands = RCommandStack::regular_stack->allCommands ();
506 	foreach (RCommand* command, all_commands) cancelCommand (command);
507 }
508 
softCancelCommand(RCommand * command)509 bool RInterface::softCancelCommand (RCommand* command) {
510 	RK_TRACE (RBACKEND);
511 
512 	if (!(command->type () & RCommand::Running)) {
513 		cancelCommand (command);
514 	}
515 	return command->status & RCommand::Canceled;
516 }
517 
cancelCommand(RCommand * command)518 void RInterface::cancelCommand (RCommand *command) {
519 	RK_TRACE (RBACKEND);
520 
521 	if (!(command->type () & RCommand::Sync)) {
522 		command->status |= RCommand::Canceled;
523 		if (command->type () & RCommand::Running) {
524 			if ((RKDebugHandler::instance ()->state () == RKDebugHandler::InDebugPrompt) && (command == RKDebugHandler::instance ()->command ())) {
525 				RKDebugHandler::instance ()->sendCancel ();
526 			} else {
527 				RKRBackendProtocolFrontend::instance ()->interruptCommand (command->id ());
528 			}
529 		}
530 		RCommandStackModel::getModel ()->itemChange (command);
531 	} else {
532 		RK_ASSERT (false);
533 	}
534 }
535 
pauseProcessing(bool pause)536 void RInterface::pauseProcessing (bool pause) {
537 	RK_TRACE (RBACKEND);
538 
539 	if (pause) locked |= User;
540 	else locked -= locked & User;
541 }
542 
processPlainGenericRequest(const QStringList & calllist)543 QStringList RInterface::processPlainGenericRequest (const QStringList &calllist) {
544 	RK_TRACE (RBACKEND);
545 
546 	QString call = calllist.value (0);
547 	if (call == "get.tempfile.name") {
548 		RK_ASSERT (calllist.count () == 3);
549 		return (QStringList (RKCommonFunctions::getUseableRKWardSavefileName (calllist.value (1), calllist.value (2))));
550 	} else if (call == "set.output.file") {
551 		RK_ASSERT (calllist.count () == 2);
552 		RKOutputWindowManager::self ()->setCurrentOutputPath (calllist.value (1));
553 	} else if (call == "wdChange") {
554 		// in case of separate processes, apply new working directory in frontend, too.
555 		QDir::setCurrent (calllist.value (1));
556 		emit (backendWorkdirChanged());
557 	} else if (call == "highlightRCode") {
558 		return (QStringList (RKCommandHighlighter::commandToHTML (calllist.value (1))));
559 	} else if (call == "quit") {
560 		RKWardMainWindow::getMain ()->close ();
561 		// if we're still alive, quitting was cancelled
562 		return (QStringList ("FALSE"));
563 	} else if (call == "preLocaleChange") {
564 		int res = KMessageBox::warningContinueCancel (0, i18n ("A command in the R backend is trying to change the character encoding. While RKWard offers support for this, and will try to adjust to the new locale, this operation may cause subtle bugs, if data windows are currently open. Also the feature is not well tested, yet, and it may be advisable to save your workspace before proceeding.\nIf you have any data editor opened, or in any doubt, it is recommended to close those first (this will probably be auto-detected in later versions of RKWard). In this case, please choose 'Cancel' now, then close the data windows, save, and retry."), i18n ("Locale change"));
565 		if (res != KMessageBox::Continue) return (QStringList ("FALSE"));
566 	} else if (call == "listPlugins") {
567 		RK_ASSERT (calllist.count () == 1);
568 		return RKComponentMap::getMap ()->listPlugins ();
569 	} else if (call == "setPluginStatus") {
570 		QStringList params = calllist.mid (1);
571 		RK_ASSERT ((params.size () % 3) == 0);
572 		const int rows = params.size () / 3;
573 		QStringList ids = params.mid (0, rows);
574 		QStringList contexts = params.mid (rows, rows);
575 		QStringList visible = params.mid (rows*2, rows);
576 		RKComponentMap::getMap ()->setPluginStatus (ids, contexts, visible);
577 	} else if (call == "loadPluginMaps") {
578 		bool force = (calllist.value (1) == "force");
579 		bool reload = (calllist.value (2) == "reload");
580 		RKSettingsModulePlugins::registerPluginMaps (calllist.mid (3), force, reload);
581 	} else if (call == "updateInstalledPackagesList") {
582 		RKSessionVars::instance ()->setInstalledPackages (calllist.mid (1));
583 	} else if (call == "showHTML") {
584 		RK_ASSERT (calllist.count () == 2);
585 		RKWorkplace::mainWorkplace ()->openHelpWindow (QUrl::fromUserInput (calllist.value (1), QDir::currentPath (), QUrl::AssumeLocalFile));
586 	} else if (call == "select.list") {
587 		QString title = calllist.value (1);
588 		bool multiple = (calllist.value (2) == "multi");
589 		int num_preselects = calllist.value (3).toInt ();
590 		QStringList preselects = calllist.mid (4, num_preselects);
591 		QStringList choices = calllist.mid (4 + num_preselects);
592 
593 		QStringList results = RKSelectListDialog::doSelect (QApplication::activeWindow(), title, choices, preselects, multiple);
594 		if (results.isEmpty ()) results.append ("");	// R wants to have it that way
595 		return (results);
596 	} else if (call == "commandHistory") {
597 		if (calllist.value (1) == "get") {
598 			return (RKConsole::mainConsole ()->commandHistory ());
599 		} else {
600 			RKConsole::mainConsole ()->setCommandHistory (calllist.mid (2), calllist.value (1) == "append");
601 		}
602 	} else if (call == "getWorkspaceUrl") {
603 		QUrl url = RKWorkplace::mainWorkplace ()->workspaceURL ();
604 		if (!url.isEmpty ()) return (QStringList (url.url ()));
605 	} else if (call == "workplace.layout") {
606 		if (calllist.value (1) == "set") {
607 			if (calllist.value (2) == "close") RKWorkplace::mainWorkplace ()->closeAll ();
608 			QStringList list = calllist.mid (3);
609 			RKWorkplace::mainWorkplace ()->restoreWorkplace (list);
610 		} else {
611 			RK_ASSERT (calllist.value (1) == "get");
612 			return (RKWorkplace::mainWorkplace ()->makeWorkplaceDescription ());
613 		}
614 	} else if (call == "set.window.placement.hint") {
615 		RKWorkplace::mainWorkplace ()->setWindowPlacementOverrides (calllist.value (1), calllist.value (2), calllist.value (3));
616 	} else if (call == "getSessionInfo") {
617 		// Non-translatable on purpose. This is meant for posting to the bug tracker, mostly.
618 		QStringList lines ("-- Frontend --");
619 		lines.append (RKSessionVars::frontendSessionInfo ());
620 		lines.append (QString ());
621 		lines.append ("-- Backend --");
622 		lines.append ("Debug message file (this may contain relevant diagnostic output in case of trouble):");
623 		lines.append (calllist.value (1));
624 		lines.append (QString ());
625 		lines.append ("R version (compile time): " + calllist.value (2));
626 		return (lines);
627 	} else if (call == "recordCommands") {
628 		RK_ASSERT (calllist.count () == 3);
629 		QString filename = calllist.value (1);
630 		bool unfiltered = (calllist.value (2) == "include.all");
631 
632 		if (filename.isEmpty ()) {
633 			command_logfile_mode = NotRecordingCommands;
634 			command_logfile.close ();
635 		} else {
636 			if (command_logfile_mode != NotRecordingCommands) {
637 				return (QStringList ("Attempt to start recording, while already recording commands. Ignoring.)"));
638 			} else {
639 				command_logfile.setFileName (filename);
640 				bool ok = command_logfile.open (QIODevice::WriteOnly | QIODevice::Truncate);
641 				if (ok) {
642 					if (unfiltered) command_logfile_mode = RecordingCommandsUnfiltered;
643 					else command_logfile_mode = RecordingCommands;
644 				} else {
645 					return (QStringList ("Could not open file for writing. Not recording commands"));
646 				}
647 			}
648 		}
649 	} else if (call == "printPreview") {
650 		RKPrintAgent::printPostscript (calllist.value (1), true);
651 	} else if (call == "endBrowserContext") {
652 		RKDebugHandler::instance ()->endDebug ();
653 	} else if (call == "switchLanguage") {
654 		RKMessageCatalog::switchLanguage (calllist.value (1));
655 	} else {
656 		return (QStringList ("Error: unrecognized request '" + call + "'."));
657 	}
658 
659 	// for those calls which were recognized, but do not return anything
660 	return QStringList ();
661 }
662 
processHistoricalSubstackRequest(const QStringList & calllist,RCommand * parent_command)663 void RInterface::processHistoricalSubstackRequest (const QStringList &calllist, RCommand *parent_command) {
664 	RK_TRACE (RBACKEND);
665 
666 	RCommandChain *in_chain;
667 	if (!parent_command) {
668 		// This can happen for Tcl events. Create a dummy command on the stack to keep things looping.
669 		parent_command = new RCommand (QString (), RCommand::App | RCommand::EmptyCommand | RCommand::Sync);
670 		RCommandStack::issueCommand (parent_command, 0);
671 		all_current_commands.append (parent_command);
672 		dummy_command_on_stack = parent_command;	// so we can get rid of it again, after it's sub-commands have finished
673 	}
674 	in_chain = openSubcommandChain (parent_command);
675 	RK_DEBUG (RBACKEND, DL_DEBUG, "started sub-command chain (%p) for command %s", in_chain, qPrintable (parent_command->command ()));
676 
677 	QString call = calllist.value (0);
678 	if (call == "sync") {
679 		RK_ASSERT (calllist.count () >= 2);
680 
681 		for (int i = 1; i < calllist.count (); ++i) {
682 			QString object_name = calllist[i];
683 			RObject *obj = RObjectList::getObjectList ()->findObject (object_name);
684 			if (obj) {
685 				RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update for symbol %s", object_name.toLatin1 ().data());
686 				obj->markDataDirty ();
687 				obj->updateFromR (in_chain);
688 			} else {
689 				RK_DEBUG (RBACKEND, DL_WARNING, "lookup failed for changed symbol %s", object_name.toLatin1 ().data());
690 			}
691 		}
692 	} else if (call == "syncenvs") {
693 		RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update of object list");
694 		int search_len = calllist.value (1).toInt ();
695 		RObjectList::getObjectList ()->updateFromR (in_chain, calllist.mid (2, search_len), calllist.mid (2 + search_len));
696 	} else if (call == "syncglobal") {
697 		RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update of globalenv");
698 		RObjectList::getGlobalEnv ()->updateFromR (in_chain, calllist.mid (1));
699 #ifndef DISABLE_RKWINDOWCATCHER
700 	// NOTE: WARNING: When converting these to PlainGenericRequests, the occasional "error, figure margins too large" starts coming up, again. Not sure, why.
701  	} else if (call == "startOpenX11") {
702 		RK_ASSERT (calllist.count () == 2);
703 		RKWindowCatcher::instance ()->start (calllist.value (1).toInt ());
704  	} else if (call == "endOpenX11") {
705 		RK_ASSERT (calllist.count () == 2);
706 		RKWindowCatcher::instance ()->stop (calllist.value (1).toInt ());
707 	} else if (call == "updateDeviceHistory") {
708 		if (calllist.count () >= 2) {
709 			RKWindowCatcher::instance ()->updateHistory (calllist.mid (1));
710 		}
711 	} else if (call == "killDevice") {
712 		RK_ASSERT (calllist.count () == 2);
713 		RKWindowCatcher::instance ()->killDevice (calllist.value (1).toInt ());
714 #endif // DISABLE_RKWINDOWCATCHER
715 	} else if (call == "edit") {
716 		RK_ASSERT (calllist.count () >= 2);
717 
718 		QStringList object_list = calllist.mid (1);
719 		new RKEditObjectAgent (object_list, in_chain);
720 	} else if (call == "require") {
721 		if (calllist.count () >= 2) {
722 			QString lib_name = calllist[1];
723 			KMessageBox::information (0, i18n ("The R-backend has indicated that in order to carry out the current task it needs the package '%1', which is not currently installed. We will open the package-management tool, and there you can try to locate and install the needed package.", lib_name), i18n ("Require package '%1'", lib_name));
724 			RKLoadLibsDialog::showInstallPackagesModal (0, in_chain, lib_name);
725 			issueCommand (".rk.set.reply (\"\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
726 		} else {
727 			issueCommand (".rk.set.reply (\"Too few arguments in call to require.\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
728 		}
729 	} else if (call == "doPlugin") {
730 		if (calllist.count () >= 3) {
731 			QString message;
732 			bool ok;
733 			RKComponentMap::ComponentInvocationMode mode = RKComponentMap::ManualSubmit;
734 			if (calllist[2] == "auto") mode = RKComponentMap::AutoSubmit;
735 			else if (calllist[2] == "submit") mode = RKComponentMap::AutoSubmitOrFail;
736 			ok = RKComponentMap::invokeComponent (calllist[1], calllist.mid (3), mode, &message, in_chain);
737 
738 			if (!message.isEmpty ()) {
739 				QString type = "warning";
740 				if (!ok) type = "error";
741 				issueCommand (".rk.set.reply (list (type=\"" + type + "\", message=\"" + RKCommonFunctions::escape (message) + "\"))", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
742 			}
743 		} else {
744 			RK_ASSERT (false);
745 		}
746 	} else {
747 		issueCommand ("stop (\"Unrecognized call '" + call + "'. Ignoring\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
748 	}
749 
750 	closeChain (in_chain);
751 }
752 
addButtonToBox(QDialog * dialog,QDialogButtonBox * box,QDialogButtonBox::StandardButton which,const QString & text,const QString & def_text,bool is_default)753 int addButtonToBox (QDialog *dialog, QDialogButtonBox *box, QDialogButtonBox::StandardButton which, const QString &text, const QString &def_text, bool is_default) {
754 	if (text.isEmpty ()) return 0;
755 	QPushButton *button = box->addButton (which);
756 	if (text != def_text) button->setText (text);
757 	if (is_default) button->setDefault (true);
758 	QObject::connect (button, &QPushButton::clicked, [dialog, which]() { dialog->done (which); });
759 	return 1;
760 }
761 
processRBackendRequest(RBackendRequest * request)762 void RInterface::processRBackendRequest (RBackendRequest *request) {
763 	RK_TRACE (RBACKEND);
764 
765 	// first, copy out the type. Allows for easier typing below
766 	RBackendRequest::RCallbackType type = request->type;
767 
768 	if (type == RBackendRequest::CommandLineIn) {
769 		int id = request->params["commandid"].toInt ();
770 		RCommand *command = all_current_commands.value (0, 0);	// User command will always be the first.
771 		if ((command == 0) || (command->id () != id)) {
772 			RK_ASSERT (false);
773 		} else {
774 			command->commandLineIn ();
775 		}
776 	} else if (type == RBackendRequest::ShowMessage) {
777 		QString caption = request->params["caption"].toString ();
778 		QString message = request->params["message"].toString ();
779 		QString button_yes = request->params["button_yes"].toString ();
780 		QString button_no = request->params["button_no"].toString ();
781 		QString button_cancel = request->params["button_cancel"].toString ();
782 		QString def_button = request->params["default"].toString ();
783 
784 		// NOTE: In order to support non-modal (information) dialogs, we cannot use KMessageBox or QMessgaeBox, below.
785 		QDialog* dialog = new QDialog ();
786 		dialog->setResult (-1);  // We use this to stand for cancelled
787 		QDialogButtonBox *button_box = new QDialogButtonBox (dialog);
788 		QPushButton *button;
789 		int button_count = 0;
790 		button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Yes, button_yes, "yes", def_button == button_yes);
791 		button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::No, button_no, "no", def_button == button_no);
792 		button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Cancel, button_cancel, "cancel", def_button == button_cancel);
793 		if (!button_count) { // cannot have no button defined at all
794 			button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Ok, "ok", "ok", true);
795 		}
796 
797 		bool synchronous = request->synchronous || (button_count > 1);
798 		KMessageBox::createKMessageBox (dialog, button_box, button_count < 2 ? QMessageBox::Information : QMessageBox::Question, message, QStringList (), QString (), 0, KMessageBox::Notify | KMessageBox::NoExec);
799 		dialog->setWindowTitle (caption);
800 
801 		if (!synchronous) {
802 			dialog->setAttribute (Qt::WA_DeleteOnClose);
803 			dialog->show();
804 
805 			RKRBackendProtocolFrontend::setRequestCompleted (request);
806 			return;
807 		} else {
808 			int result = dialog->exec ();
809 			QString result_string;
810 			if (result == QDialogButtonBox::Yes || result == QDialogButtonBox::Ok) result_string = "yes";
811 			else if (result == QDialogButtonBox::No) result_string = "no";
812 			else result_string = "cancel";
813 			request->params["result"] = result_string;
814 			delete dialog;
815 		}
816 	} else if (type == RBackendRequest::ReadLine) {
817 		QString result;
818 
819 		// yes, readline *can* be called outside of a current command (e.g. from tcl/tk)
820 		bool dummy_command = false;
821 		RCommand *command = runningCommand ();
822 		if (!command) {
823 			command = new RCommand ("", RCommand::EmptyCommand);
824 			dummy_command = true;
825 		}
826 
827 
828 		bool ok = RKReadLineDialog::readLine (0, i18n ("R backend requests information"), request->params["prompt"].toString (), command, &result);
829 		request->params["result"] = QVariant (result);
830 
831 		if (dummy_command) delete command;
832 		if (!ok) request->params["cancelled"] = QVariant (true);
833 	} else if (type == RBackendRequest::Debugger) {
834 		RKDebugHandler::instance ()->debugCall (request, runningCommand ());
835 		return;		// request will be closed by the debug handler
836 	} else if ((type == RBackendRequest::ShowFiles) || (type == RBackendRequest::EditFiles)) {
837 		ShowEditTextFileAgent::showEditFiles (request);
838 		return;		// we are not done, yet!
839 	} else if (type == RBackendRequest::ChooseFile) {
840 		QString filename;
841 		if (request->params["new"].toBool ()) {
842 			filename = QFileDialog::getSaveFileName ();
843 		} else {
844 			filename = QFileDialog::getOpenFileName ();
845 		}
846 		request->params["result"] = QVariant (filename);
847 	} else if (type == RBackendRequest::SetParamsFromBackend) {
848 			na_real = request->params["na_real"].toDouble ();
849 			na_int = request->params["na_int"].toInt ();
850 	} else if (type == RBackendRequest::BackendExit) {
851 		if (request->params.value ("regular", QVariant (false)).toBool ()) backend_dead = true;		// regular exit via QuitCommand
852 		if (!backend_dead) {
853 			backend_dead = true;
854 			QString message = request->params["message"].toString ();
855 			message += i18n ("\nThe R backend will be shut down immediately. This means, you can not use any more functions that rely on it. I.e. you can do hardly anything at all, not even save the workspace (but if you're lucky, R already did that). What you can do, however, is save any open command-files, the output, or copy data out of open data editors. Quit RKWard after that. Sorry!");
856 			RKErrorDialog::reportableErrorMessage (0, message, QString (), i18n ("R engine has died"), "r_engine_has_died");
857 		}
858 	} else {
859 		RK_ASSERT (false);
860 	}
861 
862 	RKRBackendProtocolFrontend::setRequestCompleted (request);
863 }
864 
865