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