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