(data.data()), data.size()), &parse_error));
if (doc.isObject())
{
const QJsonObject doc_object(doc.object());
QString changes_html = tr("Changes:
");
changes_html += QStringLiteral("");
const QJsonArray commits(doc_object["commits"].toArray());
bool update_will_break_save_states = false;
bool update_increases_settings_version = false;
for (const QJsonValue& commit : commits)
{
const QJsonObject commit_obj(commit["commit"].toObject());
QString message = commit_obj["message"].toString();
QString author = commit_obj["author"].toObject()["name"].toString();
if (message.contains(QStringLiteral("[SAVEVERSION+]")))
update_will_break_save_states = true;
if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))
update_increases_settings_version = true;
const int first_line_terminator = message.indexOf('\n');
if (first_line_terminator >= 0)
message.remove(first_line_terminator, message.size() - first_line_terminator);
if (!message.isEmpty())
{
changes_html +=
QStringLiteral("- %1 (%2)
").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());
}
}
changes_html += "
";
if (update_will_break_save_states)
{
changes_html.prepend(tr("Save State Warning
Installing this update will make your save states "
"incompatible. Please ensure you have saved your games to a Memory Card "
"before installing this update or you will lose progress.
"));
m_update_will_break_save_states = true;
}
if (update_increases_settings_version)
{
changes_html.prepend(
tr("Settings Warning
Installing this update will reset your program configuration. Please note "
"that you will have to reconfigure your settings after this update.
"));
}
m_ui.updateNotes->setText(changes_html);
}
else
{
reportError("Change list JSON is not an object");
}
}
else
{
reportError("Failed to download change list: %d", status_code);
}
#endif
m_ui.downloadAndInstall->setEnabled(true);
}
void AutoUpdaterDialog::downloadUpdateClicked()
{
if (m_update_will_break_save_states)
{
QMessageBox msgbox;
msgbox.setIcon(QMessageBox::Critical);
msgbox.setWindowTitle(tr("Savestate Warning"));
msgbox.setText(tr("WARNING
Installing this update will make your save states incompatible, be sure to save any progress to your memory cards before proceeding.
Do you wish to continue?
"));
msgbox.addButton(QMessageBox::Yes);
msgbox.addButton(QMessageBox::No);
msgbox.setDefaultButton(QMessageBox::No);
// This makes the box wider, for some reason sizing boxes in Qt is hard - Source: The internet.
QSpacerItem* horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
QGridLayout* layout = (QGridLayout*)msgbox.layout();
layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
if (msgbox.exec() != QMessageBox::Yes)
return;
}
m_display_messages = true;
std::optional download_result;
QtModalProgressCallback progress(this);
progress.SetTitle(tr("Automatic Updater").toUtf8().constData());
progress.SetStatusText(tr("Downloading %1...").arg(m_latest_version).toUtf8().constData());
progress.GetDialog().setWindowIcon(windowIcon());
progress.SetCancellable(true);
m_http->CreateRequest(
m_download_url.toStdString(),
[this, &download_result, &progress](s32 status_code, const std::string&, std::vector data) {
if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)
return;
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
{
reportError("Download failed: %d", status_code);
download_result = false;
return;
}
if (data.empty())
{
reportError("Download failed: Update is empty");
download_result = false;
return;
}
download_result = processUpdate(data, progress.GetDialog());
},
&progress);
// Block until completion.
while (m_http->HasAnyRequests())
{
QApplication::processEvents(QEventLoop::AllEvents, HTTP_POLL_INTERVAL);
m_http->PollRequests();
}
if (download_result.value_or(false))
{
// updater started. since we're a modal on the main window, we have to queue this.
QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, true));
done(0);
}
// download error or cancelled
}
void AutoUpdaterDialog::checkIfUpdateNeeded()
{
const QString last_checked_version(
QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion")));
Console.WriteLn(Color_StrongGreen, "Current version: %s", GIT_TAG);
Console.WriteLn(Color_StrongYellow, "Latest version: %s", m_latest_version.toUtf8().constData());
Console.WriteLn(Color_StrongOrange, "Last checked version: %s", last_checked_version.toUtf8().constData());
if (m_latest_version == GIT_TAG || m_latest_version == last_checked_version)
{
Console.WriteLn(Color_StrongGreen, "No update needed.");
if (m_display_messages)
{
QMessageBox::information(this, tr("Automatic Updater"),
tr("No updates are currently available. Please try again later."));
}
return;
}
Console.WriteLn(Color_StrongRed, "Update needed.");
// Don't show the dialog if a game started while the update info was downloading. Some people have
// really slow connections, apparently. If we're a manual triggered update check, then display
// regardless. This will fall through and signal main to delete us.
if (!m_display_messages &&
(QtHost::IsVMValid() || (g_emu_thread->isRunningFullscreenUI() && g_emu_thread->isFullscreen())))
{
Console.WriteLn(Color_StrongRed, "Not showing update dialog due to active VM.");
return;
}
m_ui.currentVersion->setText(tr("Current Version: %1 (%2)").arg(getCurrentVersion()).arg(getCurrentVersionDate()));
m_ui.newVersion->setText(tr("New Version: %1 (%2)").arg(m_latest_version).arg(m_latest_version_timestamp.toString()));
m_ui.downloadSize->setText(tr("Download Size: %1 MB").arg(static_cast(m_download_size) / 1048576.0, 0, 'f', 2));
m_ui.updateNotes->setText(tr("Loading..."));
queueGetChanges();
// We have to defer this, because it comes back through the timer/HTTP callback...
QMetaObject::invokeMethod(this, "exec", Qt::QueuedConnection);
}
void AutoUpdaterDialog::skipThisUpdateClicked()
{
Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_version.toUtf8().constData());
Host::CommitBaseSettingChanges();
done(0);
}
void AutoUpdaterDialog::remindMeLaterClicked()
{
done(0);
}
#if defined(_WIN32)
bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog&)
{
const QString update_directory = QCoreApplication::applicationDirPath();
const QString update_zip_path = QStringLiteral("%1" FS_OSPATH_SEPARATOR_STR "%2").arg(update_directory).arg(UPDATER_ARCHIVE_NAME);
const QString updater_path = QStringLiteral("%1" FS_OSPATH_SEPARATOR_STR "%2").arg(update_directory).arg(UPDATER_EXECUTABLE);
Q_ASSERT(!update_zip_path.isEmpty() && !updater_path.isEmpty() && !update_directory.isEmpty());
if ((QFile::exists(update_zip_path) && !QFile::remove(update_zip_path)) ||
(QFile::exists(updater_path) && !QFile::remove(updater_path)))
{
reportError("Removing existing update zip/updater failed");
return false;
}
{
QFile update_zip_file(update_zip_path);
if (!update_zip_file.open(QIODevice::WriteOnly) ||
update_zip_file.write(reinterpret_cast(data.data()), static_cast(data.size())) != static_cast(data.size()))
{
reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData());
return false;
}
update_zip_file.close();
}
std::string updater_extract_error;
if (!ExtractUpdater(update_zip_path.toUtf8().constData(), updater_path.toUtf8().constData(), &updater_extract_error))
{
reportError("Extracting updater failed: %s", updater_extract_error.c_str());
return false;
}
if (!doUpdate(update_zip_path, updater_path, update_directory))
{
reportError("Launching updater failed");
return false;
}
return true;
}
bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path)
{
const QString program_path = QCoreApplication::applicationFilePath();
if (program_path.isEmpty())
{
reportError("Failed to get current application path");
return false;
}
QStringList arguments;
arguments << QString::number(QCoreApplication::applicationPid());
arguments << destination_path;
arguments << zip_path;
arguments << program_path;
// this will leak, but not sure how else to handle it...
QProcess* updater_process = new QProcess();
updater_process->setProgram(updater_path);
updater_process->setArguments(arguments);
updater_process->start(QIODevice::NotOpen);
if (!updater_process->waitForStarted())
{
reportError("Failed to start updater");
return false;
}
return true;
}
void AutoUpdaterDialog::cleanupAfterUpdate()
{
// Nothing to do on Windows for now, the updater stub cleans everything up.
}
#elif defined(__linux__)
bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog&)
{
const char* appimage_path = std::getenv("APPIMAGE");
if (!appimage_path || !FileSystem::FileExists(appimage_path))
{
reportError("Missing APPIMAGE.");
return false;
}
const QString qappimage_path(QString::fromUtf8(appimage_path));
if (!QFile::exists(qappimage_path))
{
reportError("Current AppImage does not exist: %s", appimage_path);
return false;
}
const QString new_appimage_path(qappimage_path + QStringLiteral(".new"));
const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
Console.WriteLn("APPIMAGE = %s", appimage_path);
Console.WriteLn("Backup AppImage path = %s", backup_appimage_path.toUtf8().constData());
Console.WriteLn("New AppImage path = %s", new_appimage_path.toUtf8().constData());
// Remove old "new" appimage and existing backup appimage.
if (QFile::exists(new_appimage_path) && !QFile::remove(new_appimage_path))
{
reportError("Failed to remove old destination AppImage: %s", new_appimage_path.toUtf8().constData());
return false;
}
if (QFile::exists(backup_appimage_path) && !QFile::remove(backup_appimage_path))
{
reportError("Failed to remove old backup AppImage: %s", new_appimage_path.toUtf8().constData());
return false;
}
// Write "new" appimage.
{
// We want to copy the permissions from the old appimage to the new one.
QFile old_file(qappimage_path);
const QFileDevice::Permissions old_permissions = old_file.permissions();
QFile new_file(new_appimage_path);
if (!new_file.open(QIODevice::WriteOnly) ||
new_file.write(reinterpret_cast(data.data()), static_cast(data.size())) != static_cast(data.size()) ||
!new_file.setPermissions(old_permissions))
{
QFile::remove(new_appimage_path);
reportError("Failed to write new destination AppImage: %s", new_appimage_path.toUtf8().constData());
return false;
}
}
// Rename "old" appimage.
if (!QFile::rename(qappimage_path, backup_appimage_path))
{
reportError("Failed to rename old AppImage to %s", backup_appimage_path.toUtf8().constData());
QFile::remove(new_appimage_path);
return false;
}
// Rename "new" appimage.
if (!QFile::rename(new_appimage_path, qappimage_path))
{
reportError("Failed to rename new AppImage to %s", qappimage_path.toUtf8().constData());
return false;
}
// Execute new appimage.
QProcess* new_process = new QProcess();
new_process->setProgram(qappimage_path);
new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});
if (!new_process->startDetached())
{
reportError("Failed to execute new AppImage.");
return false;
}
// We exit once we return.
return true;
}
void AutoUpdaterDialog::cleanupAfterUpdate()
{
// Remove old/backup AppImage.
const char* appimage_path = std::getenv("APPIMAGE");
if (!appimage_path)
return;
const QString qappimage_path(QString::fromUtf8(appimage_path));
const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
if (!QFile::exists(backup_appimage_path))
return;
Console.WriteLn(Color_StrongOrange, QStringLiteral("Removing backup AppImage %1").arg(backup_appimage_path).toStdString());
if (!QFile::remove(backup_appimage_path))
Console.Error(QStringLiteral("Failed to remove backup AppImage %1").arg(backup_appimage_path).toStdString());
}
#elif defined(__APPLE__)
static QString UpdateVersionNumberInName(QString name, QStringView new_version)
{
QString current_version_string = QStringLiteral(GIT_TAG);
QStringView current_version = current_version_string;
if (!current_version.empty() && !new_version.empty() && current_version[0] == 'v' && new_version[0] == 'v')
{
current_version = current_version.mid(1);
new_version = new_version.mid(1);
}
if (!current_version.empty() && !new_version.empty())
name.replace(current_version.data(), current_version.size(), new_version.data(), new_version.size());
return name;
}
bool AutoUpdaterDialog::processUpdate(const std::vector& data, QProgressDialog& progress)
{
std::optional path = CocoaTools::GetNonTranslocatedBundlePath();
if (!path.has_value())
{
reportError("Couldn't get bundle path");
return false;
}
QFileInfo info(QString::fromStdString(*path));
if (!info.isBundle())
{
reportError("Application %s isn't a bundle", path->c_str());
return false;
}
if (info.suffix() != QStringLiteral("app"))
{
reportError("Unexpected application suffix %s on %s", info.suffix().toUtf8().constData(), path->c_str());
return false;
}
QString open_path;
{
QTemporaryDir temp_dir(info.path() + QStringLiteral("/PCSX2-UpdateStaging-XXXXXX"));
if (!temp_dir.isValid())
{
reportError("Failed to create update staging directory");
return false;
}
constexpr size_t chunk_size = 65536;
progress.setLabelText(QStringLiteral("Unpacking update..."));
progress.reset();
progress.setRange(0, static_cast((data.size() + chunk_size - 1) / chunk_size));
QProcess untar;
untar.setProgram(QStringLiteral("/usr/bin/tar"));
untar.setArguments({QStringLiteral("xC"), temp_dir.path()});
untar.start();
for (size_t i = 0; i < data.size(); i += chunk_size)
{
progress.setValue(static_cast(i / chunk_size));
const size_t amt = std::min(data.size() - i, chunk_size);
if (progress.wasCanceled() ||
untar.write(reinterpret_cast(data.data() + i), static_cast(amt)) != static_cast(amt))
{
if (!progress.wasCanceled())
reportError("Failed to unpack update (write stopped short)");
untar.closeWriteChannel();
if (!untar.waitForFinished(1000))
untar.kill();
return false;
}
}
untar.closeWriteChannel();
while (!untar.waitForFinished(1000))
{
if (progress.wasCanceled())
{
untar.kill();
return false;
}
}
progress.setValue(progress.maximum());
if (untar.exitCode() != EXIT_SUCCESS)
{
QByteArray msg = untar.readAllStandardError();
const char* join = msg.isEmpty() ? "" : ": ";
reportError("Failed to unpack update (tar exited with %u%s%s)", untar.exitCode(), join, msg.toStdString().c_str());
return false;
}
QFileInfoList temp_dir_contents = QDir(temp_dir.path()).entryInfoList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot);
auto new_app = std::find_if(temp_dir_contents.begin(), temp_dir_contents.end(), [](const QFileInfo& file) { return file.suffix() == QStringLiteral("app"); });
if (new_app == temp_dir_contents.end())
{
reportError("Couldn't find application in update package");
return false;
}
QString new_name = UpdateVersionNumberInName(info.completeBaseName(), m_latest_version);
std::optional trashed_path = CocoaTools::MoveToTrash(*path);
if (!trashed_path.has_value())
{
reportError("Failed to trash old application");
return false;
}
open_path = info.path() + QStringLiteral("/") + new_name + QStringLiteral(".app");
if (!QFile::rename(new_app->absoluteFilePath(), open_path))
{
QFile::rename(QString::fromStdString(*trashed_path), info.filePath());
reportError("Failed to move new application into place (couldn't rename '%s' to '%s')",
new_app->absoluteFilePath().toUtf8().constData(), open_path.toUtf8().constData());
return false;
}
QDir(QString::fromStdString(*trashed_path)).removeRecursively();
}
// For some reason if I use QProcess the shell gets killed immediately with SIGKILL, but NSTask is fine...
if (!CocoaTools::DelayedLaunch(open_path.toStdString()))
{
reportError("Failed to start new application");
return false;
}
return true;
}
void AutoUpdaterDialog::cleanupAfterUpdate()
{
}
#else
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog& progress)
{
return false;
}
void AutoUpdaterDialog::cleanupAfterUpdate()
{
}
#endif