/* * Copyright (C) 2013 Sebastian Herbord. All rights reserved. * * This file is part of the basic diagnosis plugin for Mod Organizer * * This plugin is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This plugin is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this plugin. If not, see . */ #include "diagnosebasic.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MOBase; const QRegularExpression DiagnoseBasic::RE_LOG_FILE(".*[.]log[0-9]*$"); const unsigned int DiagnoseBasic::PROBLEM_ERRORLOG; const unsigned int DiagnoseBasic::PROBLEM_OVERWRITE; const unsigned int DiagnoseBasic::PROBLEM_INVALIDFONT; const unsigned int DiagnoseBasic::PROBLEM_NITPICKINSTALLED; const unsigned int DiagnoseBasic::PROBLEM_PROFILETWEAKS; const unsigned int DiagnoseBasic::PROBLEM_MISSINGMASTERS; const unsigned int DiagnoseBasic::PROBLEM_ALTERNATE; const unsigned int DiagnoseBasic::NUM_CONTEXT_ROWS; DiagnoseBasic::DiagnoseBasic() : m_MOInfo(nullptr) {} bool DiagnoseBasic::init(IOrganizer* moInfo) { m_MOInfo = moInfo; m_MOInfo->modList()->onModStateChanged( [&](const std::map& mods) { if (mods.contains("Overwrite")) invalidate(); }); m_MOInfo->modList()->onModMoved([&](const QString&, int, int) { // invalidates only the assetOrder check but there is currently no way to recheck // individual checks invalidate(); }); m_MOInfo->pluginList()->onPluginMoved([&](const QString&, int, int) { invalidate(); }); m_MOInfo->pluginList()->onRefreshed([&]() { invalidate(); }); m_MOInfo->pluginList()->onPluginStateChanged([&](auto const&) { invalidate(); }); m_MOInfo->onAboutToRun([&](const QString& executable) { return fileAttributes(executable); }); return true; } QString DiagnoseBasic::name() const { return "Basic diagnosis plugin"; } QString DiagnoseBasic::author() const { return "Tannin"; } QString DiagnoseBasic::description() const { return tr("Checks for problems unrelated to other plugins"); } VersionInfo DiagnoseBasic::version() const { return VersionInfo(1, 1, 3, VersionInfo::RELEASE_FINAL); } bool DiagnoseBasic::isActive() const { return true; } QList DiagnoseBasic::settings() const { return QList() << PluginSetting( "check_errorlog", tr("Warn when an error occurred last time an application was run"), true) << PluginSetting("check_overwrite", tr("Warn when there are files in the overwrite directory"), true) << PluginSetting("check_font", tr("Warn when the font configuration refers to files that " "aren't installed"), true) << PluginSetting( "check_conflict", tr("Warn when mods are installed that conflict with MO functionality"), true) << PluginSetting("check_missingmasters", tr("Warn when there are esps with missing masters"), true) << PluginSetting( "check_alternategames", tr("Warn when an installed mod came from an alternative game source"), false) << PluginSetting("check_fileattributes", tr("Warn when files have unwanted attributes"), false) << PluginSetting( "ow_ignore_empty", tr("Ignore empty directories when checking overwrite directory"), false) << PluginSetting("ow_ignore_log", tr("Ignore .log files and empty directories when checking " "overwrite directory"), false); } bool DiagnoseBasic::errorReported() const { QDir dir(qApp->property("dataPath").toString() + "/logs"); QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), QDir::Files, QDir::Name | QDir::Reversed); if (files.count() > 0) { QString logFile = files.at(0).absoluteFilePath(); QFile file(logFile); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { char buffer[1024]; int line = 0; qint64 lineLengths[NUM_CONTEXT_ROWS]; for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { lineLengths[i] = 0; } while (!file.atEnd()) { lineLengths[line % NUM_CONTEXT_ROWS] = file.readLine(buffer, 1024) + 1; if (strncmp(buffer, "ERROR", 5) == 0) { qint64 sumChars = 0; for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { sumChars += lineLengths[i]; } file.seek(file.pos() - sumChars); m_ErrorMessage = ""; for (int i = 0; i < 2 * NUM_CONTEXT_ROWS; ++i) { file.readLine(buffer, 1024); QString lineString = QString::fromUtf8(buffer); if (lineString.startsWith("ERROR")) { m_ErrorMessage += "" + lineString + ""; } else { m_ErrorMessage += lineString; } } return true; } // prevent this function from taking forever if (line++ >= 50000) { break; } } } } return false; } bool DiagnoseBasic::checkEmpty(QString const& path) const { QDir dir(path); dir.setFilter(QDir::Files | QDir::Hidden | QDir::System); // Search files first for (auto const& file : dir.entryList()) { if (!m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool() || !RE_LOG_FILE.match(file).hasMatch()) { return false; } } // Then directories dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); for (QFileInfo const& subdir : dir.entryInfoList()) { if (!checkEmpty(subdir.absoluteFilePath())) { return false; } } return true; } bool DiagnoseBasic::overwriteFiles() const { // QString dirname(qApp->property("dataPath").toString() + "/overwrite"); QString dirname(m_MOInfo->overwritePath()); if (m_MOInfo->pluginSetting(name(), "ow_ignore_empty").toBool() || m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool()) { return !checkEmpty(dirname); } QDir dir(dirname); bool checkDirs = m_MOInfo->managedGame()->getModMappings().keys().size() > 1 || m_MOInfo->managedGame()->getModMappings().keys().first() != ""; if (checkDirs) { bool empty = true; for (auto dir : m_MOInfo->managedGame()->getModMappings().keys()) { auto mapDir = QDir(dirname).filePath(dir); if (QDir(mapDir).exists()) { empty = QDir(mapDir).count() == 2; // account for . and .. } if (!empty) break; } return !empty; } return dir.count() != 2; // account for . and .. } bool DiagnoseBasic::nitpickInstalled() const { QString path = m_MOInfo->resolvePath("skse/plugins/nitpick.dll"); return !path.isEmpty(); } /// unused code to remove duplicates from a vector template void makeUnique(std::vector& vector) { std::set done; auto read = vector.begin(); auto write = vector.begin(); for (; read != vector.end(); ++read) { if (done.insert(*read).second) { *write = *read; ++write; } } vector.erase(write, vector.end()); } bool DiagnoseBasic::missingMasters() const { std::set enabledPlugins; QStringList esps = m_MOInfo->findFiles("", [](const QString& fileName) -> bool { return fileName.endsWith(".esp", FileNameComparator::CaseSensitivity) || fileName.endsWith(".esm", FileNameComparator::CaseSensitivity) || fileName.endsWith(".esl", FileNameComparator::CaseSensitivity); }); // gather enabled masters first for (const QString& esp : esps) { QString baseName = QFileInfo(esp).fileName(); if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { enabledPlugins.insert(baseName.toLower()); } } m_MissingMasters.clear(); m_PluginChildren.clear(); // for each required master in each esp, test if it's in the list of enabled masters. for (const QString& esp : esps) { QString baseName = QFileInfo(esp).fileName(); if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { for (const QString master : m_MOInfo->pluginList()->masters(baseName)) { if (enabledPlugins.find(master.toLower()) == enabledPlugins.end()) { m_MissingMasters.insert(master); m_PluginChildren[master].insert(baseName); } } } } return !m_MissingMasters.empty(); } bool DiagnoseBasic::alternateGame() const { QStringList mods = m_MOInfo->modList()->allMods(); for (QString mod : mods) { if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ALTERNATE && m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) return true; } return false; } bool DiagnoseBasic::invalidFontConfig() const { if ((m_MOInfo->managedGame()->gameName() != "Skyrim") && (m_MOInfo->managedGame()->gameShortName() != "SkyrimSE")) { // this check is only for skyrim return false; } // files from skyrim_interface.bsa static std::vector defaultFonts{"interface\\fonts_console.swf", "interface\\fonts_en.swf", "interface\\fonts_cclub.swf"}; QString configPath = m_MOInfo->resolvePath("interface/fontconfig.txt"); if (configPath.isEmpty()) { return false; } QFile config(configPath); if (!config.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug("failed to open %s", qUtf8Printable(configPath)); return false; } std::regex exp("^fontlib \"([^\"]*)\"$"); while (!config.atEnd()) { QByteArray row = config.readLine(); std::cmatch match; if (std::regex_search(row.constData(), match, exp)) { std::string temp = match[1]; QString path(temp.c_str()); bool isDefault = false; for (const QString& def : defaultFonts) { if (QString::compare(def, path, FileNameComparator::CaseSensitivity) == 0) { isDefault = true; break; } } if (!isDefault && m_MOInfo->resolvePath(path).isEmpty()) { return true; } } } return false; } static bool checkFileAttributes(const QString& path) { #ifdef _WIN32 WCHAR w_path[32767]; memset(w_path, 0, sizeof(w_path)); path.toWCharArray(w_path); DWORD attrs = GetFileAttributes(w_path); if (attrs != INVALID_FILE_ATTRIBUTES) { if (!(attrs & FILE_ATTRIBUTE_ARCHIVE) && !(attrs & FILE_ATTRIBUTE_NORMAL) && (attrs & ~(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_ARCHIVE))) { QString debug; debug += QString("%1 ").arg(attrs, 8, 16, QLatin1Char('0')); // A C D H I O P R S U V X Z debug += (attrs & FILE_ATTRIBUTE_DIRECTORY) ? "D" : " "; debug += (attrs & FILE_ATTRIBUTE_ARCHIVE) ? "A" : " "; debug += (attrs & FILE_ATTRIBUTE_READONLY) ? "R" : " "; debug += (attrs & FILE_ATTRIBUTE_SYSTEM) ? "S" : " "; debug += (attrs & FILE_ATTRIBUTE_HIDDEN) ? "H" : " "; debug += (attrs & FILE_ATTRIBUTE_OFFLINE) ? "O" : " "; debug += (attrs & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) ? "I" : " "; debug += (attrs & FILE_ATTRIBUTE_NO_SCRUB_DATA) ? "X" : " "; debug += (attrs & FILE_ATTRIBUTE_INTEGRITY_STREAM) ? "V" : " "; debug += (attrs & FILE_ATTRIBUTE_PINNED) ? "P" : " "; debug += (attrs & FILE_ATTRIBUTE_UNPINNED) ? "U" : " "; debug += (attrs & FILE_ATTRIBUTE_COMPRESSED) ? "C" : " "; debug += (attrs & FILE_ATTRIBUTE_SPARSE_FILE) ? "Z" : " "; debug += QString(" %1").arg(path); qDebug() << debug; return true; } } else { DWORD error = ::GetLastError(); qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") .arg(w_path) .arg(error))); } return false; #else Q_UNUSED(path); return false; #endif } static bool fixFileAttributes(const QString& path) { #ifdef _WIN32 bool success = true; WCHAR w_path[32767]; memset(w_path, 0, sizeof(w_path)); path.toWCharArray(w_path); DWORD attrs = GetFileAttributes(w_path); if (attrs != INVALID_FILE_ATTRIBUTES) { // Clear all the attributes possible, except ARCHIVE, with SetFileAttributes if (!SetFileAttributes( w_path, attrs & FILE_ATTRIBUTE_ARCHIVE ? FILE_ATTRIBUTE_ARCHIVE : 0)) { DWORD error = GetLastError(); qWarning(qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") .arg(path) .arg(error))); success = false; } // Compression requires DeviceIoControl if (attrs & FILE_ATTRIBUTE_COMPRESSED) { HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hndl != INVALID_HANDLE_VALUE) { USHORT compressionSetting = COMPRESSION_FORMAT_NONE; DWORD bytesReturned = 0; if (!DeviceIoControl(hndl, FSCTL_SET_COMPRESSION, &compressionSetting, sizeof(compressionSetting), NULL, 0, &bytesReturned, NULL)) { DWORD error = GetLastError(); qWarning(qUtf8Printable( QString("Unable to disable compression for file %1 (error %2)") .arg(path) .arg(error))); success = false; } CloseHandle(hndl); } else { DWORD error = GetLastError(); qWarning(qUtf8Printable( QString("Unable to open file %1 (error %2)").arg(path).arg(error))); success = false; } } // Sparseness requires DeviceIoControl if (attrs & FILE_ATTRIBUTE_SPARSE_FILE) { HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hndl != INVALID_HANDLE_VALUE) { FILE_SET_SPARSE_BUFFER setting = {FALSE}; DWORD bytesReturned = 0; if (!DeviceIoControl(hndl, FSCTL_SET_SPARSE, &setting, sizeof(setting), NULL, 0, &bytesReturned, NULL)) { DWORD error = GetLastError(); qWarning(qUtf8Printable( QString("Unable to disable sparseness for file %1 (error %2)") .arg(path) .arg(error))); success = false; } CloseHandle(hndl); } else { DWORD error = GetLastError(); qWarning(qUtf8Printable( QString("Unable to open file %1 (error %2)").arg(path).arg(error))); success = false; } } // As a last ditch effort, set the archive flag if (!success) { attrs = GetFileAttributes(w_path); if (attrs != INVALID_FILE_ATTRIBUTES) { if (!SetFileAttributes(w_path, attrs | FILE_ATTRIBUTE_ARCHIVE)) { DWORD error = GetLastError(); qWarning( qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") .arg(path) .arg(error))); } else { success = true; } } else { DWORD error = GetLastError(); qWarning( qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") .arg(path) .arg(error))); } } } else { DWORD error = GetLastError(); qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") .arg(path) .arg(error))); success = false; } return success; #else Q_UNUSED(path); return true; #endif } bool DiagnoseBasic::fileAttributes(const QString& executable) const { if (!m_MOInfo->pluginSetting(name(), "check_fileattributes").toBool()) return true; QStringList filesToFix; QStringList directoriesToSearch; // Search the game directory for problems directoriesToSearch << m_MOInfo->managedGame()->dataDirectory().absolutePath(); // Find the active mods to search them too for (QString mod : m_MOInfo->modList()->allMods()) { if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) { directoriesToSearch << m_MOInfo->modList()->getMod(mod)->absolutePath(); } } // Find the subdirectories of all the directories for (int i = 0; i < directoriesToSearch.length(); i++) { for (QString dir : QDir(directoriesToSearch[i]) .entryList(QDir::Hidden | QDir::AllDirs | QDir::NoDotAndDotDot)) { directoriesToSearch << QDir(directoriesToSearch[i]).filePath(dir); } } // Set up a progress bar since this can take a while QPushButton* progressButton = new QPushButton("Cancel"); progressButton->setEnabled(false); QLabel* progressLabel = new QLabel; progressLabel->setText(tr("File attribute checker\nSearching for problems...")); progressLabel->setAlignment(Qt::AlignCenter); QProgressDialog dialog; dialog.setWindowModality(Qt::ApplicationModal); dialog.setCancelButton(progressButton); dialog.setLabel(progressLabel); dialog.setMinimumDuration(1); dialog.show(); dialog.setMaximum(directoriesToSearch.length()); // Find problems with the directories and files for (int i = 0; i < directoriesToSearch.length(); i++) { QString* dirPath = &directoriesToSearch[i]; #ifdef _WIN32 QString fixedPath = QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath)); #else QString fixedPath = *dirPath; #endif if (checkFileAttributes(fixedPath)) filesToFix << fixedPath; for (QString file : QDir(*dirPath).entryList(QDir::Hidden | QDir::AllEntries | QDir::NoDotAndDotDot)) { #ifdef _WIN32 fixedPath = QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath + "\\" + file)); #else fixedPath = QDir(*dirPath).filePath(file); #endif if (checkFileAttributes(fixedPath)) filesToFix << fixedPath; } dialog.setValue(i); } if (filesToFix.length() == 0) { return true; } QMessageBox::StandardButton userResponse = QMessageBox::question(nullptr, tr("Invalid file attributes found"), tr("One or more of your files has attributes that may " "prevent the game from reading the files. " "This can result in missing plugins, missing textures, " "and other such problems.\n\n" "Fix the file attributes?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); if (userResponse == QMessageBox::Cancel) { qDebug() << "User canceled executable launch due to file attributes"; return false; } else if (userResponse == QMessageBox::No) { return true; } // Reset progress bar progressLabel->setText(tr("File attribute checker\nFixing file attributes...")); dialog.setValue(0); dialog.setMaximum(filesToFix.length()); // Start iterating through files fixing problems bool success = true; for (int i = 0; i < filesToFix.length(); i++) { if (!fixFileAttributes(filesToFix[i])) success = false; dialog.setValue(i); } if (!success) { if (QMessageBox::question(nullptr, tr("Unable to set file attributes"), tr("Mod Organizer was unable to fix the file attributes " "of at least one file.\n\n" "Continue launching %1?") .arg(executable), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { return false; } } return true; } std::vector DiagnoseBasic::activeProblems() const { std::vector result; if (m_MOInfo->pluginSetting(name(), "check_errorlog").toBool() && errorReported()) { result.push_back(PROBLEM_ERRORLOG); } if (m_MOInfo->pluginSetting(name(), "check_overwrite").toBool() && overwriteFiles()) { result.push_back(PROBLEM_OVERWRITE); } if (m_MOInfo->pluginSetting(name(), "check_font").toBool() && invalidFontConfig()) { result.push_back(PROBLEM_INVALIDFONT); } if (m_MOInfo->pluginSetting(name(), "check_conflict").toBool() && nitpickInstalled()) { result.push_back(PROBLEM_NITPICKINSTALLED); } if (m_MOInfo->pluginSetting(name(), "check_missingmasters").toBool() && missingMasters()) { result.push_back(PROBLEM_MISSINGMASTERS); } if (m_MOInfo->pluginSetting(name(), "check_alternategames").toBool() && alternateGame()) { result.push_back(PROBLEM_ALTERNATE); } if (QFile::exists(m_MOInfo->profilePath() + "/profile_tweaks.ini")) { result.push_back(PROBLEM_PROFILETWEAKS); } return result; } QString DiagnoseBasic::shortDescription(unsigned int key) const { switch (key) { case PROBLEM_ERRORLOG: return tr("There was an error reported recently"); case PROBLEM_OVERWRITE: return tr("There are files in your Overwrite mod directory"); case PROBLEM_INVALIDFONT: return tr("Your font configuration may be broken"); case PROBLEM_NITPICKINSTALLED: return tr("Nitpick installed"); case PROBLEM_PROFILETWEAKS: return tr("INI Tweaks overwritten"); case PROBLEM_MISSINGMASTERS: return tr("Missing Masters"); case PROBLEM_ALTERNATE: return tr("At least one unverified mod is using an alternative game source"); default: throw MyException(tr("invalid problem key %1").arg(key)); } } QString DiagnoseBasic::fullDescription(unsigned int key) const { switch (key) { case PROBLEM_ERRORLOG: return "" + m_ErrorMessage.replace("\n", "
") + "
"; case PROBLEM_OVERWRITE: return tr( "There are currently files in your Overwrite directory. These files are typically newly " "created files, usually generated by an external mod tool (i.e. Wrye Bash, " "xEdit, FNIS, ...). Creation Club ESL files will also end up here when " "downloaded. Any files in Overwrite " "will take top priority when loading your mod files and will always overwrite " "any other mod in your profile.
" "
" "It is recommended that you review the files in Overwrite and move any relevant files to a new or existing " "mod. You can do this by double-clicking the Overwrite mod and dragging files from the Overwrite window to " "a mod entry in the main mod list. It is also possible to move all current " "Overwrite files to a new mod by " "right-clicking on the Overwrite mod.
" "
" "Not all files in Overwrite need to " "be removed, but there are several reasons to do so. Some generated files will " "be directly related to the active mods in your profile and will be " "incompatible with different mod setups. Since Overwrite is always active, this could cause conflicts between " "profiles. Additionally, moving relevant game files into a normal mod will " "give you greater control over those files. Some files can live safely in " "Overwrite, such as basic logs and " "cache files. It is up to you to understand how best to manage these files.
" "
" "If you do not wish to see this warning and understand how to handle your " "Overwrite directory, you can open " "the Mod Organizer settings and disable this warning under the \"Diagnose " "Basic\" plugin configuration."); case PROBLEM_INVALIDFONT: return tr("Your current configuration seems to reference a font that is not " "installed. You may see only boxes instead of letters.
" "The font configuration is in Data\\interface\\fontconfig.txt. Most " "likely you have a broken installation of a font replacer mod."); case PROBLEM_NITPICKINSTALLED: return tr("You have the nitpick skse plugin installed. This plugin is not needed " "with Mod Organizer because MO already offers the same functionality. " "Worse: The two solutions may conflict so it's strongly suggested you " "remove this plugin."); case PROBLEM_PROFILETWEAKS: { QString fileContent = readFileText(m_MOInfo->profilePath() + "/profile_tweaks.ini"); return tr("Settings provided in ini tweaks have been overwritten in-game or in an " "applications.
" "These overwrites are stored in a separate file " "(profile_tweaks.ini within the profile directory)
" "to keep ini-tweaks in their original state but you should really get " "rid of this file as there is
" "no tool support in MO to work on it.
" "Advice: Copy settings you want to keep to an appropriate ini tweak, " "then delete profile_tweaks.ini.
" "Hitting the Fix button will delete that file") + "
profile_tweaks.ini:
" + fileContent + "
"; } break; case PROBLEM_MISSINGMASTERS: { QString masterInfo; for (auto master : m_MissingMasters) { masterInfo += ""; masterInfo += "" + master + ""; masterInfo += "" + SetJoin(m_PluginChildren[master], ", ") + ""; masterInfo += ""; } return tr("The masters for some plugins (esp/esl/esm) are not enabled.
" "The game will crash unless you install and enable the following " "plugins: ") + "
" + "" + "" + masterInfo + "
" + tr("Master") + "" + tr("Required By") + "
"; } break; case PROBLEM_ALTERNATE: { return tr( "You have at least one active mod installed from an alternative game " "source.
" "This means that the mod was downloaded from a game source which does not " "match
" "the expected primary game.

" "Depending on the type of mod, this may require converting various files to " "run correctly.

" "Advice: Once you have verified the mod is working correctly, you can use the " "context menu
" "and select \"Mark as converted/working\" to remove the flag and warning."); } break; default: throw MyException(tr("invalid problem key %1").arg(key)); } } bool DiagnoseBasic::hasGuidedFix(unsigned int key) const { return (key == PROBLEM_PROFILETWEAKS); } void DiagnoseBasic::startGuidedFix(unsigned int key) const { switch (key) { case PROBLEM_PROFILETWEAKS: { shellDeleteQuiet(m_MOInfo->profilePath() + "/profile_tweaks.ini"); } break; default: throw MyException(tr("invalid problem key %1").arg(key)); } }