1 #include <qglobal.h>
2 
3 #if QT_VERSION < 0x050000
4 #include <QApplication>
5 #else
6 #include <QGuiApplication>
7 #endif
8 #include <QXmlStreamReader>
9 #include <QFileInfo>
10 #include <QStringList>
11 
12 #include <algorithm> // std::sort()
13 
14 #include "processmanager.h"
15 #include "arcadesettings.h"
16 #include "tweakedqmlappviewer.h"
17 #include "macros.h"
18 
19 extern ArcadeSettings *globalConfig;
20 
ProcessManager(QObject * parent)21 ProcessManager::ProcessManager(QObject *parent) :
22 	QObject(parent)
23 {
24 	mCurrentProcessId = 0;
25 }
26 
~ProcessManager()27 ProcessManager::~ProcessManager()
28 {
29 	foreach (QProcess *proc, mProcessMap) {
30 		proc->terminate();
31 		proc->waitForFinished(500);
32 		if ( proc->state() == QProcess::Running ) {
33 			proc->kill();
34 			proc->waitForFinished(250);
35 		}
36 	}
37 }
38 
startEmulator(QString id)39 int ProcessManager::startEmulator(QString id)
40 {
41 	if ( id.isEmpty() )
42 		return -1;
43 	else {
44 		QProcess *proc = new QProcess(this);
45 		QStringList args;
46 
47 		foreach (EmulatorOption emuOpt, mTemplateList) {
48 			QString globalOptionKey = globalConfig->emulatorPrefix + "/Configuration/Global/" + emuOpt.name;
49 			QString localOptionKey = globalConfig->emulatorPrefix + QString("/Configuration/%1/").arg(id) + emuOpt.name;
50 			bool enforceDefault = globalConfig->value(globalConfig->emulatorPrefix + "/Configuration/Global/EnforceDefault/" + emuOpt.name, false).toBool() || globalConfig->value(globalConfig->emulatorPrefix + QString("/Configuration/%1/EnforceDefault/").arg(id) + emuOpt.name, false).toBool();
51 
52 			switch ( emuOpt.type ) {
53 			case QMC2_ARCADE_EMUOPT_INT: {
54 				int dv = emuOpt.dvalue.toInt();
55 				int gv = globalConfig->value(globalOptionKey, dv).toInt();
56 				int v = globalConfig->value(localOptionKey, gv).toInt();
57 				if ( enforceDefault || v != dv )
58 					args << QString("-%1").arg(emuOpt.name) << QString("%1").arg(v);
59 				break;
60 			}
61 			case QMC2_ARCADE_EMUOPT_FLOAT: {
62 				double dv = emuOpt.dvalue.toDouble();
63 				double gv = globalConfig->value(globalOptionKey, dv).toDouble();
64 				double v = globalConfig->value(localOptionKey, gv).toDouble();
65 				if ( enforceDefault || v != dv )
66 					args << QString("-%1").arg(emuOpt.name) << QString("%1").arg(v);
67 				break;
68 			}
69 			case QMC2_ARCADE_EMUOPT_FLOAT2: {
70 				QStringList subValues = emuOpt.value.split(",");
71 				QStringList defaultSubValues = emuOpt.dvalue.split(",");
72 				double v1, v2, dv1, dv2;
73 				v1 = v2 = dv1 = dv2 = 0;
74 				if ( subValues.count() > 0 )
75 					v1 = subValues[0].toDouble();
76 				if ( subValues.count() > 1 )
77 					v2 = subValues[1].toDouble();
78 				if ( defaultSubValues.count() > 0 )
79 					dv1 = defaultSubValues[0].toDouble();
80 				if ( defaultSubValues.count() > 1 )
81 					dv2 = defaultSubValues[1].toDouble();
82 				if ( enforceDefault || v1 != dv1 || v2 != dv2 )
83 					args << QString("-%1").arg(emuOpt.name) << QString("%1,%2").arg(v1).arg(v2);
84 				break;
85 			}
86 			case QMC2_ARCADE_EMUOPT_FLOAT3: {
87 				QStringList subValues = emuOpt.value.split(",");
88 				QStringList defaultSubValues = emuOpt.dvalue.split(",");
89 				double v1, v2, v3, dv1, dv2, dv3;
90 				v1 = v2 = v3 = dv1 = dv2 = dv3 = 0;
91 				if ( subValues.count() > 0 )
92 					v1 = subValues[0].toDouble();
93 				if ( subValues.count() > 1 )
94 					v2 = subValues[1].toDouble();
95 				if ( subValues.count() > 2 )
96 					v3 = subValues[2].toDouble();
97 				if ( defaultSubValues.count() > 0 )
98 					dv1 = defaultSubValues[0].toDouble();
99 				if ( defaultSubValues.count() > 1 )
100 					dv2 = defaultSubValues[1].toDouble();
101 				if ( defaultSubValues.count() > 2 )
102 					dv3 = defaultSubValues[2].toDouble();
103 				if ( enforceDefault || v1 != dv1 || v2 != dv2 || v3 != dv3 )
104 					args << QString("-%1").arg(emuOpt.name) << QString("%1,%2,%3").arg(v1).arg(v2).arg(v3);
105 				break;
106 			}
107 			case QMC2_ARCADE_EMUOPT_BOOL: {
108 				bool dv = (emuOpt.dvalue == "true");
109 				bool gv = globalConfig->value(globalOptionKey, dv).toBool();
110 				bool v = globalConfig->value(localOptionKey, gv).toBool();
111 				if ( enforceDefault || v != dv ) {
112 					if ( v )
113 						args << QString("-%1").arg(emuOpt.name);
114 					else
115 						args << QString("-no%1").arg(emuOpt.name);
116 				}
117 				break;
118 			}
119 			case QMC2_ARCADE_EMUOPT_STRING:
120 			default: {
121 				QString dv = emuOpt.dvalue;
122 				QString gv = globalConfig->value(globalOptionKey, dv).toString();
123 				QString v = globalConfig->value(localOptionKey, gv).toString();
124 				if ( enforceDefault || v != dv )
125 					args << QString("-%1").arg(emuOpt.name) << v.replace("~", "$HOME");
126 				break;
127 			}
128 			}
129 		}
130 
131 		args << id;
132 
133 		connect(proc, SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
134 		connect(proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus)));
135 		connect(proc, SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput()));
136 		connect(proc, SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError()));
137 		connect(proc, SIGNAL(started()), this, SLOT(started()));
138 		connect(proc, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(stateChanged(QProcess::ProcessState)));
139 
140 		if ( !globalConfig->emulatorWorkingDirectory().isEmpty() )
141 			proc->setWorkingDirectory(globalConfig->emulatorWorkingDirectory());
142 
143 		mProcessMap.insert(mCurrentProcessId, proc);
144 		proc->start(globalConfig->emulatorExecutablePath(), args);
145 
146 		return mCurrentProcessId++;
147 	}
148 }
149 
createTemplateList()150 void ProcessManager::createTemplateList()
151 {
152 	QString templateFilePath = QFileInfo(globalConfig->optionsTemplateFile()).absoluteFilePath();
153 	QMC2_ARCADE_LOG_STR(tr("Loading configuration template from '%1'").arg(QDir::toNativeSeparators(templateFilePath)));
154 	mTemplateList.clear();
155 	QFile templateFile(templateFilePath);
156 	if ( templateFile.open(QFile::ReadOnly) ) {
157 		QXmlStreamReader xmlReader(&templateFile);
158 		while ( !xmlReader.atEnd() ) {
159 			xmlReader.readNext();
160 			if ( xmlReader.hasError() ) {
161 				QMC2_ARCADE_LOG_STR(tr("FATAL: XML error reading template: '%1' in file '%2' at line %3, column %4").
162 						    arg(xmlReader.errorString()).arg(QDir::toNativeSeparators(templateFilePath)).arg(xmlReader.lineNumber()).arg(xmlReader.columnNumber()));
163 			} else {
164 				if ( xmlReader.isStartElement() ) {
165 					QString elementType = xmlReader.name().toString();
166 					QXmlStreamAttributes attributes = xmlReader.attributes();
167 					QString name = attributes.value("name").toString();
168 					if ( elementType == "option" ) {
169 						bool ignore = false;
170 						QString shortName;
171 						if ( attributes.hasAttribute("shortname") )
172 							shortName = attributes.value("shortname").toString();
173 						if ( attributes.hasAttribute("ignore") )
174 							ignore = attributes.value("ignore") == "true";
175 						if ( attributes.hasAttribute(QString("ignore.%1").arg(QMC2_ARCADE_OS_NAME)) )
176 							ignore = attributes.value(QString("ignore.%1").arg(QMC2_ARCADE_OS_NAME)) == "true";
177 						if ( !ignore ) {
178 							QString type = attributes.value("type").toString();
179 							QString defaultValue;
180 							if ( attributes.hasAttribute(QString("default.%1").arg(QMC2_ARCADE_OS_NAME)) )
181 								defaultValue = attributes.value(QString("default.%1").arg(QMC2_ARCADE_OS_NAME)).toString();
182 							else
183 								defaultValue = attributes.value("default").toString();
184 							mTemplateList.append(EmulatorOption(name, shortName, type, defaultValue, QString()));
185 						}
186 					}
187 				}
188 			}
189 		}
190 		templateFile.close();
191 		std::sort(mTemplateList.begin(), mTemplateList.end(), EmulatorOption::lessThan);
192 		QMC2_ARCADE_LOG_STR(QString(tr("Done (loading configuration template from '%1')").arg(QDir::toNativeSeparators(templateFilePath)) + " - " + tr("%n option(s) loaded", "", mTemplateList.count())));
193 	} else
194 		QMC2_ARCADE_LOG_STR(tr("FATAL: Can't open the configuration template file: reason = %1").arg(fileErrorToString(templateFile.error())));
195 }
196 
fileErrorToString(QFile::FileError errorCode)197 QString ProcessManager::fileErrorToString(QFile::FileError errorCode)
198 {
199 	switch ( errorCode ) {
200 	case QFile::NoError:
201 		return tr("No error occurred");
202 	case QFile::ReadError:
203 		return tr("An error occurred when reading from the file");
204 	case QFile::WriteError:
205 		return tr("An error occurred when writing to the file");
206 	case QFile::FatalError:
207 		return tr("A fatal error occurred");
208 	case QFile::ResourceError:
209 		return tr("A resource error occurred");
210 	case QFile::OpenError:
211 		return tr("The file could not be opened");
212 	case QFile::AbortError:
213 		return tr("The operation was aborted");
214 	case QFile::TimeOutError:
215 		return tr("A timeout occurred");
216 	case QFile::UnspecifiedError:
217 		return tr("An unspecified error occurred");
218 	case QFile::RemoveError:
219 		return tr("The file could not be removed");
220 	case QFile::RenameError:
221 		return tr("The file could not be renamed");
222 	case QFile::PositionError:
223 		return tr("The position in the file could not be changed");
224 	case QFile::ResizeError:
225 		return tr("The file could not be resized");
226 	case QFile::PermissionsError:
227 		return tr("The file could not be accessed");
228 	case QFile::CopyError:
229 		return tr("The file could not be copied");
230 	default:
231 		return tr("An unknown error occurred");
232 	}
233 }
234 
processErrorToString(QProcess::ProcessError errorCode)235 QString ProcessManager::processErrorToString(QProcess::ProcessError errorCode)
236 {
237 	switch ( errorCode ) {
238 	case QProcess::FailedToStart:
239 		return tr("The process failed to start");
240 	case QProcess::Crashed:
241 		return tr("The process crashed");
242 	case QProcess::Timedout:
243 		return tr("A timeout occurred");
244 	case QProcess::WriteError:
245 		return tr("An error occurred when attempting to write to the process");
246 	case QProcess::ReadError:
247 		return tr("An error occurred when attempting to read from the process");
248 	case QProcess::UnknownError:
249 	default:
250 		return tr("An unknown error occurred");
251 	}
252 }
253 
processStateToString(QProcess::ProcessState state)254 QString ProcessManager::processStateToString(QProcess::ProcessState state)
255 {
256 	switch ( state ) {
257 	case QProcess::NotRunning:
258 		return tr("Not running");
259 	case QProcess::Starting:
260 		return tr("Starting");
261 	case QProcess::Running:
262 		return tr("Running");
263 	default:
264 		return tr("Unknown");
265 	}
266 }
267 
error(QProcess::ProcessError errorCode)268 void ProcessManager::error(QProcess::ProcessError errorCode)
269 {
270 	QProcess *proc = (QProcess *)sender();
271 	int procID = mProcessMap.key(proc);
272 	QMC2_ARCADE_LOG_STR(tr("Emulator #%1 error: reason = %2").arg(procID).arg(processErrorToString(errorCode)));
273 }
274 
finished(int exitCode,QProcess::ExitStatus exitStatus)275 void ProcessManager::finished(int exitCode, QProcess::ExitStatus exitStatus)
276 {
277 	QProcess *proc = (QProcess *)sender();
278 	int procID = mProcessMap.key(proc);
279 	QMC2_ARCADE_LOG_STR(tr("Emulator #%1 finished: exitCode = %2, exitStatus = %3").arg(procID).arg(exitCode).arg(exitStatus == QProcess::NormalExit ? tr("normal") : tr("crashed")));
280 	mProcessMap.remove(procID);
281 }
282 
readyReadStandardOutput()283 void ProcessManager::readyReadStandardOutput()
284 {
285 	QProcess *proc = (QProcess *)sender();
286 	int procID = mProcessMap.key(proc);
287 	QString data = proc->readAllStandardOutput();
288 #if defined(QMC2_ARCADE_OS_WIN)
289 	QString separator = "\r\n";
290 #else
291 	QString separator = "\n";
292 #endif
293 	foreach (QString line, data.split(separator)) {
294 		if ( !line.isEmpty() ) {
295 			QMC2_ARCADE_LOG_STR(tr("Emulator #%1 stdout: %2").arg(procID).arg(line));
296 		}
297 	}
298 }
299 
readyReadStandardError()300 void ProcessManager::readyReadStandardError()
301 {
302 	QProcess *proc = (QProcess *)sender();
303 	int procID = mProcessMap.key(proc);
304 	QString data = proc->readAllStandardError();
305 #if defined(QMC2_ARCADE_OS_WIN)
306 	QString separator = "\r\n";
307 #else
308 	QString separator = "\n";
309 #endif
310 	foreach (QString line, data.split(separator)) {
311 		if ( !line.isEmpty() ) {
312 			QMC2_ARCADE_LOG_STR(tr("Emulator #%1 stderr: %2").arg(procID).arg(line));
313 		}
314 	}
315 }
316 
started()317 void ProcessManager::started()
318 {
319 	QProcess *proc = (QProcess *)sender();
320 	int procID = mProcessMap.key(proc);
321 	QMC2_ARCADE_LOG_STR(tr("Emulator #%1 started").arg(procID));
322 	emit emulatorStarted(procID);
323 }
324 
stateChanged(QProcess::ProcessState newState)325 void ProcessManager::stateChanged(QProcess::ProcessState newState)
326 {
327 	QProcess *proc = (QProcess *)sender();
328 	int procID = mProcessMap.key(proc);
329 	QMC2_ARCADE_LOG_STR(tr("Emulator #%1 state changed: newState = %2").arg(procID).arg(processStateToString(newState)));
330 	emit emulatorFinished(procID);
331 }
332