1 /*
2 SuperCollider Qt IDE
3 Copyright (c) 2012 Jakob Leben & Tim Blechmann
4 http://www.audiosynth.com
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 #include <QBuffer>
22 #include <QCoreApplication>
23 #include <QtCore/QFuture>
24 #include <QtCore/QFutureWatcher>
25 #include <QTextDocumentFragment>
26
27 #include <QtConcurrent>
28
29 #include "main.hpp"
30 #include "main_window.hpp"
31 #include "sc_introspection.hpp"
32 #include "sc_process.hpp"
33 #include "sc_server.hpp"
34 #include "settings/manager.hpp"
35 #include "util/standard_dirs.hpp"
36 #include "../primitives/localsocket_utils.hpp"
37
38 #include <yaml-cpp/node/node.h>
39 #include <yaml-cpp/parser.h>
40
41 namespace ScIDE {
42
ScProcess(Settings::Manager * settings,QObject * parent)43 ScProcess::ScProcess(Settings::Manager* settings, QObject* parent):
44 QProcess(parent),
45 mIpcServer(new QLocalServer(this)),
46 mIpcSocket(NULL),
47 mIpcServerName("SCIde_" + QString::number(QCoreApplication::applicationPid())),
48 mTerminationRequested(false),
49 mCompiled(false) {
50 prepareActions(settings);
51
52 connect(this, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
53 connect(mIpcServer, SIGNAL(newConnection()), this, SLOT(onNewIpcConnection()));
54 connect(this, SIGNAL(stateChanged(QProcess::ProcessState)), this,
55 SLOT(onProcessStateChanged(QProcess::ProcessState)));
56 }
57
prepareActions(Settings::Manager * settings)58 void ScProcess::prepareActions(Settings::Manager* settings) {
59 QAction* action;
60
61 const QString interpreterCategory(tr("Interpreter"));
62
63 mActions[ToggleRunning] = action = new QAction(tr("Boot or Quit Interpreter"), this);
64 // the default QAction::TextHeuristicRole incorrectly detects a quit role on macOS
65 action->setMenuRole(QAction::NoRole);
66 connect(action, SIGNAL(triggered()), this, SLOT(toggleRunning()));
67 // settings->addAction( action, "interpreter-toggle-running", interpreterCategory);
68
69 mActions[Start] = action = new QAction(QIcon::fromTheme("system-run"), tr("Boot Interpreter"), this);
70 connect(action, SIGNAL(triggered()), this, SLOT(startLanguage()));
71 settings->addAction(action, "interpreter-start", interpreterCategory);
72
73 mActions[Stop] = action = new QAction(QIcon::fromTheme("system-shutdown"), tr("Quit Interpreter"), this);
74 connect(action, SIGNAL(triggered()), this, SLOT(stopLanguage()));
75 settings->addAction(action, "interpreter-stop", interpreterCategory);
76
77 mActions[Restart] = action = new QAction(QIcon::fromTheme("system-reboot"), tr("Reboot Interpreter"), this);
78 connect(action, SIGNAL(triggered()), this, SLOT(restartLanguage()));
79 settings->addAction(action, "interpreter-restart", interpreterCategory);
80
81 mActions[RecompileClassLibrary] = action =
82 new QAction(QIcon::fromTheme("system-reboot"), tr("Recompile Class Library"), this);
83 action->setShortcut(tr("Ctrl+Shift+l", "Recompile Class Library)"));
84 connect(action, SIGNAL(triggered()), this, SLOT(recompileClassLibrary()));
85 settings->addAction(action, "interpreter-recompile-lib", interpreterCategory);
86
87 mActions[StopMain] = action = new QAction(QIcon::fromTheme("media-playback-stop"), tr("Stop"), this);
88 action->setShortcut(tr("Ctrl+.", "Stop (a.k.a. cmd-period)"));
89 action->setShortcutContext(Qt::ApplicationShortcut);
90 connect(action, SIGNAL(triggered()), this, SLOT(stopMain()));
91 settings->addAction(action, "interpreter-main-stop", interpreterCategory);
92
93 mActions[ShowQuarks] = action = new QAction(tr("Quarks"), this);
94 connect(action, SIGNAL(triggered()), this, SLOT(showQuarks()));
95 settings->addAction(action, "interpreter-show-quarks-gui", interpreterCategory);
96
97 connect(mActions[Start], SIGNAL(changed()), this, SLOT(updateToggleRunningAction()));
98 connect(mActions[Stop], SIGNAL(changed()), this, SLOT(updateToggleRunningAction()));
99
100 onProcessStateChanged(QProcess::NotRunning);
101 }
102
updateToggleRunningAction()103 void ScProcess::updateToggleRunningAction() {
104 QAction* targetAction = state() == QProcess::NotRunning ? mActions[Start] : mActions[Stop];
105
106 mActions[ToggleRunning]->setText(targetAction->text());
107 mActions[ToggleRunning]->setIcon(targetAction->icon());
108 mActions[ToggleRunning]->setShortcut(targetAction->shortcut());
109 }
110
toggleRunning()111 void ScProcess::toggleRunning() {
112 switch (state()) {
113 case NotRunning:
114 startLanguage();
115 break;
116 default:
117 stopLanguage();
118 }
119 }
120
startLanguage(void)121 void ScProcess::startLanguage(void) {
122 if (state() != QProcess::NotRunning) {
123 statusMessage(tr("Interpreter is already running."));
124 return;
125 }
126
127 Settings::Manager* settings = Main::settings();
128 settings->beginGroup("IDE/interpreter");
129
130 QString workingDirectory = settings->value("runtimeDir").toString();
131 QString configFile = settings->value("configFile").toString();
132 bool standalone = settings->value("standalone").toBool();
133
134 settings->endGroup();
135
136 QString sclangCommand;
137 #ifdef Q_OS_MAC
138 sclangCommand = standardDirectory(ScResourceDir) + "/../MacOS/sclang";
139 #else
140 sclangCommand = "sclang";
141 #endif
142
143 QStringList sclangArguments;
144 if (!configFile.isEmpty())
145 sclangArguments << "-l" << configFile;
146 sclangArguments << "-i"
147 << "scqt";
148 if (standalone)
149 sclangArguments << "-a";
150
151 if (!workingDirectory.isEmpty())
152 setWorkingDirectory(workingDirectory);
153
154 QProcess::start(sclangCommand, sclangArguments);
155 bool processStarted = QProcess::waitForStarted();
156 if (!processStarted)
157 emit statusMessage(tr("Failed to start interpreter!"));
158 }
159
recompileClassLibrary(void)160 void ScProcess::recompileClassLibrary(void) {
161 if (state() != QProcess::Running) {
162 emit statusMessage(tr("Interpreter is not running!"));
163 return;
164 }
165 mCompiled = false;
166 write("\x18");
167 }
168
169
stopLanguage(void)170 void ScProcess::stopLanguage(void) {
171 if (state() != QProcess::Running) {
172 emit statusMessage(tr("Interpreter is not running!"));
173 return;
174 }
175
176 evaluateCode("0.exit", true);
177 mCompiled = false;
178 mTerminationRequested = true;
179 mTerminationRequestTime = QDateTime::currentDateTimeUtc();
180
181 bool finished = waitForFinished(1000);
182 if (!finished && (state() != QProcess::NotRunning)) {
183 terminate();
184 bool reallyFinished = waitForFinished(200);
185 if (!reallyFinished)
186 emit statusMessage(tr("Failed to stop interpreter!"));
187 }
188 closeWriteChannel();
189 mTerminationRequested = false;
190 }
191
restartLanguage()192 void ScProcess::restartLanguage() {
193 mCompiled = false;
194 stopLanguage();
195 startLanguage();
196 }
197
stopMain(void)198 void ScProcess::stopMain(void) { evaluateCode("thisProcess.stop", true); }
199
showQuarks(void)200 void ScProcess::showQuarks(void) { evaluateCode("Quarks.gui", true); }
201
202
onReadyRead(void)203 void ScProcess::onReadyRead(void) {
204 if (mTerminationRequested) {
205 // when stopping the language, we don't want to post for longer than 200 ms to prevent the UI to freeze
206 if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - mTerminationRequestTime.toMSecsSinceEpoch() > 200)
207 return;
208 }
209
210 QByteArray out = QProcess::readAll();
211 QString postString = QString::fromUtf8(out);
212 emit scPost(postString);
213 }
214
evaluateCode(QString const & commandString,bool silent)215 void ScProcess::evaluateCode(QString const& commandString, bool silent) {
216 if (state() != QProcess::Running) {
217 emit statusMessage(tr("Interpreter is not running!"));
218 return;
219 }
220
221 QByteArray bytesToWrite = commandString.toUtf8();
222 size_t writtenBytes = write(bytesToWrite);
223 if (writtenBytes != bytesToWrite.size()) {
224 emit statusMessage(tr("Error when passing data to interpreter!"));
225 return;
226 }
227
228 char commandChar = silent ? '\x1b' : '\x0c';
229
230 write(&commandChar, 1);
231 }
232
onNewIpcConnection()233 void ScProcess::onNewIpcConnection() {
234 if (mIpcSocket)
235 // we can handle only one ipc connection at a time
236 mIpcSocket->disconnect();
237
238 mIpcSocket = mIpcServer->nextPendingConnection();
239 connect(mIpcSocket, SIGNAL(disconnected()), this, SLOT(finalizeConnection()));
240 connect(mIpcSocket, SIGNAL(readyRead()), this, SLOT(onIpcData()));
241 }
242
finalizeConnection()243 void ScProcess::finalizeConnection() {
244 mIpcData.clear();
245 mIpcSocket->deleteLater();
246 mIpcSocket = NULL;
247 }
248
onProcessStateChanged(QProcess::ProcessState state)249 void ScProcess::onProcessStateChanged(QProcess::ProcessState state) {
250 switch (state) {
251 case QProcess::Starting:
252 mActions[Start]->setEnabled(false);
253 mActions[Stop]->setEnabled(true);
254 mActions[Restart]->setEnabled(true);
255 updateToggleRunningAction();
256
257 break;
258
259 case QProcess::Running:
260 mActions[StopMain]->setEnabled(true);
261 mActions[ShowQuarks]->setEnabled(true);
262 mActions[RecompileClassLibrary]->setEnabled(true);
263
264 onStart();
265
266 break;
267
268 case QProcess::NotRunning:
269 mActions[Start]->setEnabled(true);
270 mActions[Stop]->setEnabled(false);
271 mActions[Restart]->setEnabled(false);
272 mActions[StopMain]->setEnabled(false);
273 mActions[ShowQuarks]->setEnabled(false);
274 mActions[RecompileClassLibrary]->setEnabled(false);
275 updateToggleRunningAction();
276 postQuitNotification();
277 mCompiled = false;
278 break;
279 }
280 }
281
postQuitNotification()282 void ScProcess::postQuitNotification() {
283 QString message;
284 switch (exitStatus()) {
285 case QProcess::CrashExit:
286 message = tr("Interpreter has crashed or stopped forcefully. [Exit code: %1]\n").arg(exitCode());
287 break;
288 default:
289 message = tr("Interpreter has quit. [Exit code: %1]\n").arg(exitCode());
290 }
291 emit scPost(message);
292 }
293
294
onIpcData()295 void ScProcess::onIpcData() {
296 mIpcData.append(mIpcSocket->readAll());
297 // After we have put the data in the buffer, process it
298 int avail = mIpcData.length();
299 do {
300 if (mReadSize == 0 && avail > 4) {
301 mReadSize = ArrayToInt(mIpcData.left(4));
302 mIpcData.remove(0, 4);
303 avail -= 4;
304 }
305
306 if (mReadSize > 0 && avail >= mReadSize) {
307 QByteArray baReceived(mIpcData.left(mReadSize));
308 mIpcData.remove(0, mReadSize);
309 mReadSize = 0;
310 avail -= mReadSize;
311
312 QDataStream in(baReceived);
313 in.setVersion(QDataStream::Qt_4_6);
314 QString selector, message;
315 in >> selector;
316 if (in.status() != QDataStream::Ok)
317 return;
318
319 in >> message;
320 if (in.status() != QDataStream::Ok)
321 return;
322
323 onResponse(selector, message);
324 emit response(selector, message);
325 }
326 } while ((mReadSize == 0 && avail > 4) || (mReadSize > 0 && avail > mReadSize));
327 }
328
onResponse(const QString & selector,const QString & data)329 void ScProcess::onResponse(const QString& selector, const QString& data) {
330 if (selector == QStringLiteral("introspection")) {
331 using ScLanguage::Introspection;
332
333 auto watcher = new QFutureWatcher<Introspection>(this);
334 connect(watcher, &QFutureWatcher<Introspection>::finished, [=] {
335 try {
336 Introspection newIntrospection = watcher->result();
337 mIntrospection = std::move(newIntrospection);
338 emit introspectionChanged();
339 } catch (std::exception& e) {
340 MainWindow::instance()->showStatusMessage(e.what());
341 }
342 watcher->deleteLater();
343 });
344
345 // Start the computation.
346 QFuture<Introspection> future =
347 QtConcurrent::run([](QString data) { return ScLanguage::Introspection(data); }, data);
348 watcher->setFuture(future);
349 }
350
351 else if (selector == QStringLiteral("classLibraryRecompiled")) {
352 mCompiled = true;
353 emit classLibraryRecompiled();
354 }
355
356 else if (selector == QStringLiteral("requestCurrentPath"))
357 Main::documentManager()->sendActiveDocument();
358 }
359
onStart()360 void ScProcess::onStart() {
361 if (!mIpcServer->isListening()) // avoid a warning on stderr
362 mIpcServer->listen(mIpcServerName);
363
364 QString command = QStringLiteral("ScIDE.connect(\"%1\")").arg(mIpcServerName);
365 evaluateCode(command, true);
366 Main::documentManager()->sendActiveDocument();
367 }
368
369
updateTextMirrorForDocument(Document * doc,int position,int charsRemoved,int charsAdded)370 void ScProcess::updateTextMirrorForDocument(Document* doc, int position, int charsRemoved, int charsAdded) {
371 if (!mIpcSocket)
372 return;
373
374 if (mIpcSocket->state() != QLocalSocket::ConnectedState)
375 return;
376
377 QVariantList argList;
378 argList.append(QVariant(doc->id()));
379 argList.append(QVariant(position));
380 argList.append(QVariant(charsRemoved));
381
382 QTextCursor cursor = QTextCursor(doc->textDocument());
383 cursor.setPosition(position, QTextCursor::MoveAnchor);
384 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, charsAdded);
385 argList.append(QVariant(cursor.selection().toPlainText()));
386
387 try {
388 sendSelectorAndData(mIpcSocket, QStringLiteral("updateDocText"), argList);
389 } catch (std::exception const& e) {
390 scPost(QStringLiteral("Exception during ScIDE_Send: %1\n").arg(e.what()));
391 }
392 }
393
updateSelectionMirrorForDocument(Document * doc,int start,int range)394 void ScProcess::updateSelectionMirrorForDocument(Document* doc, int start, int range) {
395 if (!mIpcSocket)
396 return;
397
398 if (mIpcSocket->state() != QLocalSocket::ConnectedState)
399 return;
400
401 QVariantList argList;
402 argList.append(QVariant(doc->id()));
403 argList.append(QVariant(start));
404 argList.append(QVariant(range));
405
406
407 try {
408 sendSelectorAndData(mIpcSocket, QStringLiteral("updateDocSelection"), argList);
409 } catch (std::exception const& e) {
410 scPost(QStringLiteral("Exception during ScIDE_Send: %1\n").arg(e.what()));
411 }
412 }
413
414 } // namespace ScIDE
415