Files
iDescriptor/src/diagnosewidget.cpp
T

914 lines
33 KiB
C++

/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "diagnosewidget.h"
#ifdef WIN32
#include "platform/windows/win_common.h"
#include <archive.h>
#include <archive_entry.h>
#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