1 /*
2  * Copyright (C) 2019-2021 Ashar Khan <ashar786khan@gmail.com>
3  *
4  * This file is part of CP Editor.
5  *
6  * CP Editor 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 3 of the License, or
9  * (at your option) any later version.
10  *
11  * I will not be responsible if CP Editor behaves in unexpected way and
12  * causes your ratings to go down and or lose any important contest.
13  *
14  * Believe Software is "Software" and it isn't immune to bugs.
15  *
16  */
17 
18 #include "Core/Runner.hpp"
19 #include "Core/Compiler.hpp"
20 #include "Core/EventLogger.hpp"
21 #include <QElapsedTimer>
22 #include <QFileInfo>
23 #include <QTimer>
24 #include <generated/SettingsHelper.hpp>
25 
26 namespace Core
27 {
28 
Runner(int index)29 Runner::Runner(int index) : runnerIndex(index)
30 {
31     runProcess = new QProcess();
32     connect(runProcess, &QProcess::started, this, &Runner::onStarted);
33     connect(runProcess, &QProcess::errorOccurred, this, &Runner::onErrorOccurred);
34 }
35 
~Runner()36 Runner::~Runner()
37 {
38     // The order of destructions is important, runTimer is used when emitting signals
39 
40     delete killTimer;
41 
42     if (runProcess != nullptr)
43     {
44         if (runProcess->state() == QProcess::Running)
45         {
46             // Kill the process if it's still running when the Runner is destructed
47             LOG_WARN("Runner at index:" << runnerIndex << " was running and forcefully killed");
48             runProcess->kill();
49             emit runKilled(runnerIndex);
50         }
51         delete runProcess;
52     }
53 
54     delete runTimer;
55 }
56 
run(const QString & tmpFilePath,const QString & sourceFilePath,const QString & lang,const QString & runCommand,const QString & args,const QString & input,int timeLimit)57 void Runner::run(const QString &tmpFilePath, const QString &sourceFilePath, const QString &lang,
58                  const QString &runCommand, const QString &args, const QString &input, int timeLimit)
59 {
60     LOG_INFO(INFO_OF(tmpFilePath) << INFO_OF(sourceFilePath) << INFO_OF(lang) << INFO_OF(runCommand) << INFO_OF(args)
61                                   << INFO_OF(timeLimit));
62 
63     isDetachedRun = false;
64 
65     if (!QFile::exists(tmpFilePath)) // make sure the source file exists, this usually means the executable file exists
66     {
67         emit failedToStartRun(runnerIndex, tr("The source file %1 doesn't exist.").arg(tmpFilePath));
68         return;
69     }
70 
71     // get the command for execution
72     QStringList command = QProcess::splitCommand(getCommand(tmpFilePath, sourceFilePath, lang, runCommand, args));
73     if (command.isEmpty())
74     {
75         emit failedToStartRun(runnerIndex, tr("Failed to get run command. It's probably a bug."));
76         return;
77     }
78 
79     // connect signals and set timers
80 
81     connect(runProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, &Runner::onFinished);
82     connect(runProcess, &QProcess::readyReadStandardOutput, this, &Runner::onReadyReadStandardOutput);
83     connect(runProcess, &QProcess::readyReadStandardError, this, &Runner::onReadyReadStandardError);
84 
85     killTimer = new QTimer(runProcess);
86     killTimer->setSingleShot(true);
87     killTimer->setInterval(timeLimit);
88     connect(killTimer, &QTimer::timeout, this, &Runner::onTimeout);
89 
90     runTimer = new QElapsedTimer();
91 
92     killTimer->start();
93     runTimer->start();
94 
95     QString program = command.takeFirst();
96 
97     setWorkingDirectory(tmpFilePath, sourceFilePath, lang);
98 
99     processInput = input;
100 
101     runProcess->start(program, command);
102 }
103 
runDetached(const QString & tmpFilePath,const QString & sourceFilePath,const QString & lang,const QString & runCommand,const QString & args)104 void Runner::runDetached(const QString &tmpFilePath, const QString &sourceFilePath, const QString &lang,
105                          const QString &runCommand, const QString &args)
106 {
107     isDetachedRun = true;
108 
109     setWorkingDirectory(tmpFilePath, sourceFilePath, lang);
110 
111     // different steps on different OSs
112 #if defined(Q_OS_MACOS)
113     // use apple script on Mac OS
114     runProcess->setProgram("osascript");
115     runProcess->setArguments({"-l", "AppleScript"});
116     QString script = R"(tell app "Terminal" to do script ")" +
117                      getCommand(tmpFilePath, sourceFilePath, lang, runCommand, args).replace("\"", "'") + "\"";
118     runProcess->start();
119     LOG_INFO("Running apple script\n" << script);
120     runProcess->write(script.toUtf8());
121     runProcess->closeWriteChannel();
122 #elif defined(Q_OS_WIN)
123     // use cmd on Windows
124     runProcess->start("cmd", QProcess::splitCommand(
125                                  "/C \"start cmd /C " +
126                                  getCommand(tmpFilePath, sourceFilePath, lang, runCommand, args).replace("\"", "^\"") +
127                                  " ^& pause\""));
128     LOG_INFO("CMD Arguemnts " << runProcess->arguments().join(" "));
129 #elif defined(Q_OS_UNIX)
130     auto terminal = SettingsHelper::getDetachedRunTerminalProgram();
131     LOG_INFO("Using: " << terminal << " on UNIX");
132     auto quotedCommand = getCommand(tmpFilePath, sourceFilePath, lang, runCommand, args);
133     auto execArgs = QProcess::splitCommand(SettingsHelper::getDetachedRunTerminalArguments()) +
134                     QStringList{"/bin/bash", "-c",
135                                 QStringLiteral("%1 ; echo \"\n%2\" ; read -n 1")
136                                     .arg(quotedCommand)
137                                     .arg(tr("Program finished with exit code %1\nPress any key to exit").arg("$?"))};
138     runProcess->start(terminal, execArgs);
139 #else
140     emit failedToStartRun(runnerIndex, tr("Detached execution is not supported on your platform"));
141 #endif
142 }
143 
onFinished(int exitCode,QProcess::ExitStatus exitStatus)144 void Runner::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
145 {
146     emit runFinished(runnerIndex, processStdout + runProcess->readAllStandardOutput(),
147                      processStderr + runProcess->readAllStandardError(), exitCode, runTimer->elapsed(),
148                      timeLimitExceeded);
149 }
150 
onStarted()151 void Runner::onStarted()
152 {
153     if (!isDetachedRun)
154     {
155         runProcess->write(processInput.toUtf8());
156         runProcess->closeWriteChannel();
157     }
158     emit runStarted(runnerIndex);
159 }
160 
onTimeout()161 void Runner::onTimeout()
162 {
163     if (runProcess->state() == QProcess::Running)
164     {
165         LOG_INFO("Process was running, and forcefully killed it because time limit was reached");
166         timeLimitExceeded = true;
167         runProcess->kill();
168     }
169 }
170 
onReadyReadStandardOutput()171 void Runner::onReadyReadStandardOutput()
172 {
173     processStdout.append(runProcess->readAllStandardOutput());
174     if (!outputLimitExceededEmitted && processStdout.length() > SettingsHelper::getOutputLengthLimit())
175     {
176         outputLimitExceededEmitted = true;
177         runProcess->kill();
178         LOG_INFO("Process was running, and forcefully killed it because stdout limit was reached");
179         emit runOutputLimitExceeded(runnerIndex, "stdout");
180     }
181 }
182 
onReadyReadStandardError()183 void Runner::onReadyReadStandardError()
184 {
185     processStderr.append(runProcess->readAllStandardError());
186     if (!outputLimitExceededEmitted && processStderr.length() > SettingsHelper::getOutputLengthLimit())
187     {
188         outputLimitExceededEmitted = true;
189         runProcess->kill();
190         LOG_INFO("Process was running, and forcefully killed it because stderr limit was reached");
191         emit runOutputLimitExceeded(runnerIndex, "stderr");
192     }
193 }
194 
onErrorOccurred(QProcess::ProcessError error)195 void Runner::onErrorOccurred(QProcess::ProcessError error)
196 {
197     if (error == QProcess::FailedToStart)
198     {
199         if (isDetachedRun)
200         {
201             emit failedToStartRun(
202                 runnerIndex,
203                 tr("Failed to start detached execution. Please check your terminal emulator settings in %1.")
204                     .arg(SettingsHelper::pathOfDetachedRunTerminalProgram(true)));
205         }
206         else
207         {
208             emit failedToStartRun(runnerIndex, tr("Failed to start running. Please compile first."));
209         }
210     }
211 }
212 
getCommand(const QString & tmpFilePath,const QString & sourceFilePath,const QString & lang,const QString & runCommand,const QString & args)213 QString Runner::getCommand(const QString &tmpFilePath, const QString &sourceFilePath, const QString &lang,
214                            const QString &runCommand, const QString &args)
215 {
216     // get the execution command by the file path and the language
217     // please remember to add quotes for the paths
218 
219     QString res;
220 
221     if (lang == "C++")
222     {
223         res = QString("\"%1\" %2").arg(Compiler::outputPath(tmpFilePath, sourceFilePath, "C++")).arg(args);
224     }
225     else if (lang == "Java")
226     {
227         res = QString("%1 -classpath \"%2\" %3 %4")
228                   .arg(runCommand)
229                   .arg(Compiler::outputPath(tmpFilePath, sourceFilePath, "Java"))
230                   .arg(SettingsHelper::getJavaClassName())
231                   .arg(args);
232     }
233     else if (lang == "Python")
234     {
235         res = QString("%1 \"%2\" %3").arg(runCommand).arg(QFileInfo(tmpFilePath).canonicalFilePath()).arg(args);
236     }
237 
238     LOG_INFO("Returning runCommand as : " << res);
239 
240     return res;
241 }
242 
setWorkingDirectory(const QString & tmpFilePath,const QString & sourceFilePath,const QString & lang)243 void Runner::setWorkingDirectory(const QString &tmpFilePath, const QString &sourceFilePath, const QString &lang)
244 {
245     runProcess->setWorkingDirectory(
246         QFileInfo(Compiler::outputFilePath(tmpFilePath, sourceFilePath, lang, false)).path());
247 }
248 
249 } // namespace Core
250