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