Qt: fix some package install cancellation issues

- Abort installation if any thread has errors
- Only clean the directories of packages that actually had errors
- Additionally clean the directories of packages that were cancelled before they could finish
- Clear boot path in case of error or cancelation
- Propagate result to caller
- Skip success message if the installation was canceled
This commit is contained in:
Megamouse 2023-01-10 21:26:17 +01:00
parent 65f10ff840
commit fc85ed8730
3 changed files with 170 additions and 93 deletions

View File

@ -619,7 +619,7 @@ package_error package_reader::check_target_app_version() const
if (!target_app_ver.empty())
{
// We are unable to compare anything with the target app version
pkg_log.error("A target app version is required (%s), but no PARAM.SFO was found for %s", target_app_ver, title_id);
pkg_log.error("A target app version is required (%s), but no PARAM.SFO was found for %s. (path='%s', error=%s)", target_app_ver, title_id, sfo_path, fs::g_tls_error);
return package_error::app_version;
}
@ -840,20 +840,28 @@ bool package_reader::fill_data(std::map<std::string, install_entry*>& all_instal
fs::file DecryptEDAT(const fs::file& input, const std::string& input_file_name, int mode, u8 *custom_klic, bool verbose = false);
usz package_reader::extract_worker(thread_key thread_data_key)
void package_reader::extract_worker(thread_key thread_data_key)
{
usz num_failures = 0;
while (true)
while (m_num_failures == 0 && !m_aborted)
{
const usz maybe_index = m_entry_indexer++;
// Make sure m_entry_indexer does not exceed m_install_entries
const usz index = m_entry_indexer.fetch_op([this](usz& v)
{
if (v < m_install_entries.size())
{
v++;
return true;
}
if (maybe_index >= m_install_entries.size())
return false;
}).first;
if (index >= m_install_entries.size())
{
break;
}
const install_entry& entry = ::at32(m_install_entries, maybe_index);
const install_entry& entry = ::at32(m_install_entries, index);
if (!entry.is_dominating())
{
@ -934,7 +942,7 @@ usz package_reader::extract_worker(thread_key thread_data_key)
out = DecryptEDAT(out, name, 1, reinterpret_cast<u8*>(&m_header.klicensee), true);
if (!out || !fs::write_file(path, fs::rewrite, static_cast<fs::container_stream<std::vector<u8>>*>(out.release().get())->obj))
{
num_failures++;
m_num_failures++;
pkg_log.error("Failed to create file %s", path);
break;
}
@ -959,12 +967,12 @@ usz package_reader::extract_worker(thread_key thread_data_key)
}
else
{
num_failures++;
m_num_failures++;
}
}
else
{
num_failures++;
m_num_failures++;
pkg_log.error("Failed to create file %s", path);
}
@ -972,21 +980,19 @@ usz package_reader::extract_worker(thread_key thread_data_key)
}
default:
{
num_failures++;
m_num_failures++;
pkg_log.error("Unknown PKG entry type (0x%x) %s", entry.type, name);
break;
}
}
}
return num_failures;
}
bool package_reader::extract_data(std::deque<package_reader>& readers, std::deque<std::string>& bootable_paths)
{
std::map<std::string, install_entry*> all_install_entries;
for (auto& reader : readers)
for (package_reader& reader : readers)
{
if (!reader.fill_data(all_install_entries))
{
@ -999,31 +1005,58 @@ bool package_reader::extract_data(std::deque<package_reader>& readers, std::dequ
for (package_reader& reader : readers)
{
reader.m_bufs.resize(std::min<usz>(utils::get_thread_count(), reader.m_install_entries.size()));
reader.m_num_failures = 0;
reader.m_result = result::not_started;
atomic_t<usz> thread_indexer = 0;
named_thread_group workers("PKG Installer "sv, std::max<usz>(reader.m_bufs.size(), 1) - 1, [&]()
{
num_failures += reader.extract_worker(thread_key{thread_indexer++});
reader.extract_worker(thread_key{thread_indexer++});
});
num_failures += reader.extract_worker(thread_key{thread_indexer++});
reader.extract_worker(thread_key{thread_indexer++});
workers.join();
num_failures += reader.m_num_failures;
reader.m_bufs.clear();
reader.m_bufs.shrink_to_fit();
if (num_failures)
// We don't count this package as aborted if all entries were processed.
if (reader.m_num_failures || (reader.m_aborted && reader.m_entry_indexer < reader.m_install_entries.size()))
{
if (reader.m_was_null)
// Clear boot path. We don't want to propagate potentially broken paths to the caller.
reader.m_bootable_file_path.clear();
bool cleaned = reader.m_was_null;
if (reader.m_was_null && fs::is_dir(reader.m_install_path))
{
fs::remove_all(reader.m_install_path, true);
pkg_log.notice("Removing partial installation ('%s')", reader.m_install_path);
if (!fs::remove_all(reader.m_install_path, true))
{
pkg_log.notice("Failed to remove partial installation ('%s') (error=%s)", reader.m_install_path, fs::g_tls_error);
cleaned = false;
}
}
pkg_log.success("Package failed to install ('%s')", reader.m_install_path);
if (reader.m_num_failures)
{
pkg_log.error("Package failed to install ('%s')", reader.m_install_path);
reader.m_result = cleaned ? result::error_cleaned : result::error;
}
else
{
pkg_log.warning("Package installation aborted ('%s')", reader.m_install_path);
reader.m_result = cleaned ? result::aborted_cleaned : result::aborted;
}
break;
}
reader.m_result = result::success;
if (reader.get_progress(1) != 1)
{
pkg_log.warning("Missing %d bytes from PKG total files size.", reader.m_header.data_size - reader.m_written_bytes);
@ -1147,14 +1180,5 @@ int package_reader::get_progress(int maximum) const
void package_reader::abort_extract()
{
m_entry_indexer.fetch_op([this](usz& v)
{
if (v < m_install_entries.size())
{
v = m_install_entries.size();
return true;
}
return false;
});
m_aborted = true;
}

View File

@ -332,10 +332,21 @@ public:
package_reader(const std::string& path);
~package_reader();
enum result
{
not_started,
success,
aborted,
aborted_cleaned,
error,
error_cleaned
};
bool is_valid() const { return m_is_valid; }
package_error check_target_app_version() const;
static bool extract_data(std::deque<package_reader>& readers, std::deque<std::string>& bootable_paths);
psf::registry get_psf() const { return m_psf; }
result get_result() const { return m_result; };
int get_progress(int maximum = 100) const;
@ -351,10 +362,12 @@ private:
bool fill_data(std::map<std::string, install_entry*>& all_install_entries);
std::span<const char> archive_read_block(u64 offset, void* data_ptr, u64 num_bytes);
std::span<const char> decrypt(u64 offset, u64 size, const uchar* key, thread_key thread_data_key = {0});
usz extract_worker(thread_key thread_data_key);
void extract_worker(thread_key thread_data_key);
std::deque<install_entry> m_install_entries;
std::string m_install_path;
atomic_t<bool> m_aborted = false;
atomic_t<usz> m_num_failures = 0;
atomic_t<usz> m_entry_indexer = 0;
atomic_t<usz> m_written_bytes = 0;
bool m_was_null = false;
@ -362,6 +375,7 @@ private:
static constexpr usz BUF_SIZE = 8192 * 1024; // 8 MB
bool m_is_valid = false;
result m_result = result::not_started;
std::string m_path{};
std::string m_install_dir{};

View File

@ -900,6 +900,13 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
Emu.GracefulShutdown(false);
std::vector<std::string> path_vec;
for (const compat::package_info& pkg : packages)
{
path_vec.push_back(pkg.path.toStdString());
}
gui_log.notice("About to install packages:\n%s", fmt::merge(path_vec, "\n"));
progress_dialog pdlg(tr("RPCS3 Package Installer"), tr("Installing package, please wait..."), tr("Cancel"), 0, 1000, false, this);
pdlg.setAutoClose(false);
pdlg.show();
@ -934,7 +941,6 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
};
bool cancelled = false;
std::map<std::string, QString> bootable_paths_installed; // -> title id
std::deque<package_reader> readers;
@ -971,7 +977,7 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
{
cancelled = true;
for (auto& reader : readers)
for (package_reader& reader : readers)
{
reader.abort_extract();
}
@ -1002,15 +1008,45 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
pdlg.SetValue(pdlg.maximum());
std::this_thread::sleep_for(100ms);
for (usz i = 0; i < packages.size(); i++)
{
m_game_list_frame->Refresh(true);
const compat::package_info& package = ::at32(packages, i);
const package_reader& reader = ::at32(readers, i);
for (const auto& package : packages)
switch (reader.get_result())
{
case package_reader::result::success:
{
gui_log.success("Successfully installed %s (title_id=%s, title=%s, version=%s).", sstr(package.path), sstr(package.title_id), sstr(package.title), sstr(package.version));
break;
}
case package_reader::result::not_started:
case package_reader::result::aborted_cleaned:
{
gui_log.notice("Aborted installation of %s (title_id=%s, title=%s, version=%s).", sstr(package.path), sstr(package.title_id), sstr(package.title), sstr(package.version));
break;
}
case package_reader::result::error_cleaned:
{
gui_log.error("Failed to install %s (title_id=%s, title=%s, version=%s).", sstr(package.path), sstr(package.title_id), sstr(package.title), sstr(package.version));
break;
}
case package_reader::result::aborted:
case package_reader::result::error:
{
gui_log.error("Partially installed %s (title_id=%s, title=%s, version=%s).", sstr(package.path), sstr(package.title_id), sstr(package.title), sstr(package.version));
break;
}
}
}
gui_log.success("Package(s) successfully installed!");
m_game_list_frame->Refresh(true);
pdlg.hide();
if (!cancelled)
{
std::map<std::string, QString> bootable_paths_installed; // -> title id
for (usz index = 0; index < bootable_paths.size(); index++)
{
@ -1022,78 +1058,73 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
bootable_paths_installed[bootable_paths[index]] = packages[index].title_id;
}
if (bootable_paths_installed.empty())
{
pdlg.hide();
m_gui_settings->ShowInfoBox(tr("Success!"), tr("Successfully installed software from package(s)!"), gui::ib_pkg_success, this);
return;
}
bool create_desktop_shortcuts = false;
bool create_app_shortcut = false;
auto dlg = new QDialog(this);
dlg->setWindowTitle(tr("Success!"));
if (bootable_paths_installed.empty())
{
m_gui_settings->ShowInfoBox(tr("Success!"), tr("Successfully installed software from package(s)!"), gui::ib_pkg_success, this);
}
else
{
auto dlg = new QDialog(this);
dlg->setWindowTitle(tr("Success!"));
QVBoxLayout* vlayout = new QVBoxLayout(dlg);
QVBoxLayout* vlayout = new QVBoxLayout(dlg);
QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)"));
QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)"));
#ifdef _WIN32
QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)"));
QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)"));
#elif defined(__APPLE__)
QCheckBox* quick_check = new QCheckBox(tr("Add dock shortcut(s)"));
QCheckBox* quick_check = new QCheckBox(tr("Add dock shortcut(s)"));
#else
QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)"));
QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)"));
#endif
QLabel* label = new QLabel(tr("Successfully installed software from package(s)!\nWould you like to install shortcuts to the installed software? (%1 new software detected)\n\n").arg(bootable_paths_installed.size()), dlg);
QLabel* label = new QLabel(tr("Successfully installed software from package(s)!\nWould you like to install shortcuts to the installed software? (%1 new software detected)\n\n").arg(bootable_paths_installed.size()), dlg);
vlayout->addWidget(label);
vlayout->addStretch(10);
vlayout->addWidget(desk_check);
vlayout->addStretch(3);
vlayout->addWidget(quick_check);
vlayout->addStretch(3);
vlayout->addWidget(label);
vlayout->addStretch(10);
vlayout->addWidget(desk_check);
vlayout->addStretch(3);
vlayout->addWidget(quick_check);
vlayout->addStretch(3);
QDialogButtonBox* btn_box = new QDialogButtonBox(QDialogButtonBox::Ok);
QDialogButtonBox* btn_box = new QDialogButtonBox(QDialogButtonBox::Ok);
vlayout->addWidget(btn_box);
dlg->setLayout(vlayout);
vlayout->addWidget(btn_box);
dlg->setLayout(vlayout);
connect(btn_box, &QDialogButtonBox::accepted, this, [&]()
{
create_desktop_shortcuts = desk_check->isChecked();
create_app_shortcut = quick_check->isChecked();
dlg->accept();
});
bool create_desktop_shortcuts = false;
bool create_app_shortcut = false;
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->exec();
}
connect(btn_box, &QDialogButtonBox::accepted, this, [&]()
{
create_desktop_shortcuts = desk_check->isChecked();
create_app_shortcut = quick_check->isChecked();
dlg->accept();
});
std::set<gui::utils::shortcut_location> locations;
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->exec();
std::set<gui::utils::shortcut_location> locations;
#ifdef _WIN32
locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts);
locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts);
#endif
if (create_desktop_shortcuts)
{
locations.insert(gui::utils::shortcut_location::desktop);
}
if (create_app_shortcut)
{
locations.insert(gui::utils::shortcut_location::applications);
}
if (create_desktop_shortcuts)
{
locations.insert(gui::utils::shortcut_location::desktop);
}
if (create_app_shortcut)
{
locations.insert(gui::utils::shortcut_location::applications);
}
for (const auto& [boot_path, title_id] : bootable_paths_installed)
for (const auto& [boot_path, title_id] : bootable_paths_installed)
{
for (const game_info& gameinfo : m_game_list_frame->GetGameInfo())
{
for (const game_info& gameinfo : m_game_list_frame->GetGameInfo())
if (gameinfo && gameinfo->info.bootable && gameinfo->info.serial == sstr(title_id) && boot_path.starts_with(gameinfo->info.path))
{
if (gameinfo && gameinfo->info.bootable && gameinfo->info.serial == sstr(title_id) && boot_path.starts_with(gameinfo->info.path))
{
m_game_list_frame->CreateShortcuts(gameinfo, locations);
break;
}
m_game_list_frame->CreateShortcuts(gameinfo, locations);
break;
}
}
}
@ -1108,12 +1139,20 @@ void main_window::HandlePackageInstallation(QStringList file_paths)
{
const compat::package_info* package = nullptr;
for (usz i = 0; i < readers.size(); i++)
for (usz i = 0; i < readers.size() && !package; i++)
{
// Figure out what package failed the installation
if (readers[i].get_progress(1) != 1)
switch (readers[i].get_result())
{
case package_reader::result::success:
case package_reader::result::not_started:
case package_reader::result::aborted:
case package_reader::result::aborted_cleaned:
break;
case package_reader::result::error:
case package_reader::result::error_cleaned:
package = &packages[i];
break;
}
}