1 #include <QTimer>
2 #include <QMap>
3 #include <QHash>
4 #include <QDir>
5 #include <QFileInfo>
6 #include <QProcess>
7 
8 #include "settings.h"
9 #include "options.h"
10 #include "samplechecker.h"
11 #include "machinelist.h"
12 #include "qmc2main.h"
13 #include "processmanager.h"
14 #include "toolexec.h"
15 #include "macros.h"
16 
17 // external global variables
18 extern MainWindow *qmc2MainWindow;
19 extern ProcessManager *qmc2ProcessManager;
20 extern Settings *qmc2Config;
21 extern MachineList *qmc2MachineList;
22 extern bool qmc2CleaningUp;
23 extern bool qmc2SampleCheckActive;
24 extern bool qmc2LoadingInterrupted;
25 extern bool qmc2TemplateCheck;
26 extern QHash<QString, QTreeWidgetItem *> qmc2MachineListItemHash;
27 
SampleChecker(QWidget * parent)28 SampleChecker::SampleChecker(QWidget *parent)
29 #if defined(QMC2_OS_WIN)
30 	: QDialog(parent, Qt::Dialog)
31 #else
32 	: QDialog(parent, Qt::Dialog | Qt::SubWindow)
33 #endif
34 {
35 	setupUi(this);
36 	progressBar->setFormat(tr("Idle"));
37 	progressBar->setRange(-1, -1);
38 	progressBar->setValue(-1);
39 	adjustIconSizes();
40 }
41 
adjustIconSizes()42 void SampleChecker::adjustIconSizes()
43 {
44 	QFont f;
45 	f.fromString(qmc2Config->value(QMC2_FRONTEND_PREFIX + "GUI/Font").toString());
46 	QFontMetrics fm(f);
47 	QSize iconSize = QSize(fm.height() - 2, fm.height() - 2);
48 	toolButtonSamplesRemoveObsolete->setIconSize(iconSize);
49 	pushButtonSamplesCheck->setIconSize(iconSize);
50 }
51 
restoreLayout()52 void SampleChecker::restoreLayout()
53 {
54 	if ( qmc2Config->contains(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Position") )
55 		move(qmc2Config->value(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Position", pos()).toPoint());
56 	if ( qmc2Config->contains(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Size") )
57 		resize(qmc2Config->value(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Size", size()).toSize());
58 }
59 
closeEvent(QCloseEvent * e)60 void SampleChecker::closeEvent(QCloseEvent *e)
61 {
62 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Position", pos());
63 	qmc2Config->setValue(QMC2_FRONTEND_PREFIX + "Layout/SampleChecker/Size", size());
64 
65 	if ( e )
66 		e->accept();
67 }
68 
hideEvent(QHideEvent * e)69 void SampleChecker::hideEvent(QHideEvent *e)
70 {
71 	closeEvent(0);
72 	e->accept();
73 }
74 
showEvent(QShowEvent * e)75 void SampleChecker::showEvent(QShowEvent *e)
76 {
77 	restoreLayout();
78 	if ( e )
79 		e->accept();
80 }
81 
verify()82 void SampleChecker::verify()
83 {
84 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("verifying samples"));
85 	qmc2SampleCheckActive = true;
86 	qmc2LoadingInterrupted = false;
87 	sampleSets.clear();
88 	verifyTimer.start();
89 	listWidgetSamplesGood->clear();
90 	labelSamplesGood->setText(tr("Good: 0"));
91 	listWidgetSamplesBad->clear();
92 	labelSamplesBad->setText(tr("Bad: 0"));
93 	listWidgetSamplesMissing->clear();
94 	labelSamplesMissing->setText(tr("Missing: 0"));
95 	listWidgetSamplesObsolete->clear();
96 	labelSamplesObsolete->setText(tr("Obsolete: 0"));
97 	toolButtonSamplesRemoveObsolete->setEnabled(false);
98 	sampleMap.clear();
99 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("preparing sample-check: parsing XML data for relevant sample information"));
100 	QString currentGameName, currentSampleOf;
101 	bool hasSamples = false;
102 	int sampleCount = 0;
103 	QMap<QString, int> sampleCountMap;
104 	progressBar->setFormat(tr("Parsing XML data"));
105 	qint64 xmlRowCount = qmc2MachineList->xmlDb()->xmlRowCount();
106 	progressBar->setRange(0, xmlRowCount);
107 	progressBar->setValue(0);
108 	for (qint64 rowCounter = 1; rowCounter < xmlRowCount; rowCounter++) {
109 		QStringList xmlLines = qmc2MachineList->xmlDb()->xml(rowCounter).split("\n", QString::SkipEmptyParts);
110 		int xmlLinesCount = xmlLines.count();
111 		progressBar->setValue(rowCounter);
112 		if ( rowCounter % QMC2_CHECK_UPDATE_FAST == 0 )
113 			qApp->processEvents();
114 		for (int gameListPos = 0; gameListPos < xmlLinesCount && !qmc2LoadingInterrupted; gameListPos++) {
115 			QString line = xmlLines[gameListPos];
116 			int startIndex = line.indexOf("<machine name=\"");
117 			int endIndex = -1;
118 			if ( startIndex >= 0 ) {
119 				startIndex += 15;
120 				endIndex = line.indexOf("\"", startIndex);
121 				if ( endIndex >= 0 )
122 					currentGameName = line.mid(startIndex, endIndex - startIndex);
123 				hasSamples = false;
124 				sampleCount = 0;
125 				startIndex = line.indexOf("sampleof=\"");
126 				if ( startIndex >= 0 ) {
127 					startIndex += 10;
128 					endIndex = line.indexOf("\"", startIndex);
129 					if ( endIndex >= 0 ) {
130 						currentSampleOf = line.mid(startIndex, endIndex - startIndex);
131 						if ( currentSampleOf == currentGameName )
132 							currentSampleOf.clear();
133 					}
134 					hasSamples = true;
135 				}
136 			} else if ( line.indexOf("<sample name=\"") >= 0 ) {
137 				hasSamples |= true;
138 				sampleCount++;
139 			} else {
140 				startIndex = line.indexOf("</machine>");
141 				if ( startIndex >= 0 ) {
142 					if ( !currentGameName.isEmpty() && hasSamples ) {
143 						if ( currentSampleOf.isEmpty() ) {
144 							sampleMap[currentGameName] = currentGameName;
145 							sampleCountMap[currentGameName] = sampleCount;
146 						} else {
147 							if ( qmc2MachineListItemHash.contains(currentSampleOf) ) {
148 								sampleMap[currentGameName] = currentSampleOf;
149 								sampleCountMap[currentGameName] = sampleCount;
150 							} else
151 								qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: XML bug: the machine '%1' is referencing a non-existing sample-set (sampleof=\"%2\") -- please inform MAME developers").arg(currentGameName).arg(currentSampleOf));
152 						}
153 					}
154 					currentGameName.clear();
155 					currentSampleOf.clear();
156 					hasSamples = false;
157 					sampleCount = 0;
158 				}
159 			}
160 		}
161 	}
162 
163 	if ( qmc2LoadingInterrupted ) {
164 		progressBar->setFormat(tr("Idle"));
165 		progressBar->setRange(-1, -1);
166 		progressBar->setValue(-1);
167 		pushButtonSamplesCheck->setText(tr("&Check samples"));
168 		pushButtonSamplesCheck->setIcon(QIcon(QString::fromUtf8(":/data/img/refresh.png")));
169 		QTime elapsedTime(0, 0, 0, 0);
170 		elapsedTime = elapsedTime.addMSecs(verifyTimer.elapsed());
171 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("done (verifying samples, elapsed time = %1)").arg(elapsedTime.toString("mm:ss.zzz")));
172 		qmc2SampleCheckActive = false;
173 		return;
174 	}
175 
176 	sampleSets = sampleMap.values();
177 	sampleSets.removeDuplicates();
178 
179 	for (int i = 0; i < sampleSets.count(); i++) {
180 		QString currentSample = sampleSets[i];
181 		if ( sampleCountMap[currentSample] == 0 ) {
182 			QStringList refList = sampleMap.keys(currentSample);
183 			if ( refList.count() > 0 ) {
184 				if ( refList.count() > 1 )
185 					qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: XML bug: the following machines are referencing a sample-set which isn't required (sampleof=\"%1\"): %2 -- please inform MAME developers").arg(currentSample).arg(refList.join(", ")));
186 				else
187 					qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: XML bug: the following machine is referencing a sample-set which isn't required (sampleof=\"%1\"): %2 -- please inform MAME developers").arg(currentSample).arg(refList.join(", ")));
188 				sampleSets.removeAll(currentSample);
189 			}
190 		}
191 	}
192 
193 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("found %n individual sample set(s)", "", sampleSets.count()) + " " + tr("and") + " " + tr("%n system(s) using samples", "", sampleMap.keys().count()));
194 
195 	progressBar->setRange(0, sampleSets.count());
196 	progressBar->setValue(0);
197 	progressBar->setFormat(tr("Checking sample status"));
198 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("check pass 1: checking sample status"));
199 
200 	QString userScopePath = Options::configPath();
201 	QString emuWorkDir = qmc2Config->value(QMC2_EMULATOR_PREFIX + "FilesAndDirectories/WorkingDirectory", QString()).toString();
202 	for (int i = 0; i < sampleSets.count() && !qmc2LoadingInterrupted; i++) {
203 		progressBar->setValue(i + 1);
204 		QString sampleSet(sampleSets.at(i));
205 		QProcess commandProc;
206 		if ( !emuWorkDir.isEmpty() )
207 			commandProc.setWorkingDirectory(emuWorkDir);
208 		commandProc.setProcessChannelMode(QProcess::MergedChannels);
209 		QStringList args;
210 		if ( qmc2Config->contains(QMC2_EMULATOR_PREFIX + "Configuration/Global/samplepath") )
211 			args << "-samplepath" << QString("%1").arg(qmc2Config->value(QMC2_EMULATOR_PREFIX + "Configuration/Global/samplepath").toString().replace("~", "$HOME"));
212 		args << "-verifysamples" << sampleSet;
213 		bool commandProcStarted = false;
214 		int retries = 0;
215 		commandProc.start(qmc2Config->value(QMC2_EMULATOR_PREFIX + "FilesAndDirectories/ExecutableFile").toString(), args);
216 		bool started = commandProc.waitForStarted(QMC2_PROCESS_POLL_TIME);
217 		while ( !started && retries++ < QMC2_PROCESS_POLL_RETRIES ) {
218 			started = commandProc.waitForStarted(QMC2_PROCESS_POLL_TIME_LONG);
219 			qApp->processEvents();
220 		}
221 		if ( started ) {
222 			commandProcStarted = true;
223 			bool commandProcRunning = (commandProc.state() == QProcess::Running);
224 			while ( commandProcRunning && !commandProc.waitForFinished(QMC2_PROCESS_POLL_TIME) ) {
225 				qApp->processEvents();
226 				commandProcRunning = (commandProc.state() == QProcess::Running);
227 			}
228 		} else {
229 			qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("FATAL: can't start emulator executable within a reasonable time frame, giving up") + " (" + tr("error text = %1").arg(ProcessManager::errorText(commandProc.error())) + ")");
230 			break;
231 		}
232 		if ( commandProcStarted ) {
233 			QString buffer(commandProc.readAllStandardOutput());
234 #if defined(QMC2_OS_WIN)
235 			buffer.replace("\r\n", "\n"); // convert WinDOS's "0x0D 0x0A" to just "0x0A"
236 #endif
237 			if ( !buffer.isEmpty() ) {
238 				QStringList bufferLines(buffer.split('\n', QString::SkipEmptyParts));
239 				if ( !bufferLines.isEmpty() ) {
240 					int index = 0;
241 					for (int i = 0; i < bufferLines.count(); i++) {
242 						if ( bufferLines.at(i).startsWith("sampleset ") ) {
243 							index = i;
244 							break;
245 						}
246 					}
247 					QString bufferLine(bufferLines.at(index).simplified().replace('\"', ""));
248 					if ( bufferLine.endsWith("is good") ) {
249 						listWidgetSamplesGood->addItem(sampleSet);
250 						labelSamplesGood->setText(tr("Good: %1").arg(listWidgetSamplesGood->count()));
251 					} else if ( bufferLine.endsWith("is bad") || bufferLine.endsWith(", 0 were OK.") ) {
252 						listWidgetSamplesBad->addItem(sampleSet);
253 						labelSamplesBad->setText(tr("Bad: %1").arg(listWidgetSamplesBad->count()));
254 					} else if ( bufferLine.endsWith("not found!") ) {
255 						listWidgetSamplesMissing->addItem(sampleSet);
256 						labelSamplesMissing->setText(tr("Missing: %1").arg(listWidgetSamplesMissing->count()));
257 					} else
258 						qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: received unknown output when checking the sample status of '%1': '%2'").arg(sampleSet).arg(bufferLine));
259 					qApp->processEvents();
260 				} else
261 					qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: received no output when checking the sample status of '%1'").arg(sampleSet));
262 			} else
263 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: received no output when checking the sample status of '%1'").arg(sampleSet));
264 		}
265 	}
266 
267 	if ( !qmc2LoadingInterrupted )
268 		verifyObsolete();
269 
270 	listWidgetSamplesGood->sortItems(Qt::AscendingOrder);
271 	listWidgetSamplesBad->sortItems(Qt::AscendingOrder);
272 	listWidgetSamplesMissing->sortItems(Qt::AscendingOrder);
273 	toolButtonSamplesRemoveObsolete->setEnabled(listWidgetSamplesObsolete->count() > 0);
274 
275 	QTime elapsedTime(0, 0, 0, 0);
276 	elapsedTime = elapsedTime.addMSecs(verifyTimer.elapsed());
277 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("done (verifying samples, elapsed time = %1)").arg(elapsedTime.toString("mm:ss.zzz")));
278 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("%1 good, %2 bad, %3 missing, %4 obsolete").arg(listWidgetSamplesGood->count()).arg(listWidgetSamplesBad->count()).arg(listWidgetSamplesMissing->count()).arg(listWidgetSamplesObsolete->count()));
279 	pushButtonSamplesCheck->setText(tr("&Check samples"));
280 	pushButtonSamplesCheck->setIcon(QIcon(QString::fromUtf8(":/data/img/refresh.png")));
281 
282 	qmc2SampleCheckActive = false;
283 	progressBar->setFormat(tr("Idle"));
284 	progressBar->setRange(-1, -1);
285 	progressBar->setValue(-1);
286 }
287 
verifyObsolete()288 void SampleChecker::verifyObsolete()
289 {
290 	progressBar->setFormat(tr("Checking for obsolete files / folders"));
291 	progressBar->setRange(0, 0);
292 	progressBar->setValue(-1);
293 	qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("check pass 2: checking for obsolete files / folders"));
294 
295 	QStringList samplePaths;
296 	if ( qmc2Config->contains(QMC2_EMULATOR_PREFIX + "Configuration/Global/samplepath") )
297 		samplePaths = qmc2Config->value(QMC2_EMULATOR_PREFIX + "Configuration/Global/samplepath").toString().split(";", QString::SkipEmptyParts);
298 	else
299 		samplePaths << "samples";
300 
301 	QString emuWorkDir = QDir::toNativeSeparators(qmc2Config->value(QMC2_EMULATOR_PREFIX + "FilesAndDirectories/WorkingDirectory", QString()).toString());
302 	if ( !emuWorkDir.isEmpty() )
303 		if ( !emuWorkDir.endsWith(QDir::separator()) )
304 			emuWorkDir += QDir::separator();
305 	foreach (QString samplePath, samplePaths) {
306 		if ( QDir::isRelativePath(samplePath) ) {
307 			if ( !emuWorkDir.isEmpty() )
308 				samplePath.prepend(emuWorkDir);
309 
310 		}
311 		if ( !samplePath.endsWith(QDir::separator()) )
312 			samplePath += QDir::separator();
313 		QDir sampleDir(samplePath);
314 		if ( sampleDir.exists() ) {
315 			QStringList fileList;
316 			recursiveFileList(samplePath, fileList);
317 			foreach (QString file, fileList) {
318 				QString relativeFilePath = file.remove(samplePath);
319 				QString gameName = relativeFilePath.toLower().remove(QRegExp("\\.zip$"));
320 				if ( !sampleSets.contains(gameName) ) {
321 					listWidgetSamplesObsolete->addItem(QDir::toNativeSeparators(sampleDir.absolutePath() + QDir::separator() + relativeFilePath));
322 					labelSamplesObsolete->setText(tr("Obsolete: %1").arg(listWidgetSamplesObsolete->count()));
323 				}
324 			}
325 		} else
326 			qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("WARNING: sample path '%1' does not exist").arg(sampleDir.absolutePath()));
327 	}
328 }
329 
on_pushButtonSamplesCheck_clicked()330 void SampleChecker::on_pushButtonSamplesCheck_clicked()
331 {
332 	if ( qmc2SampleCheckActive ) {
333 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("stopping sample check upon user request"));
334 		qmc2LoadingInterrupted = true;
335 		return;
336 	}
337 
338 	if ( qmc2MainWindow->isActiveState ) {
339 		qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("please wait for current activity to finish and try again (the sample-checker can only run exclusively)"));
340 		return;
341 	}
342 
343 	pushButtonSamplesCheck->setText(tr("&Stop check"));
344 	pushButtonSamplesCheck->setIcon(QIcon(QString::fromUtf8(":/data/img/halt.png")));
345 	verify();
346 }
347 
on_toolButtonSamplesRemoveObsolete_clicked()348 void SampleChecker::on_toolButtonSamplesRemoveObsolete_clicked()
349 {
350 	toolButtonSamplesRemoveObsolete->setEnabled(false);
351 	pushButtonSamplesCheck->setEnabled(false);
352 	QString emuWorkDir = QDir::toNativeSeparators(qmc2Config->value(QMC2_EMULATOR_PREFIX + "FilesAndDirectories/WorkingDirectory", QString()).toString());
353 	if ( !emuWorkDir.isEmpty() )
354 		if ( !emuWorkDir.endsWith(QDir::separator()) )
355 			emuWorkDir += QDir::separator();
356 	int count = 0;
357 	progressBar->setFormat(tr("Removing obsolete files / folders"));
358 	progressBar->setRange(0, listWidgetSamplesObsolete->count());
359 	listWidgetSamplesObsolete->setUpdatesEnabled(false);
360 	for (int row = 0; row < listWidgetSamplesObsolete->count(); row++) {
361 		progressBar->setValue(count);
362 		QListWidgetItem *item = listWidgetSamplesObsolete->item(row);
363 		QString path = item->text();
364 		QFileInfo fi(path);
365 		if ( fi.isDir() ) {
366 			QDir d(path);
367 			if ( QDir::isRelativePath(path) )
368 				if ( !emuWorkDir.isEmpty() )
369 					path.prepend(emuWorkDir);
370 			if ( d.rmdir(path) ) {
371 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("obsolete folder '%1' removed").arg(d.absolutePath()));
372 				QListWidgetItem *itemToDelete = listWidgetSamplesObsolete->takeItem(row);
373 				if ( itemToDelete ) {
374 					delete itemToDelete;
375 					labelSamplesObsolete->setText(tr("Obsolete: %1").arg(listWidgetSamplesObsolete->count()));
376 					row--;
377 				}
378 			} else
379 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("obsolete folder '%1' cannot be removed, please check permissions").arg(d.absolutePath()));
380 		} else {
381 			if ( fi.isRelative() )
382 				if ( !emuWorkDir.isEmpty() )
383 					path.prepend(emuWorkDir);
384 			QFile f(path);
385 			if ( f.remove() ) {
386 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("obsolete file '%1' removed").arg(fi.absoluteFilePath()));
387 				QListWidgetItem *itemToDelete = listWidgetSamplesObsolete->takeItem(row);
388 				if ( itemToDelete ) {
389 					delete itemToDelete;
390 					labelSamplesObsolete->setText(tr("Obsolete: %1").arg(listWidgetSamplesObsolete->count()));
391 					row--;
392 				}
393 			} else
394 				qmc2MainWindow->log(QMC2_LOG_FRONTEND, tr("obsolete file '%1' cannot be removed, please check permissions").arg(fi.absoluteFilePath()));
395 		}
396 		if ( count++ % QMC2_CHECK_UPDATE_MEDIUM == 0 ) {
397 			listWidgetSamplesObsolete->setUpdatesEnabled(true);
398 			listWidgetSamplesObsolete->update();
399 			qApp->processEvents();
400 			listWidgetSamplesObsolete->setUpdatesEnabled(false);
401 		}
402 	}
403 	listWidgetSamplesObsolete->setUpdatesEnabled(true);
404 	toolButtonSamplesRemoveObsolete->setEnabled(listWidgetSamplesObsolete->count() > 0);
405 	pushButtonSamplesCheck->setEnabled(true);
406 	progressBar->setFormat(tr("Idle"));
407 	progressBar->setRange(-1, -1);
408 	progressBar->setValue(-1);
409 }
410 
recursiveFileList(const QString & sDir,QStringList & fileNames)411 void SampleChecker::recursiveFileList(const QString &sDir, QStringList &fileNames)
412 {
413 	QDir dir(sDir);
414 	foreach (QFileInfo info, dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System)) {
415 		QString path = QDir::toNativeSeparators(info.filePath());
416 		if ( info.isDir() ) {
417 			// directory recursion
418 			if ( info.fileName() != ".." && info.fileName() != "." ) {
419 				recursiveFileList(path, fileNames);
420 				fileNames << path + QDir::separator();
421 				qApp->processEvents();
422 			}
423 		} else
424 			fileNames << path;
425 	}
426 }
427