From bb6b5775269b2942f154046c3d311ea434b4a1ca Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sat, 11 Oct 2025 05:36:26 +0000 Subject: [PATCH] implement ServiceManager to handle services safely & cleanup UI - Added `install_ipa.cpp` to handle IPA installation on iOS devices using the installation proxy. - Introduced `ServiceManager` class to centralize and thread-safely manage device service operations, including AFC file operations. - Updated `DeviceInfoWidget` layout for improved UI responsiveness. - Refactored `GalleryWidget`, `MediaStreamer`, `PhotoExportManager`, and `PhotoModel` to utilize `ServiceManager` for safer AFC operations. - Enhanced error handling and logging across various components. - Adjusted `ToolboxWidget` to streamline device change handling and UI updates. --- CMakeLists.txt | 2 + src/afcexplorerwidget.cpp | 12 +- src/appcontext.cpp | 34 +- src/appcontext.h | 8 + src/appdownloadbasedialog.cpp | 68 ++- src/appdownloadbasedialog.h | 12 +- src/appdownloaddialog.cpp | 8 +- src/appinstalldialog.cpp | 173 ++++-- src/appinstalldialog.h | 10 + src/appswidget.cpp | 23 +- src/appswidget.h | 3 +- src/core/helpers/safe_afc_read_directory.cpp | 21 - src/core/services/get-device-info.cpp | 27 +- .../{helpers => services}/get_file_tree.cpp | 4 +- src/core/services/init_device.cpp | 253 ++++----- src/core/services/install_ipa.cpp | 526 ++++++++++++++++++ src/deviceinfowidget.cpp | 23 +- src/gallerywidget.cpp | 3 +- src/iDescriptor.h | 27 +- src/installedappswidget.cpp | 4 + src/mediastreamer.cpp | 27 +- src/photoexportmanager.cpp | 42 +- src/photoexportmanager.h | 3 +- src/photomodel.cpp | 25 +- src/servicemanager.cpp | 85 +++ src/servicemanager.h | 136 +++++ src/toolboxwidget.cpp | 17 +- src/toolboxwidget.h | 1 + 28 files changed, 1234 insertions(+), 343 deletions(-) delete mode 100644 src/core/helpers/safe_afc_read_directory.cpp rename src/core/{helpers => services}/get_file_tree.cpp (94%) create mode 100644 src/core/services/install_ipa.cpp create mode 100644 src/servicemanager.cpp create mode 100644 src/servicemanager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a31ccfa..b6d33cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,7 @@ find_library(TATSU_LIBRARY # Add QR code generation library pkg_check_modules(QRENCODE REQUIRED IMPORTED_TARGET libqrencode) pkg_check_modules(HEIF REQUIRED IMPORTED_TARGET libheif) +pkg_check_modules(ZIP REQUIRED IMPORTED_TARGET libzip) find_library(IRECOVERY_LIBRARY NAMES irecovery-1.0 PATHS ${CUSTOM_LIB_PATH} @@ -238,6 +239,7 @@ target_link_libraries(iDescriptor PRIVATE PkgConfig::QRENCODE PkgConfig::QTERMWIDGET PkgConfig::HEIF + PkgConfig::ZIP airplay ipatool-go ) diff --git a/src/afcexplorerwidget.cpp b/src/afcexplorerwidget.cpp index c773a9b..2a7123f 100644 --- a/src/afcexplorerwidget.cpp +++ b/src/afcexplorerwidget.cpp @@ -2,6 +2,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" #include "mediapreviewdialog.h" +#include "servicemanager.h" #include "settingsmanager.h" #include #include @@ -184,7 +185,7 @@ void AfcExplorerWidget::loadPath(const QString &path) updateAddressBar(path); AFCFileTree tree = - get_file_tree(m_currentAfcClient, m_device->device, path.toStdString()); + ServiceManager::safeGetFileTree(m_device, path.toStdString()); if (!tree.success) { m_fileList->addItem("Failed to load directory"); return; @@ -325,8 +326,8 @@ int AfcExplorerWidget::export_file_to_path(afc_client_t afc, const char *local_path) { uint64_t handle = 0; - if (afc_file_open(afc, device_path, AFC_FOPEN_RDONLY, &handle) != - AFC_E_SUCCESS) { + if (ServiceManager::safeAfcFileOpen(m_device, device_path, AFC_FOPEN_RDONLY, + &handle) != AFC_E_SUCCESS) { qDebug() << "Failed to open file on device:" << device_path; return -1; } @@ -339,8 +340,9 @@ int AfcExplorerWidget::export_file_to_path(afc_client_t afc, char buffer[4096]; uint32_t bytes_read = 0; - while (afc_file_read(afc, handle, buffer, sizeof(buffer), &bytes_read) == - AFC_E_SUCCESS && + while (ServiceManager::safeAfcFileRead(m_device, handle, buffer, + sizeof(buffer), + &bytes_read) == AFC_E_SUCCESS && bytes_read > 0) { fwrite(buffer, 1, bytes_read, out); } diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 50e3a36..f525905 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -65,7 +65,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, AddType addType) { try { - IDescriptorInitDeviceResult initResult = + iDescriptorInitDeviceResult initResult = init_idescriptor_device(udid.toStdString().c_str()); qDebug() << "init_idescriptor_device success ?: " << initResult.success; @@ -78,6 +78,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, if (addType == AddType::Regular) { m_pendingDevices.append(udid); emit devicePasswordProtected(udid); + emit deviceChange(); QTimer::singleShot(30000, this, [this, udid]() { // After 30 seconds, if the device is still pending, // consider the pairing expired @@ -88,6 +89,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, << "Pairing expired for device UDID: " << udid; m_pendingDevices.removeAll(udid); emit devicePairingExpired(udid); + emit deviceChange(); } }); } @@ -95,6 +97,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING) { m_pendingDevices.append(udid); emit devicePairPending(udid); + emit deviceChange(); QTimer::singleShot(30000, this, [this, udid]() { // After 30 seconds, if the device is still pending, // consider the pairing expired @@ -103,6 +106,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, qDebug() << "Pairing expired for device UDID: " << udid; m_pendingDevices.removeAll(udid); emit devicePairingExpired(udid); + emit deviceChange(); } }); } else { @@ -120,11 +124,16 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, .deviceInfo = initResult.deviceInfo, .afcClient = initResult.afcClient, .afc2Client = initResult.afc2Client, + .mutex = new std::recursive_mutex(), }; m_devices[device->udid] = device; - if (addType == AddType::Regular) - return emit deviceAdded(device); + if (addType == AddType::Regular) { + emit deviceAdded(device); + emit deviceChange(); + return; + } emit devicePaired(device); + emit deviceChange(); m_pendingDevices.removeAll(udid); } catch (const std::exception &e) { @@ -137,16 +146,22 @@ int AppContext::getConnectedDeviceCount() const return m_devices.size() + m_recoveryDevices.size(); } +/* + FIXME: + on macOS, sometimes you get wireless disconnects even though we are not + listening for wireless devices it does not have any to do with us, but it + still happens so be aware of that +*/ void AppContext::removeDevice(QString _udid) - { const std::string uuid = _udid.toStdString(); qDebug() << "AppContext::removeDevice device with UUID:" << QString::fromStdString(uuid); if (m_pendingDevices.contains(_udid)) { - m_pendingDevices.removeAll(_udid); emit devicePairingExpired(_udid); + emit deviceChange(); + m_pendingDevices.removeAll(_udid); return; } else { qDebug() << "Device with UUID " + _udid + @@ -163,9 +178,14 @@ void AppContext::removeDevice(QString _udid) m_devices.remove(uuid); emit deviceRemoved(uuid); + emit deviceChange(); + + std::lock_guard lock(*device->mutex); + if (device->afcClient) afc_client_free(device->afcClient); idevice_free(device->device); + delete device->mutex; delete device; } @@ -181,6 +201,7 @@ void AppContext::removeRecoveryDevice(uint64_t ecid) m_recoveryDevices.remove(ecid); emit recoveryDeviceRemoved(ecid); + emit deviceChange(); iDescriptorRecoveryDevice *deviceInfo = m_recoveryDevices[ecid]; delete deviceInfo; @@ -210,7 +231,7 @@ bool AppContext::noDevicesConnected() const void AppContext::addRecoveryDevice(uint64_t ecid) { - IDescriptorInitDeviceResultRecovery res = + iDescriptorInitDeviceResultRecovery res = init_idescriptor_recovery_device(ecid); if (!res.success) { @@ -229,6 +250,7 @@ void AppContext::addRecoveryDevice(uint64_t ecid) m_recoveryDevices[res.deviceInfo.ecid] = recoveryDevice; emit recoveryDeviceAdded(recoveryDevice); + emit deviceChange(); } AppContext::~AppContext() diff --git a/src/appcontext.h b/src/appcontext.h index b5923a7..828abb2 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -37,6 +37,14 @@ signals: void devicePairingExpired(const QString &udid); void systemSleepStarting(); void systemWakeup(); + /* + Generic change event for any device state change we + need this because many UI elements need to update by + listening for this only you can watch for any event + and using the public members of this class you can + do anything you want + */ + void deviceChange(); public slots: void removeDevice(QString udid); void addDevice(QString udid, idevice_connection_type connType, diff --git a/src/appdownloadbasedialog.cpp b/src/appdownloadbasedialog.cpp index d0a5400..e069d91 100644 --- a/src/appdownloadbasedialog.cpp +++ b/src/appdownloadbasedialog.cpp @@ -32,6 +32,7 @@ void AppDownloadBaseDialog::addProgressBar(int index) } AppDownloadBaseDialog::AppDownloadBaseDialog(const QString &appName, + const QString &bundleId, QWidget *parent) : QDialog(parent), m_appName(appName), m_downloadProcess(nullptr), m_progressTimer(nullptr) @@ -42,8 +43,7 @@ AppDownloadBaseDialog::AppDownloadBaseDialog(const QString &appName, m_layout->setContentsMargins(30, 30, 30, 30); QLabel *nameLabel = new QLabel(appName); - nameLabel->setStyleSheet( - "font-size: 20px; font-weight: bold; color: #333;"); + nameLabel->setStyleSheet("font-size: 20px; font-weight: bold;"); m_layout->addWidget(nameLabel); m_actionButton = nullptr; // Derived classes set this @@ -51,7 +51,8 @@ AppDownloadBaseDialog::AppDownloadBaseDialog(const QString &appName, void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId, const QString &outputDir, - int index) + int index, + bool promptToOpenDir) { bool acquireLicense = true; @@ -65,6 +66,8 @@ void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId, if (m_actionButton) m_actionButton->setEnabled(false); + m_operationInProgress = true; + AppStoreManager *manager = AppStoreManager::sharedInstance(); if (!manager) { QMessageBox::critical(this, "Error", @@ -83,28 +86,34 @@ void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId, manager->downloadApp( bundleId, outputDir, "", acquireLicense, - [this, outputDir](int result) { + [this, outputDir, promptToOpenDir](int result) { if (result == 0) { // Success + emit downloadFinished(true, "Success"); m_progressBar->setValue(100); - if (QMessageBox::Yes == - QMessageBox::question( - this, "Download Successful", - QString("Successfully downloaded. Would you like " - "to open the output directory: %1?") - .arg(outputDir))) { - QDir dir(outputDir); - if (!dir.exists()) { - QMessageBox::warning( - this, "Directory Not Found", - QString("The directory %1 does not exist.") - .arg(outputDir)); - } else { - QDesktopServices::openUrl( - QUrl::fromLocalFile(outputDir)); + if (promptToOpenDir) { + + if (QMessageBox::Yes == + QMessageBox::question( + this, "Download Successful", + QString("Successfully downloaded. Would you like " + "to open the output directory: %1?") + .arg(outputDir))) { + QDir dir(outputDir); + if (!dir.exists()) { + QMessageBox::warning( + this, "Directory Not Found", + QString("The directory %1 does not exist.") + .arg(outputDir)); + } else { + QDesktopServices::openUrl( + QUrl::fromLocalFile(outputDir)); + } } + accept(); } - accept(); } else { // Failure + emit downloadFinished(false, "Failed"); + // if (promptToOpenDir) QMessageBox::critical( this, "Download Failed", QString("Failed to download %1. Error code: %2") @@ -114,4 +123,23 @@ void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId, } }, progressCallback); +} + +void AppDownloadBaseDialog::reject() +{ + // FIXME: we need to cancel download if it gets closed + // if (m_operationInProgress) { + // AppStoreManager *manager = AppStoreManager::sharedInstance(); + // m_operationInProgress = false; + // } + + cleanup(); + QDialog::reject(); +} + +void AppDownloadBaseDialog::cleanup() +{ + if (m_progressTimer && m_progressTimer->isActive()) { + m_progressTimer->stop(); + } } \ No newline at end of file diff --git a/src/appdownloadbasedialog.h b/src/appdownloadbasedialog.h index 85b7167..9b323a1 100644 --- a/src/appdownloadbasedialog.h +++ b/src/appdownloadbasedialog.h @@ -12,14 +12,20 @@ class AppDownloadBaseDialog : public QDialog Q_OBJECT public: explicit AppDownloadBaseDialog(const QString &appName, + const QString &bundleId, QWidget *parent = nullptr); public slots: void updateProgressBar(int percentage); +signals: + void downloadFinished(bool success, const QString &message); + protected: + void reject() override; void startDownloadProcess(const QString &bundleId, - const QString &workingDir, int index); + const QString &workingDir, int index, + bool promptToOpenDir = true); void checkDownloadProgress(const QString &logFilePath, const QString &appName, const QString &outputDir); @@ -30,6 +36,10 @@ protected: QString m_appName; QPushButton *m_actionButton; QVBoxLayout *m_layout; + bool m_operationInProgress = false; + +private slots: + void cleanup(); }; #endif // APPDOWNLOADBASEDIALOG_H diff --git a/src/appdownloaddialog.cpp b/src/appdownloaddialog.cpp index a8f2301..de942ee 100644 --- a/src/appdownloaddialog.cpp +++ b/src/appdownloaddialog.cpp @@ -12,7 +12,7 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName, const QString &bundleId, const QString &description, QWidget *parent) - : AppDownloadBaseDialog(appName, parent), + : AppDownloadBaseDialog(appName, bundleId, parent), m_outputDir( QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)), m_bundleId(bundleId) @@ -32,7 +32,7 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName, // Directory selection UI QHBoxLayout *dirLayout = new QHBoxLayout(); QLabel *dirTextLabel = new QLabel("Save to:"); - dirTextLabel->setStyleSheet("font-size: 14px; color: #333;"); + dirTextLabel->setStyleSheet("font-size: 14px;"); dirLayout->addWidget(dirTextLabel); m_dirLabel = new ClickableLabel(this); @@ -60,9 +60,7 @@ AppDownloadDialog::AppDownloadDialog(const QString &appName, m_actionButton = new QPushButton("Download IPA"); m_actionButton->setFixedHeight(40); - m_actionButton->setStyleSheet( - "background-color: #34C759; color: white; border: none; border-radius: " - "6px; font-size: 16px; font-weight: bold;"); + m_actionButton->setDefault(true); connect(m_actionButton, &QPushButton::clicked, this, &AppDownloadDialog::onDownloadClicked); layout->addWidget(m_actionButton); diff --git a/src/appinstalldialog.cpp b/src/appinstalldialog.cpp index ac47671..12d0678 100644 --- a/src/appinstalldialog.cpp +++ b/src/appinstalldialog.cpp @@ -1,21 +1,25 @@ #include "appinstalldialog.h" #include "appcontext.h" #include "appdownloadbasedialog.h" +#include "iDescriptor.h" #include #include #include +#include #include #include #include #include +#include AppInstallDialog::AppInstallDialog(const QString &appName, - const QString &description, QWidget *parent) - : AppDownloadBaseDialog(appName, parent) + const QString &description, + const QString &bundleId, QWidget *parent) + : AppDownloadBaseDialog(appName, bundleId, parent), m_bundleId(bundleId), + m_statusLabel(nullptr), m_installWatcher(nullptr) { - setWindowTitle("Install " + appName); + setWindowTitle("Install " + appName + " - iDescriptor"); setModal(true); - // setFixedSize(500, 350); setFixedWidth(500); QVBoxLayout *layout = qobject_cast(this->layout()); @@ -31,13 +35,12 @@ AppInstallDialog::AppInstallDialog(const QString &appName, QVBoxLayout *detailsLayout = new QVBoxLayout(); QLabel *nameLabel = new QLabel(appName); - nameLabel->setStyleSheet( - "font-size: 20px; font-weight: bold; color: #333;"); + nameLabel->setStyleSheet("font-size: 20px; font-weight: bold;"); detailsLayout->addWidget(nameLabel); QLabel *descLabel = new QLabel(description); descLabel->setWordWrap(true); - descLabel->setStyleSheet("font-size: 14px; color: #666;"); + descLabel->setStyleSheet("font-size: 14px;"); detailsLayout->addWidget(descLabel); appInfoLayout->addLayout(detailsLayout); @@ -45,32 +48,22 @@ AppInstallDialog::AppInstallDialog(const QString &appName, layout->insertLayout(0, appInfoLayout); QLabel *deviceLabel = new QLabel("Choose Device:"); - deviceLabel->setStyleSheet( - "font-size: 16px; font-weight: bold; color: #333;"); + deviceLabel->setStyleSheet("font-size: 16px; font-weight: bold;"); layout->insertWidget(1, deviceLabel); m_deviceCombo = new QComboBox(); - m_deviceCombo->setStyleSheet("padding: 8px; border: 1px solid #ddd; " - "border-radius: 4px; font-size: 14px;"); - updateDeviceList(); layout->insertWidget(2, m_deviceCombo); + m_statusLabel = new QLabel("Ready to install"); + m_statusLabel->setStyleSheet("font-size: 14px; padding: 5px;"); + m_statusLabel->setAlignment(Qt::AlignCenter); + layout->insertWidget(3, m_statusLabel); + layout->addStretch(); m_actionButton = new QPushButton("Install"); m_actionButton->setFixedHeight(40); - bool hasDevices = m_deviceCombo->count() > 0; - if (hasDevices) { - m_actionButton->setStyleSheet( - "background-color: #34C759; color: white; border: none; " - "border-radius: 6px; font-size: 16px; font-weight: bold;"); - m_actionButton->setEnabled(true); - } else { - m_actionButton->setStyleSheet( - "background-color: #cccccc; color: #666; border: none; " - "border-radius: 6px; font-size: 16px; font-weight: bold;"); - m_actionButton->setEnabled(false); - } + connect(m_actionButton, &QPushButton::clicked, this, &AppInstallDialog::onInstallClicked); layout->addWidget(m_actionButton); @@ -82,6 +75,11 @@ AppInstallDialog::AppInstallDialog(const QString &appName, "border-radius: 6px; font-size: 16px;"); connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); layout->addWidget(cancelButton); + + connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, + &AppInstallDialog::updateDeviceList); + + updateDeviceList(); } void AppInstallDialog::updateDeviceList() @@ -91,6 +89,9 @@ void AppInstallDialog::updateDeviceList() if (devices.empty()) { m_deviceCombo->addItem("No devices connected"); m_deviceCombo->setEnabled(false); + m_actionButton->setDefault(false); + m_actionButton->setEnabled(false); + m_statusLabel->setText("No devices connected"); } else { m_deviceCombo->setEnabled(true); for (const auto &device : devices) { @@ -98,11 +99,58 @@ void AppInstallDialog::updateDeviceList() QString::fromStdString(device->deviceInfo.productType); QString deviceId = QString::fromStdString(device->udid); m_deviceCombo->addItem( - deviceName + " (" + deviceId.left(8) + "...)", deviceId); + deviceName + " / " + deviceId.left(8) + "...", deviceId); } + m_actionButton->setDefault(true); + m_actionButton->setEnabled(true); + m_statusLabel->setText("Ready to install"); } } +void AppInstallDialog::performInstallation(const QString &ipaPath, + const QString &deviceUdid) +{ + m_statusLabel->setText("Installing app..."); + + // Setup install watcher + m_installWatcher = new QFutureWatcher(this); + connect(m_installWatcher, &QFutureWatcher::finished, this, [this]() { + int result = m_installWatcher->result(); + m_installWatcher->deleteLater(); + m_installWatcher = nullptr; + + if (result == 0) { + m_statusLabel->setText("Installation completed successfully!"); + m_statusLabel->setStyleSheet( + "font-size: 14px; color: #34C759; padding: 5px;"); + QMessageBox::information(this, "Success", + "App installed successfully!"); + accept(); + } else { + m_statusLabel->setText("Installation failed"); + m_statusLabel->setStyleSheet( + "font-size: 14px; color: #FF3B30; padding: 5px;"); + QMessageBox::critical( + this, "Error", + QString("Installation failed with error code: %1").arg(result)); + } + }); + + // Run installation in background thread + QFuture future = QtConcurrent::run([ipaPath, deviceUdid]() -> int { + iDescriptorDevice *device = + AppContext::sharedInstance()->getDevice(deviceUdid.toStdString()); + if (!device) { + return -1; + } + + instproxy_error_t ret = install_IPA(device->device, device->afcClient, + ipaPath.toStdString().c_str()); + return static_cast(ret); + }); + + m_installWatcher->setFuture(future); +} void AppInstallDialog::onInstallClicked() { if (m_deviceCombo->count() == 0) { @@ -110,24 +158,71 @@ void AppInstallDialog::onInstallClicked() "Please connect a device first."); return; } + m_deviceCombo->setEnabled(false); - QString selectedDevice = m_deviceCombo->currentData().toString(); - // QStringList args = {"download", - // "-b", - // m_bundleId, - // "-o", - // "./", - // "--purchase", - // "--keychain-passphrase", - // "iDescriptor", - // "--format", - // "json"}; m_actionButton->setEnabled(false); + m_statusLabel->setText("Downloading app..."); + + QString selectedDevice = m_deviceCombo->currentData().toString(); int buttonIndex = m_layout->indexOf(m_actionButton); layout()->removeWidget(m_actionButton); m_actionButton->deleteLater(); - m_actionButton = nullptr; // Reset to avoid double deletion - // TODO: need to implement the actual installation logic - // startDownloadProcess(args, QDir::currentPath(), buttonIndex); + m_actionButton = nullptr; + + m_tempDir = QDir::tempPath(); + startDownloadProcess(m_bundleId, m_tempDir, buttonIndex, false); + connect(this, &AppDownloadBaseDialog::downloadFinished, this, + [this, selectedDevice](bool success) { + if (success) { + qDebug() << "Download finished, starting installation..."; + /* + FIXME: libipatool generates random id and appends that + to the downloaded IPA filename, so we need to search for + it. + */ + // Find the actual downloaded IPA file + QDir outDir = m_tempDir; + QStringList filters; + filters << m_bundleId + "*.ipa"; + QStringList matches = + outDir.entryList(filters, QDir::Files, QDir::Time); + if (matches.isEmpty()) { + m_statusLabel->setText( + "Download failed - IPA not found"); + m_statusLabel->setStyleSheet( + "font-size: 14px; color: #FF3B30; padding: 5px;"); + QMessageBox::critical( + this, "Error", + QString("Downloaded IPA not found in %1") + .arg(outDir.absolutePath())); + return; + } + + QString ipaFile = outDir.filePath(matches.first()); + performInstallation(ipaFile, selectedDevice); + + } else { + m_statusLabel->setText("Download failed"); + m_statusLabel->setStyleSheet( + "font-size: 14px; color: #FF3B30; padding: 5px;"); + } + }); +} + +void AppInstallDialog::reject() +{ + // Cancel installation if it's running + if (m_installWatcher && !m_installWatcher->isFinished()) { + m_installWatcher->cancel(); + m_installWatcher->deleteLater(); + m_installWatcher = nullptr; + if (m_statusLabel) { + m_statusLabel->setText("Installation cancelled"); + m_statusLabel->setStyleSheet( + "font-size: 14px; color: #FF3B30; padding: 5px;"); + } + } + + AppDownloadBaseDialog::reject(); } diff --git a/src/appinstalldialog.h b/src/appinstalldialog.h index da37579..1a522d3 100644 --- a/src/appinstalldialog.h +++ b/src/appinstalldialog.h @@ -4,6 +4,8 @@ #include "appdownloadbasedialog.h" #include #include +#include +#include class AppInstallDialog : public AppDownloadBaseDialog { @@ -11,15 +13,23 @@ class AppInstallDialog : public AppDownloadBaseDialog public: explicit AppInstallDialog(const QString &appName, const QString &description, + const QString &bundleId, QWidget *parent = nullptr); +protected: + void reject() override; + private slots: void onInstallClicked(); private: QComboBox *m_deviceCombo; QString m_bundleId; + QLabel *m_statusLabel; + QFutureWatcher *m_installWatcher; + QString m_tempDir; void updateDeviceList(); + void performInstallation(const QString &ipaPath, const QString &deviceUdid); }; #endif // APPINSTALLDIALOG_H diff --git a/src/appswidget.cpp b/src/appswidget.cpp index 7167e80..9516599 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -279,19 +279,12 @@ void AppsWidget::clearAppGrid() } } -// void AppsWidget::showStatusMessage(const QString &message) -// { -// showError(message); -// } - void AppsWidget::createAppCard(const QString &name, const QString &bundleId, const QString &description, const QString &iconPath, QGridLayout *gridLayout, int row, int col) { QWidget *cardWidget = new QWidget(); - // cardWidget->setFixedSize(200, 250); - cardWidget->setCursor(Qt::PointingHandCursor); QHBoxLayout *cardLayout = new QHBoxLayout(cardWidget); cardLayout->setContentsMargins(15, 15, 15, 15); @@ -355,14 +348,14 @@ void AppsWidget::createAppCard(const QString &name, const QString &bundleId, QPushButton *installLabel = new QPushButton("Install"); QPushButton *downloadIpaLabel = new QPushButton("Download IPA"); - installLabel->setStyleSheet( - "font-size: 12px; color: #007AFF; font-weight: bold;"); + installLabel->setStyleSheet("font-size: 12px; color: #007AFF; font-weight: " + "bold; background-color: transparent;"); installLabel->setCursor(Qt::PointingHandCursor); installLabel->setFixedHeight(30); - // installLabel->setAlignment(Qt::AlignCenter); - connect( - installLabel, &QPushButton::clicked, this, - [this, name, description]() { onAppCardClicked(name, description); }); + connect(installLabel, &QPushButton::clicked, this, + [this, name, bundleId, description]() { + onAppCardClicked(name, bundleId, description); + }); connect(downloadIpaLabel, &QPushButton::clicked, this, [this, name, bundleId]() { onDownloadIpaClicked(name, bundleId); }); @@ -416,6 +409,7 @@ void AppsWidget::onLoginClicked() } void AppsWidget::onAppCardClicked(const QString &appName, + const QString &bundleId, const QString &description) { if (!m_isLoggedIn) { @@ -424,7 +418,7 @@ void AppsWidget::onAppCardClicked(const QString &appName, return; } - AppInstallDialog dialog(appName, description, this); + AppInstallDialog dialog(appName, description, bundleId, this); dialog.exec(); } @@ -477,6 +471,7 @@ void AppsWidget::onSearchFinished(bool success, const QString &results) return; } + qDebug() << "Search results:" << doc; QJsonObject rootObj = doc.object(); if (!rootObj.value("success").toBool()) { QString errorMessage = diff --git a/src/appswidget.h b/src/appswidget.h index 6f5dd27..b7499b5 100644 --- a/src/appswidget.h +++ b/src/appswidget.h @@ -27,7 +27,8 @@ public: private slots: void onLoginClicked(); - void onAppCardClicked(const QString &appName, const QString &description); + void onAppCardClicked(const QString &appName, const QString &bundleId, + const QString &description); void onDownloadIpaClicked(const QString &name, const QString &bundleId); void onSearchTextChanged(); void performSearch(); diff --git a/src/core/helpers/safe_afc_read_directory.cpp b/src/core/helpers/safe_afc_read_directory.cpp deleted file mode 100644 index 2d11cc1..0000000 --- a/src/core/helpers/safe_afc_read_directory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include -#include - -afc_error_t safe_afc_read_directory(afc_client_t afcClient, idevice_t device, - const char *path, char ***dirs) -{ - try { - if (!afcClient || !device) { - qDebug() << "AFC client is null in safe_afc_read_directory"; - return AFC_E_INVALID_ARG; - } - - afc_error_t result = afc_read_directory(afcClient, path, dirs); - - return result; - } catch (const std::exception &e) { - qDebug() << "Exception in safe_afc_read_directory:" << e.what(); - return AFC_E_UNKNOWN_ERROR; - } -} \ No newline at end of file diff --git a/src/core/services/get-device-info.cpp b/src/core/services/get-device-info.cpp index b418935..d7193b9 100644 --- a/src/core/services/get-device-info.cpp +++ b/src/core/services/get-device-info.cpp @@ -63,8 +63,8 @@ static const char *domains[] = { "com.apple.mobile.iTunes", "com.apple.fmip", "com.apple.Accessibility", NULL}; -plist_t get_device_info(const char *udid, int use_network, int simple, - lockdownd_client_t client, idevice_t device) +plist_t get_device_info(const char *udid, lockdownd_client_t client, + idevice_t device) { lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; idevice_error_t ret = IDEVICE_E_UNKNOWN_ERROR; @@ -78,21 +78,6 @@ plist_t get_device_info(const char *udid, int use_network, int simple, plist_t disk_info = nullptr; uint64_t total_space = 0; uint64_t free_space = 0; - /* { - "AmountDataAvailable": 6663077888, - "AmountDataReserved": 209715200, - "AmountRestoreAvailable": 11524079616, - "CalculateDiskUsage": "OkilyDokily", - "NANDInfo": <01000000 01000000 01000000 00000080 ... 00 00000000 000000>, - "TotalDataAvailable": 6872793088, - "TotalDataCapacity": 11306721280, - "TotalDiskCapacity": 16000000000, - "TotalSystemAvailable": 0, - "TotalSystemCapacity": 4693204992 - }*/ - /* trying to set DiskInfo as key results in - xplist.c:365: node_to_xml: Assertion `(node->children->count % 2) == 0' - failed. so lets do merge it*/ if (lockdownd_get_value(client, "com.apple.disk_usage", nullptr, &disk_info) == LOCKDOWN_E_SUCCESS) { // merge dict @@ -103,14 +88,12 @@ plist_t get_device_info(const char *udid, int use_network, int simple, return node; } -void get_device_info_xml(const char *udid, int use_network, int simple, - pugi::xml_document &infoXml, lockdownd_client_t client, - idevice_t device) +void get_device_info_xml(const char *udid, lockdownd_client_t client, + idevice_t device, pugi::xml_document &infoXml) { - plist_t node = get_device_info(udid, use_network, simple, client, device); + plist_t node = get_device_info(udid, client, device); if (!node) return; - char *xml_string = nullptr; uint32_t xml_length = 0; plist_to_xml(node, &xml_string, &xml_length); diff --git a/src/core/helpers/get_file_tree.cpp b/src/core/services/get_file_tree.cpp similarity index 94% rename from src/core/helpers/get_file_tree.cpp rename to src/core/services/get_file_tree.cpp index e6fb51a..f57ed5a 100644 --- a/src/core/helpers/get_file_tree.cpp +++ b/src/core/services/get_file_tree.cpp @@ -5,7 +5,7 @@ #include #include -AFCFileTree get_file_tree(afc_client_t afcClient, idevice_t device, +AFCFileTree get_file_tree(afc_client_t afcClient, const std::string &path) { @@ -19,7 +19,7 @@ AFCFileTree get_file_tree(afc_client_t afcClient, idevice_t device, } char **dirs = NULL; - if (safe_afc_read_directory(afcClient, device, path.c_str(), &dirs) != + if (afc_read_directory(afcClient, path.c_str(), &dirs) != AFC_E_SUCCESS) { result.success = false; return result; diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 3100d5c..60f92ed 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -1,5 +1,6 @@ #include "../../devicedatabase.h" #include "../../iDescriptor.h" +#include "../../servicemanager.h" #include "libirecovery.h" #include #include @@ -120,7 +121,7 @@ void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d) // TODO: return tyype DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, afc_client_t &afcClient, - IDescriptorInitDeviceResult &result) + iDescriptorInitDeviceResult &result) { pugi::xml_node dict = doc.child("plist").child("dict"); auto safeGet = [&](const char *key) -> std::string { @@ -206,7 +207,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, const DeviceDatabaseInfo *info = DeviceDatabase::findByIdentifier(rawProductType); d.productType = - info ? info->displayName ? info->marketingName : "Unknown Device" + info ? info->displayName ? info->displayName : info->marketingName : "Unknown Device"; d.rawProductType = rawProductType; d.jailbroken = detect_jailbroken(afcClient); @@ -271,152 +272,134 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, } } -// TODO: IDescriptorInitDeviceResult -IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) +iDescriptorInitDeviceResult init_idescriptor_device(const char *udid) { - // TODO:on a broken usb cable this can hang for a long time - // causing the UI to freeze qDebug() << "Initializing iDescriptor device with UDID: " << QString::fromUtf8(udid); - IDescriptorInitDeviceResult result = {}; + iDescriptorInitDeviceResult result = {}; - lockdownd_client_t client; - // TODO: LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING - // LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING = -19, - lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; + // 1. Initialize all resource handles to nullptr + idevice_t device = nullptr; + lockdownd_client_t client = nullptr; lockdownd_service_descriptor_t lockdownService = nullptr; afc_client_t afcClient = nullptr; afc_client_t afc2Client = nullptr; - try { - idevice_error_t ret = idevice_new_with_options(&result.device, udid, - IDEVICE_LOOKUP_USBMUX); + pugi::xml_document infoXml; - if (ret != IDEVICE_E_SUCCESS) { - qDebug() << "Failed to connect to device: " << ret; - return result; - } - if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake( - result.device, &client, APP_LABEL))) { - result.error = ldret; - qDebug() << "Failed to create lockdown client: " << ldret; - idevice_free(result.device); - return result; - } + idevice_error_t ret = + idevice_new_with_options(&device, udid, IDEVICE_LOOKUP_USBMUX); - if (LOCKDOWN_E_SUCCESS != - (ldret = lockdownd_start_service(client, "com.apple.afc", - &lockdownService))) { - lockdownd_client_free(client); - idevice_free(result.device); - qDebug() << "Failed to start AFC service: " << ldret; - return result; - } - if (lockdownService) { - qDebug() << "AFC service started successfully."; - } else { - qDebug() << "AFC service descriptor is null."; - // lockdownd_client_free(result.client); - // idevice_free(result.device); - // return result; - } - - if (afc_client_new(result.device, lockdownService, &afcClient) != - AFC_E_SUCCESS) { - lockdownd_service_descriptor_free(lockdownService); - lockdownd_client_free(client); - idevice_free(result.device); - qDebug() << "Failed to create AFC client: " << ldret; - return result; - } - - try { - afc_error_t err = AFC_E_UNKNOWN_ERROR; - if ((err = afc2_client_new(result.device, &afc2Client)) != - AFC_E_SUCCESS) { - qDebug() << "AFC2 client not available." << "Error:" << err; - } else { - result.afc2Client = afc2Client; - qDebug() << "AFC2 client created successfully."; - } - } catch (const std::exception &e) { - /* Fine! This only works on Jailbroken and AFC2 tweak installed - * devices */ - } - - pugi::xml_document infoXml; - get_device_info_xml(udid, 0, 0, infoXml, client, result.device); - - if (infoXml.empty()) { - qDebug() << "Failed to retrieve device info XML for UDID: " - << QString::fromUtf8(udid); - // Clean up resources before returning - // afc_client_free(result.afcClient); - // lockdownd_service_descriptor_free(result.lockdownService); - // lockdownd_client_free(result.client); - idevice_free(result.device); - return result; - } - - std::string productType = - safeGetXML("ProductType", infoXml.child("plist").child("dict")); - - // if (result.device) idevice_free(result.device); - - fullDeviceInfo(infoXml, afcClient, result); - result.afcClient = afcClient; - result.success = true; - - if (lockdownService) - lockdownd_service_descriptor_free(lockdownService); - if (client) - lockdownd_client_free(client); - - return result; - - } catch (const std::exception &e) { - qDebug() << "Exception in init_idescriptor_device: " << e.what(); - // Clean up any allocated resources - // if (result.afcClient) afc_client_free(result.afcClient); - // if (result.lockdownService) - // lockdownd_service_descriptor_free(result.lockdownService); - if (client) - lockdownd_client_free(client); - if (result.device) - idevice_free(result.device); - return result; + if (ret != IDEVICE_E_SUCCESS) { + qDebug() << "Failed to connect to device: " << ret; + // result.error is not set here as idevice_error_t is different + goto cleanup; } + + lockdownd_error_t ldret; + if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake( + device, &client, APP_LABEL))) { + result.error = ldret; + qDebug() << "Failed to create lockdown client: " << ldret; + goto cleanup; + } + + if (LOCKDOWN_E_SUCCESS != + (ldret = lockdownd_start_service(client, "com.apple.afc", + &lockdownService))) { + result.error = ldret; + qDebug() << "Failed to start AFC service: " << ldret; + goto cleanup; + } + + if (afc_client_new(device, lockdownService, &afcClient) != AFC_E_SUCCESS) { + qDebug() << "Failed to create AFC client."; + + goto cleanup; + } + + // AFC2 is optional, so we don't goto cleanup on failure + afc_error_t afc2_err; + if ((afc2_err = afc2_client_new(device, &afc2Client)) != AFC_E_SUCCESS) { + qDebug() << "AFC2 client not available. Error:" << afc2_err; + afc2Client = nullptr; + } else { + qDebug() << "AFC2 client created successfully."; + } + + get_device_info_xml(udid, client, device, infoXml); + + if (infoXml.empty()) { + qDebug() << "Failed to retrieve device info XML for UDID: " + << QString::fromUtf8(udid); + goto cleanup; + } + + // If we got this far, the core initialization is successful + result.success = true; + result.device = device; + result.afcClient = afcClient; + result.afc2Client = afc2Client; + fullDeviceInfo(infoXml, afcClient, result); + +cleanup: + if (lockdownService) { + lockdownd_service_descriptor_free(lockdownService); + } + if (client) { + lockdownd_client_free(client); + } + + // free on error + if (!result.success) { + if (afc2Client) { + afc_client_free(afc2Client); + } + if (afcClient) { + afc_client_free(afcClient); + } + if (device) { + idevice_free(device); + } + } + + return result; } -IDescriptorInitDeviceResultRecovery +iDescriptorInitDeviceResultRecovery init_idescriptor_recovery_device(uint64_t ecid) { - IDescriptorInitDeviceResultRecovery result; + qDebug() << "Initializing iDescriptor recovery device with ECID: " << ecid; + iDescriptorInitDeviceResultRecovery result = {}; + irecv_client_t client = nullptr; - irecv_error_t ret = IRECV_E_UNKNOWN_ERROR; - ret = irecv_open_with_ecid_and_attempts(&client, ecid, - RECOVERY_CLIENT_CONNECTION_TRIES); - - if (ret != IRECV_E_SUCCESS) { - result.error = ret; - return result; - } - ret = irecv_get_mode(client, (int *)&result.mode); - - if (ret != IRECV_E_SUCCESS) { - result.error = ret; - irecv_close(client); - return result; - } - - const irecv_device_info *deviceInfo = irecv_get_device_info(client); - if (!deviceInfo) { - result.error = IRECV_E_UNKNOWN_ERROR; - irecv_close(client); - return result; - } - + const irecv_device_info *deviceInfo = nullptr; irecv_device_t device = nullptr; const DeviceDatabaseInfo *info = nullptr; + + irecv_error_t ret = irecv_open_with_ecid_and_attempts( + &client, ecid, RECOVERY_CLIENT_CONNECTION_TRIES); + + if (ret != IRECV_E_SUCCESS) { + qDebug() << "Failed to open recovery client with ECID:" << ecid + << "Error:" << ret; + result.error = ret; + goto cleanup; + } + + ret = irecv_get_mode(client, (int *)&result.mode); + if (ret != IRECV_E_SUCCESS) { + qDebug() << "Failed to get recovery mode. Error:" << ret; + result.error = ret; + goto cleanup; + } + + deviceInfo = irecv_get_device_info(client); + if (!deviceInfo) { + qDebug() << "Failed to get device info from recovery client"; + result.error = IRECV_E_UNKNOWN_ERROR; + goto cleanup; + } + if (irecv_devices_get_device_by_client(client, &device) == IRECV_E_SUCCESS && device && device->hardware_model) { @@ -431,9 +414,13 @@ init_idescriptor_recovery_device(uint64_t ecid) result.displayName = info ? (info->displayName ? info->displayName : info->marketingName) : "Unknown Device"; - result.deviceInfo = *deviceInfo; result.success = true; - irecv_close(client); + +cleanup: + if (client) { + irecv_close(client); + } + return result; } \ No newline at end of file diff --git a/src/core/services/install_ipa.cpp b/src/core/services/install_ipa.cpp new file mode 100644 index 0000000..de14dc2 --- /dev/null +++ b/src/core/services/install_ipa.cpp @@ -0,0 +1,526 @@ +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_UNISTD_H +#include +#endif + +#include +#include +#include +#include + +#include + +#include + +#ifdef WIN32 +#include +#define wait_ms(x) Sleep(x) +#else +#define wait_ms(x) \ + { \ + struct timespec ts; \ + ts.tv_sec = 0; \ + ts.tv_nsec = x * 1000000; \ + nanosleep(&ts, NULL); \ + } +#endif + +#define ITUNES_METADATA_PLIST_FILENAME "iTunesMetadata.plist" + +const char PKG_PATH[] = "PublicStaging"; + +struct install_status_data { + int command_completed; + int err_occurred; + char *last_status; +}; + +static void status_cb(plist_t command, plist_t status, void *user_data) +{ + struct install_status_data *isd = (struct install_status_data *)user_data; + if (command && status) { + char *command_name = NULL; + instproxy_command_get_name(command, &command_name); + + /* get status */ + char *status_name = NULL; + instproxy_status_get_name(status, &status_name); + + if (status_name) { + if (!strcmp(status_name, "Complete")) { + isd->command_completed = 1; + } + } + + /* get error if any */ + char *error_name = NULL; + char *error_description = NULL; + uint64_t error_code = 0; + instproxy_status_get_error(status, &error_name, &error_description, + &error_code); + + /* output/handling */ + if (!error_name) { + if (status_name) { + /* get progress if any */ + int percent = -1; + instproxy_status_get_percent_complete(status, &percent); + + if (isd->last_status && + (strcmp(isd->last_status, status_name))) { + printf("\n"); + } + + if (percent >= 0) { + printf("\r%s: %s (%d%%)", command_name, status_name, + percent); + } else { + printf("\r%s: %s", command_name, status_name); + } + if (isd->command_completed) { + printf("\n"); + } + } + } else { + /* report error to the user */ + if (error_description) + fprintf(stderr, + "ERROR: %s failed. Got error \"%s\" with code " + "0x%08" PRIx64 ": %s\n", + command_name, error_name, error_code, + error_description ? error_description : "N/A"); + else + fprintf(stderr, "ERROR: %s failed. Got error \"%s\".\n", + command_name, error_name); + isd->err_occurred = 1; + } + + /* clean up */ + free(error_name); + free(error_description); + + free(isd->last_status); + isd->last_status = status_name; + + free(command_name); + command_name = NULL; + } else { + fprintf(stderr, "ERROR: %s was called with invalid arguments!\n", + __func__); + } +} + +static int zip_get_contents(struct zip *zf, const char *filename, + int locate_flags, char **buffer, uint32_t *len) +{ + struct zip_stat zs; + struct zip_file *zfile; + int zindex = zip_name_locate(zf, filename, locate_flags); + + *buffer = NULL; + *len = 0; + + if (zindex < 0) { + return -1; + } + + zip_stat_init(&zs); + + if (zip_stat_index(zf, zindex, 0, &zs) != 0) { + fprintf(stderr, "ERROR: zip_stat_index '%s' failed!\n", filename); + return -2; + } + + if (zs.size > 10485760) { + fprintf(stderr, "ERROR: file '%s' is too large!\n", filename); + return -3; + } + + zfile = zip_fopen_index(zf, zindex, 0); + if (!zfile) { + fprintf(stderr, "ERROR: zip_fopen '%s' failed!\n", filename); + return -4; + } + + *buffer = (char *)malloc(zs.size); + if (zs.size > LLONG_MAX || + zip_fread(zfile, *buffer, zs.size) != (zip_int64_t)zs.size) { + fprintf(stderr, "ERROR: zip_fread %" PRIu64 " bytes from '%s'\n", + (uint64_t)zs.size, filename); + free(*buffer); + *buffer = NULL; + zip_fclose(zfile); + return -5; + } + *len = zs.size; + zip_fclose(zfile); + return 0; +} + +static int zip_get_app_directory(struct zip *zf, char **path) +{ + zip_int64_t i = 0; + zip_int64_t c = (zip_int64_t)zip_get_num_entries(zf, 0); + int len = 0; + const char *name = NULL; + + /* look through all filenames in the archive */ + do { + /* get filename at current index */ + name = zip_get_name(zf, i++, 0); + if (name != NULL) { + /* check if we have a "Payload/.../" name */ + len = strlen(name); + if (!strncmp(name, "Payload/", 8) && (len > 8)) { + /* skip hidden files */ + if (name[8] == '.') + continue; + + /* locate the second directory delimiter */ + const char *p = name + 8; + do { + if (*p == '/') { + break; + } + } while (p++ != NULL); + + /* try next entry if not found */ + if (p == NULL) + continue; + + len = p - name + 1; + + /* make sure app directory endwith .app */ + if (len < 12 || strncmp(p - 4, ".app", 4)) { + continue; + } + + if (path != NULL) { + free(*path); + *path = NULL; + } + + /* allocate and copy filename */ + *path = (char *)malloc(len + 1); + strncpy(*path, name, len); + + /* add terminating null character */ + char *t = *path + len; + *t = '\0'; + break; + } + } + } while (i < c); + + if (*path == NULL) { + return -1; + } + + return 0; +} + +static int afc_upload_file(afc_client_t afc, const char *filename, + const char *dstfn) +{ + FILE *f = NULL; + uint64_t af = 0; + char buf[1048576]; + + f = fopen(filename, "rb"); + if (!f) { + fprintf(stderr, "fopen: %s: %s\n", filename, strerror(errno)); + return -1; + } + + if ((afc_file_open(afc, dstfn, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || + !af) { + fclose(f); + fprintf(stderr, "afc_file_open on '%s' failed!\n", dstfn); + return -1; + } + + size_t amount = 0; + do { + amount = fread(buf, 1, sizeof(buf), f); + if (amount > 0) { + uint32_t written, total = 0; + while (total < amount) { + written = 0; + afc_error_t aerr = + afc_file_write(afc, af, buf, amount, &written); + if (aerr != AFC_E_SUCCESS) { + fprintf(stderr, "AFC Write error: %d\n", aerr); + break; + } + total += written; + } + if (total != amount) { + fprintf(stderr, "Error: wrote only %u of %u\n", total, + (uint32_t)amount); + afc_file_close(afc, af); + fclose(f); + return -1; + } + } + } while (amount > 0); + + afc_file_close(afc, af); + fclose(f); + + return 0; +} + +instproxy_error_t install_IPA(idevice_t device, afc_client_t afc, + const char *filePath) +{ + lockdownd_client_t client = NULL; + instproxy_client_t ipc = NULL; + lockdownd_service_descriptor_t service = NULL; + instproxy_error_t err = INSTPROXY_E_UNKNOWN_ERROR; + char *bundleidentifier = NULL; + struct install_status_data status_data = {0, 0, NULL}; + plist_t sinf = NULL; + plist_t meta = NULL; + char *pkgname = NULL; + struct stat fst; + char **strs = NULL; + plist_t client_opts = instproxy_client_options_new(); + char *zbuf = NULL; + uint32_t len = 0; + plist_t meta_dict = NULL; + int errp = 0; + struct zip *zf = zip_open(filePath, 0, &errp); + plist_t info = NULL; + char *filename = NULL; + char *app_directory_name = NULL; + char *bundleexecutable = NULL; + plist_t bname = NULL; + char *sinfname = NULL; + + if (!device || !filePath || !afc) { + fprintf(stderr, "ERROR: Invalid arguments passed to install_IPA.\n"); + return INSTPROXY_E_INVALID_ARG; + } + + lockdownd_error_t lerr = lockdownd_client_new_with_handshake( + device, &client, "ideviceinstaller"); + if (lerr != LOCKDOWN_E_SUCCESS) { + fprintf(stderr, "Could not connect to lockdownd: %s. Exiting.\n", + lockdownd_strerror(lerr)); + return INSTPROXY_E_OP_FAILED; + } + + lerr = lockdownd_start_service( + client, "com.apple.mobile.installation_proxy", &service); + if (lerr != LOCKDOWN_E_SUCCESS) { + fprintf(stderr, + "Could not start com.apple.mobile.installation_proxy: %s\n", + lockdownd_strerror(lerr)); + lockdownd_client_free(client); + return INSTPROXY_E_OP_FAILED; + } + + err = instproxy_client_new(device, service, &ipc); + if (service) { + lockdownd_service_descriptor_free(service); + service = NULL; + } + + if (err != INSTPROXY_E_SUCCESS) { + fprintf(stderr, "Could not connect to installation_proxy!\n"); + lockdownd_client_free(client); + return err; + } + + setbuf(stdout, NULL); + + if (stat(filePath, &fst) != 0) { + fprintf(stderr, "ERROR: stat: %s: %s\n", filePath, strerror(errno)); + err = INSTPROXY_E_INVALID_ARG; + goto leave_cleanup; + } + + if (afc_get_file_info(afc, PKG_PATH, &strs) != AFC_E_SUCCESS) { + if (afc_make_directory(afc, PKG_PATH) != AFC_E_SUCCESS) { + fprintf(stderr, + "WARNING: Could not create directory '%s' on device!\n", + PKG_PATH); + } + } + if (strs) { + int i = 0; + while (strs[i]) { + free(strs[i]); + i++; + } + free(strs); + } + + if (!zf) { + fprintf(stderr, "ERROR: zip_open: %s: %d\n", filePath, errp); + err = INSTPROXY_E_INVALID_ARG; + goto leave_cleanup; + } + + /* extract iTunesMetadata.plist from package */ + if (zip_get_contents(zf, ITUNES_METADATA_PLIST_FILENAME, 0, &zbuf, &len) == + 0) { + meta = plist_new_data(zbuf, len); + plist_from_memory(zbuf, len, &meta_dict, NULL); + } + if (!meta_dict) { + plist_free(meta); + meta = NULL; + fprintf(stderr, "WARNING: could not locate %s in archive!\n", + ITUNES_METADATA_PLIST_FILENAME); + } + free(zbuf); + + /* determine .app directory in archive */ + zbuf = NULL; + len = 0; + + if (zip_get_app_directory(zf, &app_directory_name)) { + fprintf(stderr, "ERROR: Unable to locate .app directory in archive. " + "Make sure it is inside a 'Payload' directory.\n"); + err = INSTPROXY_E_INVALID_ARG; + goto zip_cleanup; + } + + /* construct full filename to Info.plist */ + filename = (char *)malloc(strlen(app_directory_name) + 10 + 1); + strcpy(filename, app_directory_name); + free(app_directory_name); + app_directory_name = NULL; + strcat(filename, "Info.plist"); + + if (zip_get_contents(zf, filename, 0, &zbuf, &len) < 0) { + fprintf(stderr, "WARNING: could not locate %s in archive!\n", filename); + free(filename); + err = INSTPROXY_E_INVALID_ARG; + goto zip_cleanup; + } + free(filename); + plist_from_memory(zbuf, len, &info, NULL); + free(zbuf); + + if (!info) { + fprintf(stderr, "Could not parse Info.plist!\n"); + err = INSTPROXY_E_INVALID_ARG; + goto zip_cleanup; + } + + bname = plist_dict_get_item(info, "CFBundleExecutable"); + if (bname) { + plist_get_string_val(bname, &bundleexecutable); + } + + bname = plist_dict_get_item(info, "CFBundleIdentifier"); + if (bname) { + plist_get_string_val(bname, &bundleidentifier); + } + plist_free(info); + info = NULL; + + if (!bundleexecutable) { + fprintf(stderr, "Could not determine value for CFBundleExecutable!\n"); + err = INSTPROXY_E_INVALID_ARG; + goto zip_cleanup; + } + + if (asprintf(&sinfname, "Payload/%s.app/SC_Info/%s.sinf", bundleexecutable, + bundleexecutable) < 0) { + fprintf(stderr, "Out of memory!?\n"); + err = INSTPROXY_E_UNKNOWN_ERROR; + goto zip_cleanup; + } + free(bundleexecutable); + + /* extract .sinf from package */ + zbuf = NULL; + len = 0; + if (zip_get_contents(zf, sinfname, 0, &zbuf, &len) == 0) { + sinf = plist_new_data(zbuf, len); + } else { + fprintf(stderr, "WARNING: could not locate %s in archive!\n", sinfname); + } + free(sinfname); + free(zbuf); + + /* copy archive to device */ + pkgname = NULL; + if (asprintf(&pkgname, "%s/%s", PKG_PATH, bundleidentifier) < 0) { + fprintf(stderr, "Out of memory!?\n"); + err = INSTPROXY_E_UNKNOWN_ERROR; + goto zip_cleanup; + } + + printf("Copying '%s' to device... ", filePath); + + if (afc_upload_file(afc, filePath, pkgname) < 0) { + printf("FAILED\n"); + free(pkgname); + err = INSTPROXY_E_OP_FAILED; + goto zip_cleanup; + } + + printf("DONE.\n"); + + if (bundleidentifier) { + instproxy_client_options_add(client_opts, "CFBundleIdentifier", + bundleidentifier, NULL); + } + if (sinf) { + instproxy_client_options_add(client_opts, "ApplicationSINF", sinf, + NULL); + } + if (meta) { + instproxy_client_options_add(client_opts, "iTunesMetadata", meta, NULL); + } + +zip_cleanup: + if (zf) { + zip_unchange_all(zf); + zip_close(zf); + } + if (err != INSTPROXY_E_SUCCESS) { + goto leave_cleanup; + } + + /* perform installation */ + printf("Installing '%s'\n", bundleidentifier); + instproxy_install(ipc, pkgname, client_opts, status_cb, &status_data); + + instproxy_client_options_free(client_opts); + free(pkgname); + + while (!status_data.command_completed && !status_data.err_occurred) { + wait_ms(50); + } + + if (status_data.err_occurred) { + err = INSTPROXY_E_OP_FAILED; + } else { + err = INSTPROXY_E_SUCCESS; + } + +leave_cleanup: + instproxy_client_free(ipc); + lockdownd_client_free(client); + free(bundleidentifier); + free(status_data.last_status); + + return err; +} \ No newline at end of file diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index f0b7307..7d3dec0 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -31,6 +31,7 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(0, 0, 10, 0); mainLayout->setSpacing(1); + mainLayout->addStretch(); // Left side container for image and actions QWidget *leftContainer = new QWidget(); @@ -78,15 +79,17 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) actionsLayout->addWidget(restartBtn); actionsLayout->addWidget(recoveryBtn); + leftLayout->addStretch(); leftLayout->addWidget(m_deviceImageLabel); leftLayout->addWidget(actionsWidget, 0, Qt::AlignCenter); - leftLayout->addStretch(); // stretch to push everything to the top + leftLayout->addStretch(); - mainLayout->addWidget(leftContainer); // Stretch factor 1 + mainLayout->addWidget(leftContainer); // Right side: Info Table QWidget *infoContainer = new QWidget(); - infoContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + // 2. Change the horizontal size policy from Expanding to Preferred + infoContainer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); QVBoxLayout *infoLayout = new QVBoxLayout(infoContainer); @@ -111,6 +114,7 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) // background-color: rgba(0, 255, 30, 0.5); diskCapacityLabel->setStyleSheet(QString("background-color: %1;" "padding: 2px 4px;" + "color : white;" "border-radius: 13px;") .arg(COLOR_ACCENT_BLUE.name())); @@ -285,20 +289,25 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) // Footer QLabel *footerLabel = new QLabel("UDID: " + QString::fromStdString(device->udid)); + footerLabel->setToolTip("Unique Device Identifier"); footerLabel->setStyleSheet( - "font-size: 10px; color: #666; padding-top: 5px; " - "border-top: 1px solid #eee;"); + "font-size: 10px; color: #666; margin-top: 5px; "); footerLabel->setWordWrap(true); infoLayout->addWidget(footerLabel); - // Create a vertical layout for the right side to stack info and disk usage QVBoxLayout *rightSideLayout = new QVBoxLayout(); rightSideLayout->setSpacing(10); + rightSideLayout->addStretch(); + rightSideLayout->addWidget(infoContainer); rightSideLayout->addWidget(new DiskUsageWidget(device, this)); + + rightSideLayout->addStretch(); // TODO: layout shift cause ? // rightSideLayout->setAlignment(Qt::AlignCenter); - mainLayout->addLayout(rightSideLayout, 2); // Stretch factor 2 + + mainLayout->addLayout(rightSideLayout); + m_updateTimer = new QTimer(this); connect(m_updateTimer, &QTimer::timeout, this, &DeviceInfoWidget::updateBatteryInfo); diff --git a/src/gallerywidget.cpp b/src/gallerywidget.cpp index ff52e16..b43634a 100644 --- a/src/gallerywidget.cpp +++ b/src/gallerywidget.cpp @@ -19,6 +19,7 @@ #include #include #include +#include "servicemanager.h" void GalleryWidget::load() { @@ -399,7 +400,7 @@ void GalleryWidget::loadAlbumList() // Get DCIM directory contents qDebug() << "Loading album list from /DCIM"; AFCFileTree dcimTree = - get_file_tree(m_device->afcClient, m_device->device, "/DCIM"); + ServiceManager::safeGetFileTree(m_device, "/DCIM"); if (!dcimTree.success) { qDebug() << "Failed to read DCIM directory"; diff --git a/src/iDescriptor.h b/src/iDescriptor.h index 96c779f..652bff3 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -2,11 +2,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -143,10 +145,11 @@ struct iDescriptorDevice { afc_client_t afcClient; afc_client_t afc2Client; bool is_iPhone; + std::recursive_mutex *mutex; }; -struct IDescriptorInitDeviceResult { - bool success; +struct iDescriptorInitDeviceResult { + bool success = false; lockdownd_error_t error; idevice_t device; DeviceInfo deviceInfo; @@ -163,11 +166,11 @@ struct iDescriptorRecoveryDevice { }; struct TakeScreenshotResult { - bool success; + bool success = false; QImage img; }; -struct IDescriptorInitDeviceResultRecovery { +struct iDescriptorInitDeviceResultRecovery { irecv_client_t client = nullptr; irecv_device_info deviceInfo; irecv_error_t error; @@ -266,18 +269,17 @@ struct AFCFileTree { std::string currentPath; }; -AFCFileTree get_file_tree(afc_client_t afcClient, idevice_t device, +AFCFileTree get_file_tree(afc_client_t afcClient, const std::string &path = "/"); bool detect_jailbroken(afc_client_t afc); -void get_device_info_xml(const char *udid, int use_network, int simple, - pugi::xml_document &infoXml, lockdownd_client_t client, - idevice_t device); +void get_device_info_xml(const char *udid, lockdownd_client_t client, + idevice_t device, pugi::xml_document &infoXml); -IDescriptorInitDeviceResult init_idescriptor_device(const char *udid); +iDescriptorInitDeviceResult init_idescriptor_device(const char *udid); -IDescriptorInitDeviceResultRecovery +iDescriptorInitDeviceResultRecovery init_idescriptor_recovery_device(uint64_t ecid); bool set_location(idevice_t device, char *lat, char *lon); @@ -398,4 +400,7 @@ QPixmap load_heic(const QByteArray &data); QByteArray read_afc_file_to_byte_array(afc_client_t afcClient, const char *path); -bool isDarkMode(); \ No newline at end of file +bool isDarkMode(); + +instproxy_error_t install_IPA(idevice_t device, afc_client_t afc, + const char *filePath); \ No newline at end of file diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp index caf27df..8b5041f 100644 --- a/src/installedappswidget.cpp +++ b/src/installedappswidget.cpp @@ -605,6 +605,10 @@ void InstalledAppsWidget::filterApps(const QString &searchText) } } +/* + FIXME: maybe we better have this in servicemanager, + for now it's ok as it's only used here +*/ void InstalledAppsWidget::loadAppContainer(const QString &bundleId) { if (!m_device || !m_device->device) { diff --git a/src/mediastreamer.cpp b/src/mediastreamer.cpp index 8bcddee..827f414 100644 --- a/src/mediastreamer.cpp +++ b/src/mediastreamer.cpp @@ -2,6 +2,7 @@ #include #include "iDescriptor.h" +#include "servicemanager.h" #include #include #include @@ -44,6 +45,7 @@ QUrl MediaStreamer::getUrl() const if (!isListening()) { return QUrl(); } + // todo pass folder/filename return QUrl(QString("http://127.0.0.1:%1/%2") .arg(serverPort()) .arg(QFileInfo(m_filePath).fileName())); @@ -257,11 +259,10 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte, context->afcHandle = 0; qDebug() << "m_filepath" << m_filePath; - // Open file on device + // Open file on device using ServiceManager const QByteArray pathBytes = m_filePath.toUtf8(); - afc_error_t openResult = - afc_file_open(m_afcClient, pathBytes.constData(), AFC_FOPEN_RDONLY, - &context->afcHandle); + afc_error_t openResult = ServiceManager::safeAfcFileOpen( + m_device, pathBytes.constData(), AFC_FOPEN_RDONLY, &context->afcHandle); if (openResult != AFC_E_SUCCESS || context->afcHandle == 0) { qWarning() << "Failed to open file on device:" << m_filePath; @@ -272,11 +273,11 @@ void MediaStreamer::streamFileRange(QTcpSocket *socket, qint64 startByte, // Seek to start position if needed if (startByte > 0) { - afc_error_t seekResult = - afc_file_seek(m_afcClient, context->afcHandle, startByte, SEEK_SET); + afc_error_t seekResult = ServiceManager::safeAfcFileSeek( + m_device, context->afcHandle, startByte, SEEK_SET); if (seekResult != AFC_E_SUCCESS) { qWarning() << "Failed to seek in file:" << m_filePath; - afc_file_close(m_afcClient, context->afcHandle); + ServiceManager::safeAfcFileClose(m_device, context->afcHandle); delete context; socket->disconnectFromHost(); return; @@ -329,11 +330,11 @@ qint64 MediaStreamer::getFileSize() return m_cachedFileSize; } - // Get file info from device + // Get file info from device using ServiceManager char **info = nullptr; const QByteArray pathBytes = m_filePath.toUtf8(); - afc_error_t result = - afc_get_file_info(m_afcClient, pathBytes.constData(), &info); + afc_error_t result = ServiceManager::safeAfcGetFileInfo( + m_device, pathBytes.constData(), &info); if (result != AFC_E_SUCCESS || !info) { qWarning() << "Failed to get file info for:" << m_filePath; @@ -409,8 +410,8 @@ void MediaStreamer::streamNextChunk(StreamingContext *context) auto buffer = std::make_unique(bytesToRead); uint32_t bytesRead = 0; - afc_error_t readResult = afc_file_read( - m_afcClient, context->afcHandle, buffer.get(), bytesToRead, &bytesRead); + afc_error_t readResult = ServiceManager::safeAfcFileRead( + m_device, context->afcHandle, buffer.get(), bytesToRead, &bytesRead); if (readResult != AFC_E_SUCCESS || bytesRead == 0) { qWarning() << "AFC read error or EOF during streaming"; @@ -467,7 +468,7 @@ void MediaStreamer::cleanupStreamingContext(StreamingContext *context) } if (context->afcHandle != 0) { - afc_file_close(context->device->afcClient, context->afcHandle); + ServiceManager::safeAfcFileClose(context->device, context->afcHandle); context->afcHandle = 0; } diff --git a/src/photoexportmanager.cpp b/src/photoexportmanager.cpp index 425acb8..dadcbac 100644 --- a/src/photoexportmanager.cpp +++ b/src/photoexportmanager.cpp @@ -1,4 +1,5 @@ #include "photoexportmanager.h" +#include "servicemanager.h" #include #include #include @@ -104,7 +105,7 @@ void PhotoExportManager::performExport() outputPath = generateUniqueOutputPath(outputPath); ExportResult result = - exportSingleFile(m_device->afcClient, devicePath, outputPath); + exportSingleFile(m_device, devicePath, outputPath); if (result.success) { successful++; @@ -126,24 +127,26 @@ void PhotoExportManager::performExport() emit exportFinished(successful, failed); } -PhotoExportManager::ExportResult PhotoExportManager::exportSingleFile( - afc_client_t afc, const QString &devicePath, const QString &outputPath) +PhotoExportManager::ExportResult +PhotoExportManager::exportSingleFile(iDescriptorDevice *device, + const QString &devicePath, + const QString &outputPath) { ExportResult result; result.filePath = devicePath; result.outputPath = outputPath; result.success = false; - // Open file on device + // Use ServiceManager for thread-safe AFC operations uint64_t handle = 0; - afc_error_t afc_err = afc_file_open(afc, devicePath.toUtf8().constData(), - AFC_FOPEN_RDONLY, &handle); + afc_error_t openResult = ServiceManager::safeAfcFileOpen( + device, devicePath.toUtf8().constData(), AFC_FOPEN_RDONLY, &handle); - if (afc_err != AFC_E_SUCCESS) { + if (openResult != AFC_E_SUCCESS) { result.errorMessage = QString("Failed to open file on device: %1 (AFC error: %2)") .arg(devicePath) - .arg(static_cast(afc_err)); + .arg(static_cast(openResult)); return result; } @@ -153,30 +156,35 @@ PhotoExportManager::ExportResult PhotoExportManager::exportSingleFile( result.errorMessage = QString("Failed to create local file: %1 (%2)") .arg(outputPath) .arg(outputFile.errorString()); - afc_file_close(afc, handle); + ServiceManager::safeAfcFileClose(device, handle); return result; } - // Copy data from device to local file + // Copy data from device to local file using ServiceManager char buffer[4096]; uint32_t bytesRead = 0; qint64 totalBytes = 0; - while (afc_file_read(afc, handle, buffer, sizeof(buffer), &bytesRead) == - AFC_E_SUCCESS && - bytesRead > 0) { + while (true) { // Check for cancellation during file copy { QMutexLocker locker(&m_mutex); if (m_cancelRequested) { outputFile.close(); outputFile.remove(); // Clean up partial file - afc_file_close(afc, handle); + ServiceManager::safeAfcFileClose(device, handle); result.errorMessage = "Export cancelled"; return result; } } + afc_error_t readResult = ServiceManager::safeAfcFileRead( + device, handle, buffer, sizeof(buffer), &bytesRead); + + if (readResult != AFC_E_SUCCESS || bytesRead == 0) { + break; // End of file or error + } + qint64 bytesWritten = outputFile.write(buffer, bytesRead); if (bytesWritten != bytesRead) { result.errorMessage = @@ -185,7 +193,7 @@ PhotoExportManager::ExportResult PhotoExportManager::exportSingleFile( .arg(bytesRead); outputFile.close(); outputFile.remove(); // Clean up partial file - afc_file_close(afc, handle); + ServiceManager::safeAfcFileClose(device, handle); return result; } @@ -194,7 +202,7 @@ PhotoExportManager::ExportResult PhotoExportManager::exportSingleFile( // Clean up outputFile.close(); - afc_file_close(afc, handle); + ServiceManager::safeAfcFileClose(device, handle); if (totalBytes == 0) { result.errorMessage = "No data read from device file"; @@ -244,4 +252,4 @@ PhotoExportManager::generateUniqueOutputPath(const QString &basePath) const counter < 10000); // Prevent infinite loop return uniquePath; -} \ No newline at end of file +} diff --git a/src/photoexportmanager.h b/src/photoexportmanager.h index a088087..a98ff2f 100644 --- a/src/photoexportmanager.h +++ b/src/photoexportmanager.h @@ -46,7 +46,8 @@ private slots: private: // Export single file using AFC - ExportResult exportSingleFile(afc_client_t afc, const QString &devicePath, + ExportResult exportSingleFile(iDescriptorDevice *device, + const QString &devicePath, const QString &outputPath); // Extract filename from device path diff --git a/src/photomodel.cpp b/src/photomodel.cpp index b0fbf46..546115c 100644 --- a/src/photomodel.cpp +++ b/src/photomodel.cpp @@ -1,6 +1,7 @@ #include "photomodel.h" #include "iDescriptor.h" #include "mediastreamermanager.h" +#include "servicemanager.h" #include #include #include @@ -330,9 +331,9 @@ QPixmap PhotoModel::loadThumbnailFromDevice(iDescriptorDevice *device, } } - // Load from device using your AFC function - QByteArray imageData = read_afc_file_to_byte_array( - device->afcClient, filePath.toUtf8().constData()); + // Load from device using ServiceManager + QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray( + device, filePath.toUtf8().constData()); if (imageData.isEmpty()) { qDebug() << "Could not read from device:" << filePath; @@ -377,9 +378,9 @@ QPixmap PhotoModel::loadImage(iDescriptorDevice *device, } } - // Load from device using your AFC function - QByteArray imageData = read_afc_file_to_byte_array( - device->afcClient, filePath.toUtf8().constData()); + // Load from device using ServiceManager + QByteArray imageData = ServiceManager::safeReadAfcFileToByteArray( + device, filePath.toUtf8().constData()); if (imageData.isEmpty()) { qDebug() << "Could not read from device:" << filePath; @@ -422,9 +423,9 @@ void PhotoModel::populatePhotoPaths() m_allPhotos.clear(); m_photos.clear(); - // Your existing logic to populate photo paths - char **files = nullptr; - qDebug() << "Populating photos from album path:" << m_albumPath; + // // Your existing logic to populate photo paths + // char **files = nullptr; + // qDebug() << "Populating photos from album path:" << m_albumPath; // First verify the album path exists QByteArray albumPathBytes = m_albumPath.toUtf8(); @@ -449,8 +450,10 @@ void PhotoModel::populatePhotoPaths() qDebug() << "Photo directory:" << m_albumPath; qDebug() << "Photo directory C string:" << photoDir; - afc_error_t readResult = safe_afc_read_directory( - m_device->afcClient, m_device->device, photoDir, &files); + // Use ServiceManager for thread-safe AFC operations + char **files = nullptr; + afc_error_t readResult = + ServiceManager::safeAfcReadDirectory(m_device, photoDir, &files); if (readResult != AFC_E_SUCCESS) { qDebug() << "Failed to read photo directory:" << photoDir << "Error:" << readResult; diff --git a/src/servicemanager.cpp b/src/servicemanager.cpp new file mode 100644 index 0000000..da9316f --- /dev/null +++ b/src/servicemanager.cpp @@ -0,0 +1,85 @@ +#include "servicemanager.h" + +afc_error_t ServiceManager::safeAfcReadDirectory(iDescriptorDevice *device, + const char *path, char ***dirs) +{ + return executeAfcOperation(device, [device, path, dirs]() { + return afc_read_directory(device->afcClient, path, dirs); + }); +} + +afc_error_t ServiceManager::safeAfcGetFileInfo(iDescriptorDevice *device, + const char *path, char ***info) +{ + return executeAfcOperation(device, [device, path, info]() { + return afc_get_file_info(device->afcClient, path, info); + }); +} + +afc_error_t ServiceManager::safeAfcFileOpen(iDescriptorDevice *device, + const char *path, + afc_file_mode_t mode, + uint64_t *handle) +{ + return executeAfcOperation(device, [device, path, mode, handle]() { + return afc_file_open(device->afcClient, path, mode, handle); + }); +} + +afc_error_t ServiceManager::safeAfcFileRead(iDescriptorDevice *device, + uint64_t handle, char *data, + uint32_t length, + uint32_t *bytes_read) +{ + return executeAfcOperation( + device, [device, handle, data, length, bytes_read]() { + return afc_file_read(device->afcClient, handle, data, length, + bytes_read); + }); +} + +afc_error_t ServiceManager::safeAfcFileWrite(iDescriptorDevice *device, + uint64_t handle, const char *data, + uint32_t length, + uint32_t *bytes_written) +{ + return executeAfcOperation( + device, [device, handle, data, length, bytes_written]() { + return afc_file_write(device->afcClient, handle, data, length, + bytes_written); + }); +} + +afc_error_t ServiceManager::safeAfcFileClose(iDescriptorDevice *device, + uint64_t handle) +{ + return executeAfcOperation(device, [device, handle]() { + return afc_file_close(device->afcClient, handle); + }); +} + +afc_error_t ServiceManager::safeAfcFileSeek(iDescriptorDevice *device, + uint64_t handle, int64_t offset, + int whence) +{ + return executeAfcOperation(device, [device, handle, offset, whence]() { + return afc_file_seek(device->afcClient, handle, offset, whence); + }); +} + +QByteArray ServiceManager::safeReadAfcFileToByteArray(iDescriptorDevice *device, + const char *path) +{ + return executeOperation(device, [device, path]() -> QByteArray { + return read_afc_file_to_byte_array(device->afcClient, path); + }); +} + +AFCFileTree ServiceManager::safeGetFileTree(iDescriptorDevice *device, + const std::string &path) +{ + return executeOperation( + device, [device, path]() -> AFCFileTree { + return get_file_tree(device->afcClient, path.c_str()); + }); +} \ No newline at end of file diff --git a/src/servicemanager.h b/src/servicemanager.h new file mode 100644 index 0000000..f811a14 --- /dev/null +++ b/src/servicemanager.h @@ -0,0 +1,136 @@ +#ifndef SERVICEMANAGER_H +#define SERVICEMANAGER_H + +#include "iDescriptor.h" +#include +#include +#include +#include + +/** + * @brief Centralized manager for device service operations with thread safety + * + * This class provides thread-safe wrappers for all device operations to prevent + * crashes when devices are unplugged during active operations. It uses a + * per-device recursive mutex to ensure that device cleanup waits for all + * operations to complete. + */ +class ServiceManager +{ +public: + /** + * @brief Execute an AFC operation safely with device locking + * @param device The device to operate on + * @param operation Function that performs the AFC operation + * @return Result of the operation + */ + template + static T executeOperation(iDescriptorDevice *device, + std::function operation) + { + if (!device || !device->mutex) { + return T{}; // Return default-constructed value for the type + } + + std::lock_guard lock(*device->mutex); + + // Double-check device is still valid after acquiring lock + if (!device->afcClient) { + return T{}; + } + + return operation(); + } + + template + static T executeOperation(iDescriptorDevice *device, + std::function operation, T failureValue) + { + if (!device || !device->mutex) { + return failureValue; + } + + std::lock_guard lock(*device->mutex); + + // Double-check device is still valid after acquiring lock + if (!device->afcClient) { + return failureValue; + } + + return operation(); + } + + static afc_error_t + executeAfcOperation(iDescriptorDevice *device, + std::function operation) + { + try { + if (!device || !device->mutex) { + return AFC_E_UNKNOWN_ERROR; + } + + std::lock_guard lock(*device->mutex); + + // Double-check device is still valid after acquiring lock + if (!device->afcClient) { + return AFC_E_UNKNOWN_ERROR; + } + + return operation(); + } catch (const std::exception &e) { + qDebug() << "Exception in executeAfcOperation:" << e.what(); + return AFC_E_UNKNOWN_ERROR; + } + } + + /** + * @brief Execute an AFC operation safely (void return version) + * @param device The device to operate on + * @param operation Function that performs the AFC operation + */ + static void executeOperation(iDescriptorDevice *device, + std::function operation) + { + if (!device || !device->mutex) { + return; + } + + std::lock_guard lock(*device->mutex); + + // Double-check device is still valid after acquiring lock + if (!device->afcClient) { + return; + } + + operation(); + } + + // Specific AFC operation wrappers + static afc_error_t safeAfcReadDirectory(iDescriptorDevice *device, + const char *path, char ***dirs); + static afc_error_t safeAfcGetFileInfo(iDescriptorDevice *device, + const char *path, char ***info); + static afc_error_t safeAfcFileOpen(iDescriptorDevice *device, + const char *path, afc_file_mode_t mode, + uint64_t *handle); + static afc_error_t safeAfcFileRead(iDescriptorDevice *device, + uint64_t handle, char *data, + uint32_t length, uint32_t *bytes_read); + static afc_error_t safeAfcFileWrite(iDescriptorDevice *device, + uint64_t handle, const char *data, + uint32_t length, + uint32_t *bytes_written); + static afc_error_t safeAfcFileClose(iDescriptorDevice *device, + uint64_t handle); + static afc_error_t safeAfcFileSeek(iDescriptorDevice *device, + uint64_t handle, int64_t offset, + int whence); + + // Utility functions + static QByteArray safeReadAfcFileToByteArray(iDescriptorDevice *device, + const char *path); + static AFCFileTree safeGetFileTree(iDescriptorDevice *device, + const std::string &path = "/"); +}; + +#endif // SERVICEMANAGER_H diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index f7e6256..2d1b60d 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -72,11 +72,8 @@ ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} updateDeviceList(); updateToolboxStates(); - // Connect to AppContext signals - connect(AppContext::sharedInstance(), &AppContext::deviceAdded, this, - &ToolboxWidget::onDeviceAdded); - connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, - &ToolboxWidget::onDeviceRemoved); + connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, + &ToolboxWidget::updateUI); } void ToolboxWidget::setupUI() @@ -304,13 +301,7 @@ void ToolboxWidget::updateToolboxStates() } } -void ToolboxWidget::onDeviceAdded() -{ - updateDeviceList(); - updateToolboxStates(); -} - -void ToolboxWidget::onDeviceRemoved() +void ToolboxWidget::updateUI() { updateDeviceList(); updateToolboxStates(); @@ -331,7 +322,7 @@ void ToolboxWidget::onDeviceSelectionChanged() return; } } - m_uuid.clear(); // No valid device selected + m_uuid.clear(); } void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) diff --git a/src/toolboxwidget.h b/src/toolboxwidget.h index 815dbdc..72618f4 100644 --- a/src/toolboxwidget.h +++ b/src/toolboxwidget.h @@ -31,6 +31,7 @@ private: void setupUI(); void updateDeviceList(); void updateToolboxStates(); + void updateUI(); ClickableWidget *createToolbox(iDescriptorTool tool, const QString &description, bool requiresDevice);