gui-qt: don't crash when deleting an archive locked by another process (#3956)

* gui-qt: don't crash on install file delete failures

when you install a .zip, .vpk, .pkg, or firmware .pup and tick the matching "delete after install" option, vita3k tries to remove the installer file from disk. the archive and pkg dialogs use the throwing fs::remove overload, which raises a filesystem_error when the file is locked by another process. since the call runs inside a qt slot, the exception escapes into the qt event loop and the whole emulator goes down with it.

the firmware dialog uses QFile::remove instead, which returns a bool that the code was ignoring, so a locked firmware file silently stuck around with no feedback to the user.

leaving the installer open in something like 7-zip while installing is enough to trigger either case.

this swaps fs::remove for the non-throwing error_code overload in the archive and pkg dialogs, and checks the bool from QFile::remove in the firmware dialog. anything that could not be removed gets shown in a warning so the user knows what stuck around, and the install itself still finishes normally.

* gui-qt: route firmware drops through the install dialog

dropping a .pup onto the main window used to run install_pup inline with a bare QProgressDialog and its own success/failure handling, bypassing FirmwareInstallDialog entirely. that path duplicated the progress UI and never offered the "delete firmware file after install?" option that the menu-triggered flow exposes.

route the drop through FirmwareInstallDialog instead, matching the existing .pkg and .vpk drop routing right next to it. this adds a constructor that takes a pre-supplied firmware path so the dialog skips its file picker when invoked from a drop.

dropped firmware installs now share the threaded progress UI, the warning when the file is locked, and the optional cleanup checkbox with the menu trigger.
This commit is contained in:
troglodyte
2026-05-23 20:06:55 +03:00
committed by GitHub
parent 85cb48046f
commit f3a0d0abc4
5 changed files with 57 additions and 34 deletions
@@ -52,6 +52,7 @@ class FirmwareInstallDialog : public QDialog {
Q_OBJECT
public:
explicit FirmwareInstallDialog(EmuEnvState &emuenv, QWidget *parent = nullptr);
FirmwareInstallDialog(EmuEnvState &emuenv, const QString &path, QWidget *parent = nullptr);
Q_SIGNALS:
void install_complete();
+13 -2
View File
@@ -431,9 +431,20 @@ void ArchiveInstallDialog::run_install(const std::vector<fs::path> &archives) {
[this, del_check, results]() {
if (del_check->isChecked()) {
std::set<fs::path> deleted;
QStringList failed;
for (const auto &r : results) {
if (!r.archive_full_path.empty() && deleted.insert(r.archive_full_path).second)
fs::remove(r.archive_full_path);
if (r.archive_full_path.empty() || !deleted.insert(r.archive_full_path).second)
continue;
// fs::remove throws and crashes the app when the archive is locked so use the non throwing overload
boost::system::error_code error;
fs::remove(r.archive_full_path, error);
if (error)
failed.push_back(r.archive_name);
}
if (!failed.isEmpty()) {
QMessageBox::warning(this, tr("Could Not Delete Archives"),
tr("The following archive files could not be deleted. They may still be open in another program:\n\n%1").arg(failed.join(QStringLiteral("\n"))));
}
}
Q_EMIT install_complete(results);
+19 -2
View File
@@ -60,6 +60,20 @@ FirmwareInstallDialog::FirmwareInstallDialog(EmuEnvState &emuenv, QWidget *paren
run_install(path);
}
FirmwareInstallDialog::FirmwareInstallDialog(EmuEnvState &emuenv, const QString &path, QWidget *parent)
: QDialog(parent)
, m_emuenv(emuenv) {
setWindowTitle(tr("Install Firmware"));
setModal(true);
if (path.isEmpty()) {
QMetaObject::invokeMethod(this, &QDialog::reject, Qt::QueuedConnection);
return;
}
run_install(path);
}
void FirmwareInstallDialog::run_install(const QString &path) {
auto *main_layout = new QVBoxLayout(this);
@@ -122,8 +136,11 @@ void FirmwareInstallDialog::run_install(const QString &path) {
layout()->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, [this, del_check, path]() {
if (del_check->isChecked())
QFile::remove(path);
// QFile::remove returns false silently when the firmware file is locked, so surface that to the user
if (del_check->isChecked() && !QFile::remove(path)) {
QMessageBox::warning(this, tr("Could Not Delete Firmware"),
tr("The firmware file could not be deleted. It may still be open in another program:\n\n%1").arg(path));
}
Q_EMIT install_complete();
accept();
});
+5 -26
View File
@@ -93,7 +93,6 @@
#include <QMimeData>
#include <QOpenGLContext>
#include <QPainter>
#include <QProgressDialog>
#include <QPushButton>
#include <QSlider>
#include <QStandardPaths>
@@ -715,31 +714,11 @@ void MainWindow::handle_drop(const QString &path) {
const std::string ext = string_utils::tolower(drop_path.extension().string());
if (ext == ".pup") {
QProgressDialog prog(tr("Installing firmware\u2026"),
/*cancel=*/QString(), 0, 100, this);
prog.setWindowTitle(tr("Install Firmware"));
prog.setWindowModality(Qt::WindowModal);
prog.setMinimumDuration(0);
prog.setValue(0);
const std::string fw_version = install_pup(
emuenv.pref_path,
drop_path,
[&prog](uint32_t pct) {
prog.setValue(static_cast<int>(pct));
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
});
prog.setValue(100);
if (!fw_version.empty()) {
LOG_INFO("Firmware {} installed successfully via drop!", fw_version);
on_install_finished();
} else {
QMessageBox::critical(this, tr("Install Failed"),
tr("Failed to install the dropped firmware file.\n"
"Check the log for details."));
}
auto *dlg = new FirmwareInstallDialog(emuenv, path, this);
connect(dlg, &FirmwareInstallDialog::install_complete,
this, &MainWindow::on_install_finished);
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
dlg->exec();
} else if (ext == ".vpk" || ext == ".zip") {
auto *dlg = new ArchiveInstallDialog(emuenv, { drop_path }, this);
+19 -4
View File
@@ -322,10 +322,25 @@ void PkgInstallDialog::run_install(const fs::path &pkg_path,
connect(buttons, &QDialogButtonBox::accepted, this,
[this, del_pkg, del_bin, pkg_path, title_id]() {
if (del_pkg->isChecked())
fs::remove(pkg_path);
if (del_bin->isChecked() && !m_license_path.empty())
fs::remove(m_license_path);
QStringList failed;
// fs::remove throws and crashes the app when the file is locked so use the non throwing overload
if (del_pkg->isChecked()) {
boost::system::error_code error;
fs::remove(pkg_path, error);
if (error)
failed.push_back(QString::fromStdString(pkg_path.filename().string()));
}
if (del_bin->isChecked() && !m_license_path.empty()) {
boost::system::error_code error;
fs::remove(m_license_path, error);
if (error)
failed.push_back(QString::fromStdString(m_license_path.filename().string()));
}
if (!failed.isEmpty()) {
QMessageBox::warning(this, tr("Could Not Delete Files"),
tr("The following files could not be deleted. They may still be open in another program:\n\n%1").arg(failed.join(QStringLiteral("\n"))));
}
Q_EMIT install_complete(title_id);
accept();