/* * iDescriptor: A free and open-source idevice management tool. * * Copyright (C) 2025 Uncore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "diagnosewidget.h" #ifdef WIN32 #include "platform/windows/win_common.h" #include #include #endif DependencyItem::DependencyItem(const QString &name, const QString &description, bool optional, QWidget *parent) : QWidget(parent), m_name(name) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); QHBoxLayout *infoLayout = new QHBoxLayout(); m_nameLabel = new QLabel(name); QFont nameFont = m_nameLabel->font(); nameFont.setBold(true); nameFont.setPointSize(nameFont.pointSize() + 1); m_nameLabel->setFont(nameFont); m_descriptionLabel = new QLabel(QString("(%1)").arg(description)); if (optional) { m_descriptionLabel->setText(m_descriptionLabel->text() + " (Optional)"); } m_descriptionLabel->setWordWrap(false); infoLayout->addWidget(m_nameLabel); infoLayout->addWidget(m_descriptionLabel); // Middle - status m_statusLabel = new QLabel("Checking..."); m_statusLabel->setMinimumWidth(100); m_statusLabel->setAlignment(Qt::AlignCenter); // Right side - actions QHBoxLayout *actionLayout = new QHBoxLayout(); actionLayout->setContentsMargins(0, 0, 0, 0); m_installButton = new QPushButton("Install"); m_installButton->setMinimumWidth(80); m_installButton->setVisible(false); connect(m_installButton, &QPushButton::clicked, this, &DependencyItem::onInstallClicked); m_processIndicator = new QProcessIndicator(); m_processIndicator->setType(QProcessIndicator::line_rotate); m_processIndicator->setFixedSize(24, 24); m_processIndicator->setVisible(false); actionLayout->addWidget(m_processIndicator); actionLayout->addWidget(m_installButton); layout->addLayout(infoLayout); layout->addStretch(); layout->addWidget(m_statusLabel); layout->addLayout(actionLayout); } void DependencyItem::setInstalled(SERVICE_AVAILABILITY availability, bool isRequired) { setChecking(false); m_availability = availability; switch (availability) { case SERVICE_AVAILABLE: /* code */ if (m_name == "Avahi Daemon" || m_name == "Bonjour Service") { m_statusLabel->setText("Activated"); } else { m_statusLabel->setText("Installed"); } #ifndef WIN32 m_statusLabel->setStyleSheet("color: green;"); #else // FIXME: if we call this multiple times, the styles will keep stacking // and become a mess, need a better way to handle this m_statusLabel->setStyleSheet(mergeStyles( m_statusLabel, QString("QLabel { color: %1; }").arg(COLOR_GREEN.name()))); #endif m_installButton->setVisible(false); return; break; case SERVICE_AVAILABLE_BUT_NOT_RUNNING: m_statusLabel->setText("Not running"); m_installButton->setText("Enable"); break; case UNABLE_TO_CHECK: m_statusLabel->setText("Action needed"); m_installButton->setText("Info"); break; case SERVICE_UNAVAILABLE: if (isRequired) { m_statusLabel->setText("Not Installed"); } else { m_statusLabel->setText("Not Installed (Optional)"); } break; default: break; } #ifndef WIN32 if (isRequired) { m_statusLabel->setStyleSheet("color: red;"); } #else // FIXME: if we call this multiple times, the styles will keep stacking // and become a mess, need a better way to handle this m_statusLabel->setStyleSheet(mergeStyles( m_statusLabel, QString("QLabel { color: %1; }").arg(COLOR_RED.name()))); #endif m_installButton->setVisible(true); } void DependencyItem::setChecking(bool checking) { if (checking) { m_statusLabel->setText("Checking..."); m_statusLabel->setStyleSheet("color: gray;"); m_installButton->setVisible(false); m_processIndicator->setVisible(false); m_processIndicator->stop(); } } void DependencyItem::setInstalling(bool installing) { if (installing) { m_statusLabel->setText("Installing..."); m_statusLabel->setStyleSheet("color: gray;"); m_installButton->setVisible(false); m_processIndicator->setVisible(true); m_processIndicator->start(); } else { m_processIndicator->stop(); m_processIndicator->setVisible(false); } } void DependencyItem::setActivating(bool activating) { if (activating) { m_statusLabel->setText("Activating..."); m_statusLabel->setStyleSheet("color: gray;"); m_installButton->setVisible(false); m_processIndicator->setVisible(true); m_processIndicator->start(); } else { m_processIndicator->stop(); m_processIndicator->setVisible(false); } } void DependencyItem::setProgress(const QString &message) { m_statusLabel->setText(message); m_statusLabel->setStyleSheet("color: gray;"); } void DependencyItem::onInstallClicked() { emit installRequested(m_name, m_availability); } DiagnoseWidget::DiagnoseWidget(QWidget *parent) : QWidget(parent), m_isExpanded(false) { setupUI(); #ifdef WIN32 addDependencyItem( "Bonjour Service", "Required for AirPlay, wireless devices and network service discovery"); addDependencyItem("Apple Mobile Device Support", "Required for iOS device communication"); addDependencyItem("WinFsp", "Required for mounting your device as a drive", true); #endif #ifdef __linux__ addDependencyItem("Avahi Daemon", "Required for AirPlay, wireless devices"); #ifdef ENABLE_RECOVERY_DEVICE_SUPPORT addDependencyItem("UDEV rules", "Required for recovery devices requires manual setup", true); #endif #endif // Auto-check on startup QTimer::singleShot(100, this, [this]() { checkDependencies(); }); } void DiagnoseWidget::setupUI() { setObjectName("diagnoseWidget"); setContentsMargins(20, 2, 20, 0); setAutoFillBackground(true); m_mainLayout = new QVBoxLayout(this); m_mainLayout->setSpacing(5); // Title and summary QLabel *titleLabel = new QLabel("Dependency Check"); QFont titleFont = titleLabel->font(); titleFont.setBold(true); titleFont.setPointSize(titleFont.pointSize() + 2); titleLabel->setFont(titleFont); m_summaryLabel = new QLabel("Checking system dependencies..."); // Check button m_checkButton = new QPushButton("Refresh Check(s)"); m_checkButton->setMaximumWidth(m_checkButton->sizeHint().width()); connect(m_checkButton, &QPushButton::clicked, this, [this]() { checkDependencies(false); }); // Toggle button m_toggleButton = new ZIconWidget( QIcon(":/resources/icons/MaterialSymbolsLightKeyboardArrowDown.png"), "Expand/Collapse"); // m_toggleButton->setFixedSize(24, 24); m_toggleButton->setCheckable(true); connect(m_toggleButton, &QPushButton::clicked, this, &DiagnoseWidget::onToggleExpand); m_itemsWidget = new QWidget(); m_itemsLayout = new QVBoxLayout(m_itemsWidget); m_itemsLayout->setSpacing(10); m_itemsLayout->addStretch(); m_itemsWidget->setVisible(m_isExpanded); // Layout assembly QHBoxLayout *headerLayout = new QHBoxLayout(); headerLayout->addWidget(titleLabel); headerLayout->addWidget(m_checkButton); headerLayout->addWidget(m_toggleButton); m_mainLayout->addLayout(headerLayout); m_mainLayout->addWidget(m_summaryLabel); m_mainLayout->addWidget(m_itemsWidget); } void DiagnoseWidget::addDependencyItem(const QString &name, const QString &description, bool optional) { DependencyItem *item = new DependencyItem(name, description, optional); item->setProperty("name", name); item->setProperty("optional", optional); connect(item, &DependencyItem::installRequested, this, &DiagnoseWidget::onInstallRequested); m_dependencyItems[name] = item; // Insert before the stretch m_itemsLayout->insertWidget(m_itemsLayout->count() - 1, item); } void DiagnoseWidget::checkDependencies(bool autoExpand) { m_summaryLabel->setText("Checking system dependencies..."); m_checkButton->setEnabled(false); for (DependencyItem *item : m_dependencyItems) { item->setChecking(true); } QTimer::singleShot(500, [this, autoExpand]() { int installedCount = 0; int totalCount = 0; int optionalInstalledCount = 0; int optionalTotalCount = 0; for (DependencyItem *item : m_dependencyItems) { SERVICE_AVAILABILITY installed = SERVICE_UNAVAILABLE; QString itemName = item->property("name").toString(); #ifdef WIN32 if (itemName == "Bonjour Service") { installed = IsBonjourServiceInstalled(); } else if (itemName == "Apple Mobile Device Support") { installed = IsAppleMobileDeviceSupportInstalled(); } else if (itemName == "WinFsp") { installed = IsWinFspInstalled(); } #endif #ifdef __linux__ if (itemName == "UDEV rules") { installed = checkUdevRulesInstalled(); } else if (itemName == "Avahi Daemon") { installed = checkAvahiDaemonRunning(); } #endif bool isRequired = item->property("optional").toBool() == false; if (!isRequired) { ++optionalTotalCount; if (installed == SERVICE_AVAILABILITY::SERVICE_AVAILABLE) { ++optionalInstalledCount; } } else { ++totalCount; if (installed == SERVICE_AVAILABILITY::SERVICE_AVAILABLE) { ++installedCount; } } item->setInstalled(installed, isRequired); } if (installedCount == totalCount) { if (optionalTotalCount != optionalInstalledCount) { int optionalMissingCount = optionalTotalCount - optionalInstalledCount; QString optionalText = optionalMissingCount == 1 ? "optional capability is" : "optional capabilities are"; m_summaryLabel->setText(QString("%1 %2 available") .arg(optionalMissingCount) .arg(optionalText)); } else { m_summaryLabel->setText( "All required dependencies are installed"); } m_summaryLabel->setStyleSheet( QString("color: %1; font-weight: bold;") .arg(COLOR_GREEN.name())); if (m_isExpanded && autoExpand) { onToggleExpand(); } } else { m_summaryLabel->setText( QString("Missing required dependencies (%1/%2 installed)") .arg(installedCount) .arg(totalCount)); m_summaryLabel->setStyleSheet( QString("color: %1; font-weight: bold;").arg(COLOR_RED.name())); if (!m_isExpanded && autoExpand) { onToggleExpand(); } } m_checkButton->setEnabled(true); }); } void DiagnoseWidget::onInstallRequested(const QString &name) { DependencyItem *itemToInstall = m_dependencyItems.value(name); if (!itemToInstall) { QMessageBox::warning(this, "Error", "Dependency item not found: " + name); return; } SERVICE_AVAILABILITY availability = itemToInstall->availability(); #ifdef WIN32 if (name == "Bonjour Service") { if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { itemToInstall->setActivating(true); QProcess *proc = new QProcess(this); connect( proc, &QProcess::finished, this, [this, proc, itemToInstall](int exitCode, QProcess::ExitStatus status) { itemToInstall->setActivating(false); if (status != QProcess::NormalExit || exitCode != 0) { QString err = proc->readAllStandardError(); if (err.isEmpty()) err = proc->readAllStandardOutput(); QMessageBox::warning( this, "Activation Failed", "Failed to start Bonjour Service.\n\nDetails:\n" + err.trimmed()); } checkDependencies(false); proc->deleteLater(); }); QString ps = "Set-Service -Name 'Bonjour Service' " "-StartupType Automatic; " "Start-Service -Name 'Bonjour Service'"; QStringList args; args << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-Command" << QString( "Start-Process -FilePath powershell.exe -Verb RunAs " "-ArgumentList \"%1\" -Wait") .arg(ps.replace("\"", "\\\"")); proc->start("powershell.exe", args); return; } installBonjourRuntime(); return; } if (name == "Apple Mobile Device Support") { if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { itemToInstall->setActivating(true); QProcess *proc = new QProcess(this); connect(proc, &QProcess::finished, this, [this, proc, itemToInstall](int exitCode, QProcess::ExitStatus status) { itemToInstall->setActivating(false); if (status != QProcess::NormalExit || exitCode != 0) { QString err = proc->readAllStandardError(); if (err.isEmpty()) err = proc->readAllStandardOutput(); QMessageBox::warning( this, "Activation Failed", "Failed to start Apple Mobile Device " "Service.\n\nDetails:\n" + err.trimmed()); } checkDependencies(false); proc->deleteLater(); }); QString ps = "Set-Service -Name 'Apple Mobile Device Service' " "-StartupType Automatic; " "Start-Service -Name 'Apple Mobile Device Service'"; QStringList args; args << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-Command" << QString( "Start-Process -FilePath powershell.exe -Verb RunAs " "-ArgumentList \"%1\" -Wait") .arg(ps.replace("\"", "\\\"")); proc->start("powershell.exe", args); return; } itemToInstall->setInstalling(true); QString scriptPath = QCoreApplication::applicationDirPath() + "/install-apple-drivers.ps1"; QProcess *installProcess = new QProcess(this); connect(installProcess, &QProcess::finished, this, [this, installProcess, itemToInstall](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode != 0) { QString errorOutput = installProcess->readAllStandardError(); if (errorOutput.isEmpty()) { errorOutput = installProcess->readAllStandardOutput(); } QMessageBox::warning( this, "Installation Failed", "This might be a " "permissions issue or an internal error.\n\n" "Details: " + errorOutput.trimmed()); } checkDependencies(false); installProcess->deleteLater(); }); QString command = QString("Start-Process -FilePath powershell.exe -Verb RunAs " "-ArgumentList '-NoProfile -ExecutionPolicy Bypass -File " "\"%1\"' -Wait") .arg(scriptPath); QStringList args; args << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-Command" << command; installProcess->start("powershell.exe", args); return; } if (name == "WinFsp") { if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { itemToInstall->setActivating(true); QProcess *proc = new QProcess(this); connect( proc, &QProcess::finished, this, [this, proc, itemToInstall](int exitCode, QProcess::ExitStatus status) { itemToInstall->setActivating(false); if (status != QProcess::NormalExit || exitCode != 0) { QString err = proc->readAllStandardError(); if (err.isEmpty()) err = proc->readAllStandardOutput(); QMessageBox::warning( this, "Activation Failed", "Failed to start WinFsp.Launcher.\n\nDetails:\n" + err.trimmed()); } checkDependencies(false); proc->deleteLater(); }); // Use single quotes around the service name; no Read-Host QString ps = "Set-Service -Name 'WinFsp.Launcher' " "-StartupType Automatic; " "Start-Service -Name 'WinFsp.Launcher'"; QStringList args; args << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-Command" << QString( "Start-Process -FilePath powershell.exe -Verb RunAs " "-ArgumentList \"%1\" -Wait") .arg(ps.replace("\"", "\\\"")); proc->start("powershell.exe", args); return; } itemToInstall->setInstalling(true); QString scriptPath = QCoreApplication::applicationDirPath() + "/install-win-fsp.silent.bat"; QProcess *installProcess = new QProcess(this); connect( installProcess, &QProcess::finished, this, [this, installProcess](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode != 0) { QMessageBox::warning( this, "Installation Failed", "The installation script failed to run correctly. " "This might be because the action was cancelled or an " "error occurred.\n\nPlease try again."); } checkDependencies(false); installProcess->deleteLater(); }); QStringList args; args << "-NoProfile" << "-ExecutionPolicy" << "Bypass" << "-Command" << QString("Start-Process -FilePath \"%1\" -Verb RunAs -Wait;") .arg(scriptPath); installProcess->start("powershell.exe", args); } #endif #ifdef __linux__ if (name == "UDEV rules") { QMessageBox msgBox(this); msgBox.setWindowTitle("Manual configuration required"); msgBox.setText( "USB device permissions are required for recovery " "devices.\n\n" "Due to the variety of Linux distributions and package managers, " "you should configure these permissions manually.\n\n" "Please refer to the UDEV.md file in the project repository for " "detailed instructions."); msgBox.setInformativeText( "Would you like to open the instructions now?"); QPushButton *openButton = msgBox.addButton("Open Instructions", QMessageBox::ActionRole); msgBox.addButton("Close", QMessageBox::RejectRole); msgBox.exec(); if (msgBox.clickedButton() == openButton) { QDesktopServices::openUrl(QUrl( "https://github.com/uncor3/iDescriptor/blob/main/UDEV.md")); } } if (name == "Avahi Daemon") { if (availability == SERVICE_AVAILABLE_BUT_NOT_RUNNING) { QProcess *installProcess = new QProcess(this); connect( installProcess, &QProcess::finished, this, [this, installProcess, itemToInstall](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus != QProcess::NormalExit || exitCode != 0) { QString errorOutput = installProcess->readAllStandardError(); if (errorOutput.isEmpty()) { errorOutput = installProcess->readAllStandardOutput(); } QMessageBox::warning(this, "Error", "Failed to enable Avahi daemon. " "This might be because the action " "was cancelled or an " "error occurred.\n\nDetails: " + errorOutput.trimmed()); checkDependencies(false); } else { checkDependencies(false); } itemToInstall->setInstalling(false); installProcess->deleteLater(); }); QStringList args; args << "systemctl" << "enable" << "--now" << "avahi-daemon.service"; installProcess->start("pkexec", args); return; } QMessageBox::information(this, "Avahi Daemon", "The Avahi daemon is responsible for network " "service discovery and is required for " "AirPlay and wireless device features.\n\n" "Please use your distribution's package " "manager to install 'avahi'"); } #endif } #ifdef __linux__ SERVICE_AVAILABILITY DiagnoseWidget::checkUdevRulesInstalled() { // Check if udev rules file exists QFile rulesFile("/etc/udev/rules.d/99-idevice.rules"); if (!rulesFile.exists()) { return UNABLE_TO_CHECK; } // Check if the file contains the correct rule if (!rulesFile.open(QIODevice::ReadOnly | QIODevice::Text)) { return UNABLE_TO_CHECK; } QTextStream in(&rulesFile); QString content = in.readAll(); rulesFile.close(); // Check for the essential parts of the rule bool hasUsbSubsystem = content.contains("SUBSYSTEM==\"usb\""); bool hasAppleVendor = content.contains("ATTR{idVendor}==\"05ac\""); bool hasMode = content.contains("MODE=\"0666\""); if (!hasUsbSubsystem || !hasAppleVendor || !hasMode) { return UNABLE_TO_CHECK; } // Check if current user is in the idevice group QProcess groupsProcess; groupsProcess.start("groups"); groupsProcess.waitForFinished(3000); if (groupsProcess.exitCode() != 0) { // If we can't check groups, consider it not installed return UNABLE_TO_CHECK; } QString groupsOutput = QString::fromUtf8(groupsProcess.readAllStandardOutput()).trimmed(); QStringList groups = groupsOutput.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); bool isInIdeviceGroup = groups.contains("idevice"); return isInIdeviceGroup ? SERVICE_AVAILABLE : UNABLE_TO_CHECK; } SERVICE_AVAILABILITY DiagnoseWidget::checkAvahiDaemonRunning() { // Connect to the system bus QDBusConnection systemBus = QDBusConnection::systemBus(); if (!systemBus.isConnected()) { return UNABLE_TO_CHECK; } QDBusConnectionInterface *iface = systemBus.interface(); if (!iface) { return UNABLE_TO_CHECK; } // Avahi daemon D-Bus name const QString avahiService = QStringLiteral("org.freedesktop.Avahi"); // If the service is registered, Avahi is running if (iface->isServiceRegistered(avahiService).value()) { return SERVICE_AVAILABLE; } // maybe installed ? bool hasBinary = !QStandardPaths::findExecutable(QStringLiteral("avahi-browse")) .isEmpty(); return hasBinary ? SERVICE_AVAILABLE_BUT_NOT_RUNNING : SERVICE_UNAVAILABLE; } #endif void DiagnoseWidget::onToggleExpand() { m_isExpanded = !m_isExpanded; m_itemsWidget->setVisible(m_isExpanded); m_toggleButton->setIcon( m_isExpanded ? QIcon(":/resources/icons/MaterialSymbolsLightKeyboardArrowUp.png") : QIcon(":/resources/icons/" "MaterialSymbolsLightKeyboardArrowDown.png")); m_itemsWidget->updateGeometry(); adjustSize(); } #ifdef WIN32 void DiagnoseWidget::installBonjourRuntime() { DependencyItem *itemToInstall = m_dependencyItems.value("Bonjour Service"); if (!itemToInstall) return; itemToInstall->setInstalling(true); itemToInstall->setProgress("Downloading..."); // Download Bonjour SDK QNetworkAccessManager *manager = new QNetworkAccessManager(this); QNetworkRequest request( QUrl("https://github.com/tempx-x/bonjour-sdk/raw/refs/heads/main/" "bonjoursdksetup.exe")); QNetworkReply *reply = manager->get(request); connect(reply, &QNetworkReply::downloadProgress, this, [itemToInstall](qint64 bytesReceived, qint64 bytesTotal) { if (bytesTotal > 0) { int percent = (bytesReceived * 100) / bytesTotal; itemToInstall->setProgress( QString("Downloading... %1%").arg(percent)); } }); connect( reply, &QNetworkReply::finished, this, [this, reply, manager, itemToInstall]() { reply->deleteLater(); manager->deleteLater(); if (reply->error() != QNetworkReply::NoError) { QMessageBox::critical(this, "Download Failed", "Failed to download Bonjour SDK: " + reply->errorString()); checkDependencies(false); return; } itemToInstall->setProgress("Verifying..."); // Verify MD5 checksum QByteArray data = reply->readAll(); QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Md5); QString actualHash = hash.toHex(); QString expectedHash = "4ff2aae8205aec31b06743782cfcadce"; if (actualHash != expectedHash) { QMessageBox::critical( this, "Checksum Mismatch", QString("Downloaded file checksum does not match!\n" "Expected: %1\n" "Got: %2") .arg(expectedHash, actualHash)); checkDependencies(false); return; } itemToInstall->setProgress("Extracting..."); // Create temp directory QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/bonjour_install"; QDir().mkpath(tempDir); // Save the downloaded file QString exePath = tempDir + "/bonjoursdksetup.exe"; QFile file(exePath); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::critical(this, "Error", "Failed to save downloaded file"); checkDependencies(false); return; } file.write(data); file.close(); // Extract using libarchive struct archive *a = archive_read_new(); archive_read_support_format_all(a); archive_read_support_filter_all(a); struct archive *ext = archive_write_disk_new(); archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME); if (archive_read_open_filename(a, exePath.toUtf8().constData(), 10240) != ARCHIVE_OK) { QMessageBox::critical(this, "Extraction Failed", QString("Failed to open archive: %1") .arg(archive_error_string(a))); archive_read_free(a); archive_write_free(ext); checkDependencies(false); return; } struct archive_entry *entry; QString msiPath; while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { QString entryName = QString::fromUtf8(archive_entry_pathname(entry)); if (entryName.endsWith("Bonjour64.msi", Qt::CaseInsensitive)) { QString fullPath = tempDir + "/" + entryName; archive_entry_set_pathname(entry, fullPath.toUtf8().constData()); if (archive_write_header(ext, entry) != ARCHIVE_OK) { qWarning() << "Failed to write header for" << entryName; } else { const void *buff; size_t size; la_int64_t offset; while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) { archive_write_data_block(ext, buff, size, offset); } } archive_write_finish_entry(ext); msiPath = fullPath; break; // Only need Bonjour64.msi } else { archive_read_data_skip(a); } } archive_read_free(a); archive_write_free(ext); if (msiPath.isEmpty()) { QMessageBox::critical(this, "Extraction Failed", "Could not find Bonjour64.msi in the " "archive"); QDir(tempDir).removeRecursively(); checkDependencies(false); return; } itemToInstall->setProgress("Installing..."); // Launch the MSI via the shell (same behavior as double-click) itemToInstall->setInstalling(false); // we can't track MSI process if (!QDesktopServices::openUrl(QUrl::fromLocalFile(msiPath))) { QMessageBox::warning(this, "Installation Failed", "Failed to launch Bonjour installer.\n\n" "You can also run it manually from:\n" + msiPath); checkDependencies(false); return; } QMessageBox::information( this, "Installation Started", "The Bonjour installer has been launched.\n" "Please complete the setup, then re-run the dependency check."); itemToInstall->setProgress("Refresh to verify installation."); }); } #endif