mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
udev rule checks for recovery devices
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
#include "diagnosewidget.h"
|
||||
#ifdef WIN32
|
||||
#include "platform/windows/check_deps.h"
|
||||
#endif
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
DependencyItem::DependencyItem(const QString &name, const QString &description,
|
||||
QWidget *parent)
|
||||
: QWidget(parent), m_name(name)
|
||||
{
|
||||
setFixedHeight(80);
|
||||
|
||||
QHBoxLayout *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(10, 5, 10, 5);
|
||||
|
||||
// Left side - info
|
||||
QVBoxLayout *infoLayout = new QVBoxLayout();
|
||||
|
||||
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(description);
|
||||
m_descriptionLabel->setWordWrap(true);
|
||||
|
||||
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);
|
||||
actionLayout->addStretch();
|
||||
|
||||
layout->addLayout(infoLayout, 1);
|
||||
layout->addWidget(m_statusLabel);
|
||||
layout->addLayout(actionLayout);
|
||||
}
|
||||
|
||||
void DependencyItem::setInstalled(bool installed)
|
||||
{
|
||||
setChecking(false);
|
||||
|
||||
if (installed) {
|
||||
m_statusLabel->setText("✓ Installed");
|
||||
m_statusLabel->setStyleSheet("color: green; font-weight: bold;");
|
||||
m_installButton->setVisible(false);
|
||||
} else {
|
||||
m_statusLabel->setText("✗ Not Installed");
|
||||
m_statusLabel->setStyleSheet("color: red; font-weight: bold;");
|
||||
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::onInstallClicked() { emit installRequested(m_name); }
|
||||
|
||||
DiagnoseWidget::DiagnoseWidget(QWidget *parent)
|
||||
: QWidget(parent), m_isExpanded(false)
|
||||
{
|
||||
setupUI();
|
||||
|
||||
#ifdef WIN32
|
||||
// Add dependency items
|
||||
addDependencyItem("Apple Mobile Device Support",
|
||||
"Required for iOS device communication");
|
||||
addDependencyItem("WinFsp",
|
||||
"Required for filesystem operations and mounting");
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
// Add Linux-specific dependency items
|
||||
addDependencyItem("USB Device Permissions",
|
||||
"Required for recovery device access (udev rules)");
|
||||
#endif
|
||||
|
||||
// Auto-check on startup
|
||||
QTimer::singleShot(100, this, [this]() { checkDependencies(); });
|
||||
}
|
||||
|
||||
void DiagnoseWidget::setupUI()
|
||||
{
|
||||
setAutoFillBackground(true);
|
||||
m_mainLayout = new QVBoxLayout(this);
|
||||
m_mainLayout->setSpacing(10);
|
||||
|
||||
// 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(150);
|
||||
connect(m_checkButton, &QPushButton::clicked, this,
|
||||
[this]() { checkDependencies(false); });
|
||||
|
||||
// Toggle button
|
||||
m_toggleButton = new QPushButton("▼");
|
||||
m_toggleButton->setFixedSize(24, 24);
|
||||
m_toggleButton->setCheckable(true);
|
||||
connect(m_toggleButton, &QPushButton::clicked, this,
|
||||
&DiagnoseWidget::onToggleExpand);
|
||||
|
||||
m_itemsWidget = new QWidget();
|
||||
// m_itemsWidget->setSizePolicy(QSizePolicy::Expanding,
|
||||
// QSizePolicy::Preferred);
|
||||
m_itemsWidget->setFixedHeight(400);
|
||||
m_itemsLayout = new QVBoxLayout(m_itemsWidget);
|
||||
m_itemsLayout->setSpacing(5);
|
||||
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)
|
||||
{
|
||||
DependencyItem *item = new DependencyItem(name, description);
|
||||
item->setProperty("name", name);
|
||||
connect(item, &DependencyItem::installRequested, this,
|
||||
&DiagnoseWidget::onInstallRequested);
|
||||
|
||||
m_dependencyItems.append(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 = m_dependencyItems.size();
|
||||
|
||||
for (DependencyItem *item : m_dependencyItems) {
|
||||
bool installed = false;
|
||||
QString itemName = item->property("name").toString();
|
||||
|
||||
#ifdef WIN32
|
||||
if (itemName == "Apple Mobile Device Support") {
|
||||
installed = IsAppleMobileDeviceSupportInstalled();
|
||||
} else if (itemName == "WinFsp") {
|
||||
installed = IsWinFspInstalled();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
if (itemName == "USB Device Permissions") {
|
||||
installed = checkUdevRulesInstalled();
|
||||
}
|
||||
#endif
|
||||
|
||||
item->setInstalled(installed);
|
||||
if (installed)
|
||||
installedCount++;
|
||||
}
|
||||
|
||||
if (installedCount == totalCount) {
|
||||
m_summaryLabel->setText(
|
||||
QString("All dependencies are installed (%1/%2)")
|
||||
.arg(installedCount)
|
||||
.arg(totalCount));
|
||||
m_summaryLabel->setStyleSheet("color: green; font-weight: bold;");
|
||||
if (m_isExpanded && autoExpand) {
|
||||
onToggleExpand();
|
||||
}
|
||||
} else {
|
||||
m_summaryLabel->setText(
|
||||
QString("Missing dependencies (%1/%2 installed)")
|
||||
.arg(installedCount)
|
||||
.arg(totalCount));
|
||||
m_summaryLabel->setStyleSheet("color: red; font-weight: bold;");
|
||||
if (!m_isExpanded && autoExpand) {
|
||||
onToggleExpand();
|
||||
}
|
||||
}
|
||||
|
||||
m_checkButton->setEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
void DiagnoseWidget::onInstallRequested(const QString &name)
|
||||
{
|
||||
#ifdef WIN32
|
||||
if (name == "Apple Mobile Device Support") {
|
||||
DependencyItem *itemToInstall = nullptr;
|
||||
for (DependencyItem *item : m_dependencyItems) {
|
||||
if (item->property("name").toString() == name) {
|
||||
itemToInstall = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemToInstall)
|
||||
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",
|
||||
"Failed to launch the installation script. This "
|
||||
"might be a "
|
||||
"permissions issue or an internal error.\n\n"
|
||||
"Details: " +
|
||||
errorOutput.trimmed());
|
||||
checkDependencies(false); // Revert UI on failure
|
||||
} else {
|
||||
// FIXME: we need to track process completion
|
||||
QMessageBox::information(
|
||||
this, "Installation Started",
|
||||
"The installation process has been started.\n"
|
||||
"Please approve the administrator prompt (UAC) if it "
|
||||
"appears.\n"
|
||||
"After installation, please re-run the dependency "
|
||||
"check");
|
||||
|
||||
itemToInstall->setInstalling(false);
|
||||
}
|
||||
installProcess->deleteLater();
|
||||
});
|
||||
|
||||
// Correctly launch powershell.exe elevated, and pass the script to it.
|
||||
// The -Wait parameter is removed as it does not work with -Verb RunAs.
|
||||
QString command =
|
||||
QString("Start-Process -FilePath powershell.exe -Verb RunAs "
|
||||
"-ArgumentList '-NoProfile -ExecutionPolicy Bypass -File "
|
||||
"\"%1\"'")
|
||||
.arg(scriptPath);
|
||||
|
||||
QStringList args;
|
||||
args << "-NoProfile"
|
||||
<< "-ExecutionPolicy"
|
||||
<< "Bypass"
|
||||
<< "-Command" << command;
|
||||
|
||||
installProcess->start("powershell.exe", args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "WinFsp") {
|
||||
DependencyItem *itemToInstall = nullptr;
|
||||
for (DependencyItem *item : m_dependencyItems) {
|
||||
if (item->property("name").toString() == name) {
|
||||
itemToInstall = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemToInstall)
|
||||
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 == "USB Device Permissions") {
|
||||
DependencyItem *itemToInstall = nullptr;
|
||||
for (DependencyItem *item : m_dependencyItems) {
|
||||
if (item->property("name").toString() == name) {
|
||||
itemToInstall = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemToInstall)
|
||||
return;
|
||||
|
||||
itemToInstall->setInstalling(true);
|
||||
|
||||
// Create a temporary script to set up udev rules
|
||||
QString scriptPath = QDir::temp().filePath("setup-idevice-udev.sh");
|
||||
QFile scriptFile(scriptPath);
|
||||
|
||||
if (!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::critical(this, "Error",
|
||||
"Failed to create installation script");
|
||||
itemToInstall->setInstalling(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: maybe, can be handled better
|
||||
QTextStream out(&scriptFile);
|
||||
out << "#!/bin/bash\n";
|
||||
out << "set -e\n\n";
|
||||
out << "# Create udev rules file\n";
|
||||
out << "echo 'SUBSYSTEM==\"usb\", ATTR{idVendor}==\"05ac\", "
|
||||
"MODE=\"0666\"' | tee /etc/udev/rules.d/99-idevice.rules > "
|
||||
"/dev/null\n\n";
|
||||
out << "# Create idevice group if it doesn't exist\n";
|
||||
out << "if ! getent group idevice > /dev/null 2>&1; then\n";
|
||||
out << " groupadd idevice\n";
|
||||
out << "fi\n\n";
|
||||
out << "# Add current user to idevice group\n";
|
||||
out << "usermod -aG idevice $SUDO_USER\n\n";
|
||||
out << "# Reload udev rules\n";
|
||||
out << "udevadm control --reload-rules\n";
|
||||
out << "udevadm trigger\n\n";
|
||||
out << "echo 'USB device permissions configured successfully!'\n";
|
||||
out << "echo 'Note: You may need to log out and log back in for group "
|
||||
"changes to take effect.'\n";
|
||||
scriptFile.close();
|
||||
|
||||
// Make script executable
|
||||
QFile::setPermissions(scriptPath, QFileDevice::ReadOwner |
|
||||
QFileDevice::WriteOwner |
|
||||
QFileDevice::ExeOwner);
|
||||
|
||||
QProcess *installProcess = new QProcess(this);
|
||||
connect(
|
||||
installProcess, &QProcess::finished, this,
|
||||
[this, installProcess, itemToInstall,
|
||||
scriptPath](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
// Clean up temporary script
|
||||
QFile::remove(scriptPath);
|
||||
|
||||
if (exitStatus != QProcess::NormalExit || exitCode != 0) {
|
||||
QString errorOutput =
|
||||
installProcess->readAllStandardError();
|
||||
if (errorOutput.isEmpty()) {
|
||||
errorOutput = installProcess->readAllStandardOutput();
|
||||
}
|
||||
QMessageBox::warning(
|
||||
this, "Installation Failed",
|
||||
"Failed to configure USB device permissions. "
|
||||
"This might be because the action was cancelled or an "
|
||||
"error occurred.\n\nDetails: " +
|
||||
errorOutput.trimmed());
|
||||
checkDependencies(false);
|
||||
} else {
|
||||
QMessageBox::information(
|
||||
this, "Installation Complete",
|
||||
"USB device permissions have been configured.\n\n"
|
||||
"Note: You may need to log out and log back in for "
|
||||
"group membership changes to take full effect.");
|
||||
checkDependencies(false);
|
||||
}
|
||||
itemToInstall->setInstalling(false);
|
||||
installProcess->deleteLater();
|
||||
});
|
||||
|
||||
QStringList args;
|
||||
args << scriptPath;
|
||||
installProcess->start("pkexec", args);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
bool DiagnoseWidget::checkUdevRulesInstalled()
|
||||
{
|
||||
// Check if udev rules file exists
|
||||
QFile rulesFile("/etc/udev/rules.d/99-idevice.rules");
|
||||
if (!rulesFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the file contains the correct rule
|
||||
if (!rulesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
|
||||
// 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 false;
|
||||
}
|
||||
|
||||
QString groupsOutput =
|
||||
QString::fromUtf8(groupsProcess.readAllStandardOutput()).trimmed();
|
||||
QStringList groups =
|
||||
groupsOutput.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
|
||||
bool isInIdeviceGroup = groups.contains("idevice");
|
||||
|
||||
return isInIdeviceGroup;
|
||||
}
|
||||
#endif
|
||||
|
||||
void DiagnoseWidget::onToggleExpand()
|
||||
{
|
||||
m_isExpanded = !m_isExpanded;
|
||||
m_itemsWidget->setVisible(m_isExpanded);
|
||||
m_toggleButton->setText(m_isExpanded ? "▲" : "▼");
|
||||
}
|
||||
Reference in New Issue
Block a user