mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-04-07 09:01:29 -06:00
Merge 7a6532171f into 3066887ff4
This commit is contained in:
commit
4a7d6095d7
@ -143,6 +143,8 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via
|
||||
option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
|
||||
option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON)
|
||||
|
||||
set(AZAHAR_IPC_SOCKET_NAME "" CACHE STRING "Override IPC socket name (e.g. azahar-prof-ipc)")
|
||||
|
||||
# Handle incompatible options for libretro builds
|
||||
if(ENABLE_LIBRETRO)
|
||||
# Check for explicitly-set conflicting options
|
||||
@ -351,7 +353,7 @@ if (ENABLE_QT)
|
||||
download_qt(6.9.3)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent Network)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus)
|
||||
|
||||
@ -197,6 +197,7 @@ endif()
|
||||
|
||||
if (ENABLE_QT)
|
||||
add_subdirectory(citra_qt)
|
||||
add_subdirectory(azahar_ctl)
|
||||
endif()
|
||||
|
||||
if (ENABLE_QT) # Or any other hypothetical future frontends
|
||||
|
||||
15
src/azahar_ctl/CMakeLists.txt
Normal file
15
src/azahar_ctl/CMakeLists.txt
Normal file
@ -0,0 +1,15 @@
|
||||
add_executable(azahar-ctl
|
||||
azahar_ctl.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(azahar-ctl PRIVATE Qt6::Core Qt6::Network)
|
||||
|
||||
if (AZAHAR_IPC_SOCKET_NAME)
|
||||
target_compile_definitions(azahar-ctl PRIVATE AZAHAR_IPC_SOCKET_NAME="${AZAHAR_IPC_SOCKET_NAME}")
|
||||
endif()
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
install(TARGETS azahar-ctl RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
||||
elseif(WIN32)
|
||||
install(TARGETS azahar-ctl RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
||||
endif()
|
||||
173
src/azahar_ctl/azahar_ctl.cpp
Normal file
173
src/azahar_ctl/azahar_ctl.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QElapsedTimer>
|
||||
#include <QLocalSocket>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#ifdef AZAHAR_IPC_SOCKET_NAME
|
||||
static constexpr const char* SOCKET_NAME = AZAHAR_IPC_SOCKET_NAME;
|
||||
#else
|
||||
static constexpr const char* SOCKET_NAME = "azahar-dev-ipc";
|
||||
#endif
|
||||
static constexpr int CONNECT_TIMEOUT_MS = 3000;
|
||||
static constexpr int DEFAULT_TIMEOUT_MS = 5000;
|
||||
static constexpr int RELOAD_TIMEOUT_MS = 60000;
|
||||
|
||||
// Exit codes
|
||||
static constexpr int EXIT_OK = 0;
|
||||
static constexpr int EXIT_USAGE = 1;
|
||||
static constexpr int EXIT_CONNECT_FAILED = 2;
|
||||
static constexpr int EXIT_TIMEOUT = 3;
|
||||
static constexpr int EXIT_SERVER_ERROR = 4;
|
||||
|
||||
static void PrintUsage() {
|
||||
std::fprintf(stderr, "Usage: azahar-ctl <command> [args...]\n\n");
|
||||
std::fprintf(stderr, "Commands:\n");
|
||||
std::fprintf(stderr, " ping Check if Azahar is running\n");
|
||||
std::fprintf(stderr, " status Show emulation state\n");
|
||||
std::fprintf(stderr, " shutdown Stop current game\n");
|
||||
std::fprintf(stderr, " hot-reload <file> [--purge] [--wipe]\n");
|
||||
std::fprintf(stderr,
|
||||
" Reload cycle (CIA/3DSX/ELF/3DS/CXI/APP)\n");
|
||||
std::fprintf(stderr,
|
||||
" --purge Uninstall all titles before CIA "
|
||||
"install\n");
|
||||
std::fprintf(stderr,
|
||||
" --wipe Delete save data for the replaced "
|
||||
"title\n");
|
||||
std::fprintf(stderr, " hot-reload --last Re-run the previous hot-reload\n");
|
||||
std::fprintf(stderr, " help Show this help message\n");
|
||||
std::fprintf(stderr, "\nOptions:\n");
|
||||
std::fprintf(stderr,
|
||||
" --timeout <ms> Override response timeout (default: "
|
||||
"5000/60000)\n");
|
||||
std::fprintf(stderr, "\nExit codes:\n");
|
||||
std::fprintf(stderr, " 0 Success\n");
|
||||
std::fprintf(stderr, " 1 Usage error\n");
|
||||
std::fprintf(stderr, " 2 Cannot connect to Azahar\n");
|
||||
std::fprintf(stderr, " 3 Timeout waiting for response\n");
|
||||
std::fprintf(stderr, " 4 Server returned an error\n");
|
||||
}
|
||||
|
||||
static bool ReadSingleLine(QLocalSocket& socket, int timeout_ms, QString& out) {
|
||||
QByteArray buffer;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
while (true) {
|
||||
const int remaining = timeout_ms - static_cast<int>(timer.elapsed());
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
if (!socket.waitForReadyRead(remaining)) {
|
||||
break;
|
||||
}
|
||||
buffer.append(socket.readAll());
|
||||
if (buffer.contains('\n')) {
|
||||
out = QString::fromUtf8(buffer).trimmed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (buffer.contains('\n')) {
|
||||
out = QString::fromUtf8(buffer).trimmed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int ExtractTimeout(int& argc, char* argv[], int default_timeout) {
|
||||
int timeout = default_timeout;
|
||||
for (int i = 1; i < argc - 1; i++) {
|
||||
if (QString::fromLocal8Bit(argv[i]) == QStringLiteral("--timeout")) {
|
||||
timeout = std::atoi(argv[i + 1]);
|
||||
if (timeout <= 0) {
|
||||
timeout = default_timeout;
|
||||
}
|
||||
for (int j = i; j < argc - 2; j++) {
|
||||
argv[j] = argv[j + 2];
|
||||
}
|
||||
argc -= 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
if (argc < 2) {
|
||||
PrintUsage();
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
|
||||
const QString verb = QString::fromLocal8Bit(argv[1]);
|
||||
|
||||
if (verb == QStringLiteral("help") || verb == QStringLiteral("--help") ||
|
||||
verb == QStringLiteral("-h")) {
|
||||
PrintUsage();
|
||||
return EXIT_OK;
|
||||
}
|
||||
|
||||
int custom_timeout = ExtractTimeout(argc, argv, 0);
|
||||
QString command;
|
||||
|
||||
if (verb == QStringLiteral("ping")) {
|
||||
command = QStringLiteral("PING");
|
||||
} else if (verb == QStringLiteral("status")) {
|
||||
command = QStringLiteral("STATUS");
|
||||
} else if (verb == QStringLiteral("shutdown")) {
|
||||
command = QStringLiteral("SHUTDOWN");
|
||||
} else if (verb == QStringLiteral("hot-reload")) {
|
||||
if (argc == 3 && QString::fromLocal8Bit(argv[2]) == QStringLiteral("--last")) {
|
||||
command = QStringLiteral("HOT_RELOAD_LAST");
|
||||
} else {
|
||||
if (argc < 3) {
|
||||
std::fprintf(stderr, "Error: hot-reload requires a file path or --last\n");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
QString path = QString::fromLocal8Bit(argv[2]);
|
||||
command = QStringLiteral("HOT_RELOAD ") + path;
|
||||
for (int i = 3; i < argc; i++) {
|
||||
command += QStringLiteral(" ") + QString::fromLocal8Bit(argv[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::fprintf(stderr, "Unknown command: %s\n", argv[1]);
|
||||
PrintUsage();
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(QString::fromLatin1(SOCKET_NAME));
|
||||
|
||||
if (!socket.waitForConnected(CONNECT_TIMEOUT_MS)) {
|
||||
std::fprintf(stderr, "Error: Cannot connect to Azahar.\n");
|
||||
std::fprintf(stderr, "Make sure Azahar is running and 'Enable developer IPC server'\n");
|
||||
std::fprintf(stderr, "is checked in Emulation > Configuration > Debug.\n");
|
||||
return EXIT_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
socket.write((command + QStringLiteral("\n")).toUtf8());
|
||||
socket.flush();
|
||||
|
||||
int timeout = custom_timeout > 0 ? custom_timeout
|
||||
: verb == QStringLiteral("hot-reload") ? RELOAD_TIMEOUT_MS
|
||||
: DEFAULT_TIMEOUT_MS;
|
||||
|
||||
QString response;
|
||||
if (!ReadSingleLine(socket, timeout, response)) {
|
||||
std::fprintf(stderr, "Error: Timeout waiting for response\n");
|
||||
return EXIT_TIMEOUT;
|
||||
}
|
||||
|
||||
std::printf("%s\n", response.toLocal8Bit().constData());
|
||||
|
||||
socket.disconnectFromServer();
|
||||
|
||||
return response.startsWith(QStringLiteral("ERR")) ? EXIT_SERVER_ERROR : EXIT_OK;
|
||||
}
|
||||
@ -29,6 +29,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL
|
||||
camera/qt_multimedia_camera.h
|
||||
citra_qt.cpp
|
||||
citra_qt.h
|
||||
dev_ipc_server.cpp
|
||||
dev_ipc_server.h
|
||||
configuration/config.cpp
|
||||
configuration/config.h
|
||||
configuration/configure.ui
|
||||
@ -272,7 +274,11 @@ endif()
|
||||
create_target_directory_groups(citra_qt)
|
||||
|
||||
target_link_libraries(citra_qt PRIVATE audio_core citra_common citra_core input_common network video_core)
|
||||
target_link_libraries(citra_qt PRIVATE Boost::boost nihstro-headers Qt6::Widgets Qt6::Multimedia Qt6::Concurrent)
|
||||
target_link_libraries(citra_qt PRIVATE Boost::boost nihstro-headers Qt6::Widgets Qt6::Multimedia Qt6::Concurrent Qt6::Network)
|
||||
|
||||
if (AZAHAR_IPC_SOCKET_NAME)
|
||||
target_compile_definitions(citra_qt PRIVATE AZAHAR_IPC_SOCKET_NAME="${AZAHAR_IPC_SOCKET_NAME}")
|
||||
endif()
|
||||
target_link_libraries(citra_qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
|
||||
|
||||
if (ENABLE_OPENGL)
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
#include "citra_qt/configuration/config.h"
|
||||
#include "citra_qt/configuration/configure_dialog.h"
|
||||
#include "citra_qt/configuration/configure_per_game.h"
|
||||
#include "citra_qt/dev_ipc_server.h"
|
||||
#include "citra_qt/debugger/console.h"
|
||||
#include "citra_qt/debugger/graphics/graphics.h"
|
||||
#include "citra_qt/debugger/graphics/graphics_breakpoints.h"
|
||||
@ -394,6 +395,14 @@ GMainWindow::GMainWindow(Core::System& system_)
|
||||
ConnectMenuEvents();
|
||||
ConnectWidgetEvents();
|
||||
|
||||
dev_ipc_server_ = new DevIpcServer(this, this);
|
||||
connect(dev_ipc_server_, &DevIpcServer::HotReloadRequested, this,
|
||||
&GMainWindow::OnHotReloadRequested);
|
||||
connect(dev_ipc_server_, &DevIpcServer::ShutdownRequested, this, &GMainWindow::ShutdownGame);
|
||||
if (UISettings::values.enable_dev_ipc_server.GetValue()) {
|
||||
dev_ipc_server_->Start();
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
|
||||
Common::g_scm_desc);
|
||||
#if CITRA_ARCH(x86_64)
|
||||
@ -2303,6 +2312,12 @@ void GMainWindow::OnMenuSetUpSystemFiles() {
|
||||
}
|
||||
|
||||
void GMainWindow::OnMenuInstallCIA() {
|
||||
if (hot_reload_pending_) {
|
||||
QMessageBox::warning(this, tr("Install CIA"),
|
||||
tr("Cannot install CIA while a hot-reload is in progress."));
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList filepaths = QFileDialog::getOpenFileNames(
|
||||
this, tr("Load Files"), UISettings::values.roms_path,
|
||||
tr("3DS Installation File (*.cia *.zcia)") + QStringLiteral(";;") + tr("All Files (*.*)"));
|
||||
@ -2360,6 +2375,13 @@ void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) {
|
||||
}
|
||||
|
||||
void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) {
|
||||
if (hot_reload_pending_) {
|
||||
if (status != Service::AM::InstallStatus::Success) {
|
||||
hot_reload_install_status_ = status;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QString filename = QFileInfo(filepath).fileName();
|
||||
switch (status) {
|
||||
case Service::AM::InstallStatus::Success:
|
||||
@ -2420,9 +2442,159 @@ void GMainWindow::OnCIAInstallFinished() {
|
||||
progress_bar->setValue(0);
|
||||
game_list->SetDirectoryWatcherEnabled(true);
|
||||
ui->action_Install_CIA->setEnabled(true);
|
||||
|
||||
if (hot_reload_pending_) {
|
||||
hot_reload_pending_ = false;
|
||||
|
||||
if (hot_reload_install_status_ != Service::AM::InstallStatus::Success) {
|
||||
const QString err = QStringLiteral("CIA install failed (status=%1)")
|
||||
.arg(static_cast<u32>(hot_reload_install_status_));
|
||||
LOG_ERROR(Frontend, "Hot-reload: {}", err.toStdString());
|
||||
dev_ipc_server_->OnHotReloadComplete(false, err);
|
||||
hot_reload_cia_path_.clear();
|
||||
hot_reload_install_status_ = Service::AM::InstallStatus::Success;
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = Service::AM::GetCIAInfos(hot_reload_cia_path_.toStdString());
|
||||
if (result.Succeeded()) {
|
||||
const auto& info = result.Unwrap().first;
|
||||
u64 title_id = info.tid;
|
||||
auto media_type = Service::AM::GetTitleMediaType(title_id);
|
||||
std::string app_path = Service::AM::GetTitleContentPath(media_type, title_id);
|
||||
|
||||
if (FileUtil::Exists(app_path)) {
|
||||
LOG_INFO(Frontend, "Hot-reload: Booting {}", app_path);
|
||||
BootGame(QString::fromStdString(app_path));
|
||||
dev_ipc_server_->OnHotReloadComplete(true, QString{});
|
||||
} else {
|
||||
const QString err =
|
||||
QStringLiteral("Installed .app not found at: ") +
|
||||
QString::fromStdString(app_path);
|
||||
LOG_ERROR(Frontend, "Hot-reload: {}", err.toStdString());
|
||||
dev_ipc_server_->OnHotReloadComplete(false, err);
|
||||
}
|
||||
} else {
|
||||
dev_ipc_server_->OnHotReloadComplete(false,
|
||||
QStringLiteral("Failed to read CIA title info"));
|
||||
}
|
||||
|
||||
hot_reload_cia_path_.clear();
|
||||
hot_reload_install_status_ = Service::AM::InstallStatus::Success;
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
return;
|
||||
}
|
||||
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
|
||||
void GMainWindow::OnHotReloadRequested(const QString& file_path, bool purge,
|
||||
bool wipe_saves) {
|
||||
LOG_INFO(Frontend, "Hot-reload requested: {} (purge={}, wipe={})", file_path.toStdString(),
|
||||
purge, wipe_saves);
|
||||
|
||||
const QString ext = QFileInfo(file_path).suffix().toLower();
|
||||
const bool is_cia = (ext == QStringLiteral("cia"));
|
||||
const bool is_direct_boot = (ext == QStringLiteral("3dsx") || ext == QStringLiteral("elf") ||
|
||||
ext == QStringLiteral("3ds") || ext == QStringLiteral("cxi") ||
|
||||
ext == QStringLiteral("app"));
|
||||
|
||||
if (!is_cia && !is_direct_boot) {
|
||||
LOG_ERROR(Frontend, "Hot-reload: unsupported file type: {}", ext.toStdString());
|
||||
dev_ipc_server_->OnHotReloadComplete(
|
||||
false, QStringLiteral("Unsupported file type: .") + ext);
|
||||
return;
|
||||
}
|
||||
|
||||
if (emulation_running) {
|
||||
auto video_dumper = system.GetVideoDumper();
|
||||
if (video_dumper && video_dumper->IsDumping()) {
|
||||
OnStopVideoDumping();
|
||||
}
|
||||
ShutdownGame();
|
||||
}
|
||||
|
||||
if (is_direct_boot) {
|
||||
LOG_INFO(Frontend, "Hot-reload: Direct-booting {}", file_path.toStdString());
|
||||
BootGame(file_path);
|
||||
dev_ipc_server_->OnHotReloadComplete(true, QString{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (purge) {
|
||||
// Uninstall all SDMC game titles (00040000)
|
||||
const std::string sdmc_games_path =
|
||||
Service::AM::GetMediaTitlePath(Service::FS::MediaType::SDMC) + "00040000/";
|
||||
|
||||
if (FileUtil::Exists(sdmc_games_path)) {
|
||||
FileUtil::FSTEntry parent;
|
||||
FileUtil::ScanDirectoryTree(sdmc_games_path, parent, 0);
|
||||
|
||||
for (const auto& entry : parent.children) {
|
||||
if (!entry.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 tid_low = std::strtoul(entry.virtualName.c_str(), nullptr, 16);
|
||||
u64 title_id = (static_cast<u64>(0x00040000) << 32) | tid_low;
|
||||
|
||||
Service::AM::UninstallProgram(Service::FS::MediaType::SDMC, title_id);
|
||||
|
||||
if (wipe_saves) {
|
||||
std::string data_path =
|
||||
Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, title_id) +
|
||||
"data/";
|
||||
if (FileUtil::Exists(data_path)) {
|
||||
FileUtil::DeleteDirRecursively(data_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall update titles (0004000e)
|
||||
const std::string sdmc_updates_path =
|
||||
Service::AM::GetMediaTitlePath(Service::FS::MediaType::SDMC) + "0004000e/";
|
||||
|
||||
if (FileUtil::Exists(sdmc_updates_path)) {
|
||||
FileUtil::FSTEntry parent;
|
||||
FileUtil::ScanDirectoryTree(sdmc_updates_path, parent, 0);
|
||||
|
||||
for (const auto& entry : parent.children) {
|
||||
if (!entry.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 tid_low = std::strtoul(entry.virtualName.c_str(), nullptr, 16);
|
||||
u64 title_id = (static_cast<u64>(0x0004000e) << 32) | tid_low;
|
||||
|
||||
Service::AM::UninstallProgram(Service::FS::MediaType::SDMC, title_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto cia_info = Service::AM::GetCIAInfos(file_path.toStdString());
|
||||
if (cia_info.Succeeded()) {
|
||||
u64 title_id = cia_info.Unwrap().first.tid;
|
||||
auto media_type = Service::AM::GetTitleMediaType(title_id);
|
||||
Service::AM::UninstallProgram(media_type, title_id);
|
||||
|
||||
if (wipe_saves) {
|
||||
std::string data_path =
|
||||
Service::AM::GetTitlePath(media_type, title_id) + "data/";
|
||||
if (FileUtil::Exists(data_path)) {
|
||||
FileUtil::DeleteDirRecursively(data_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hot_reload_pending_ = true;
|
||||
hot_reload_cia_path_ = file_path;
|
||||
hot_reload_install_status_ = Service::AM::InstallStatus::Success;
|
||||
|
||||
InstallCIA(QStringList{file_path});
|
||||
}
|
||||
|
||||
void GMainWindow::UninstallTitles(
|
||||
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles) {
|
||||
if (titles.empty()) {
|
||||
@ -2837,6 +3009,7 @@ void GMainWindow::OnConfigure() {
|
||||
#ifdef __unix__
|
||||
const bool old_gamemode = Settings::values.enable_gamemode.GetValue();
|
||||
#endif
|
||||
const bool old_ipc_server = UISettings::values.enable_dev_ipc_server.GetValue();
|
||||
auto result = configureDialog.exec();
|
||||
game_list->SetDirectoryWatcherEnabled(true);
|
||||
if (result == QDialog::Accepted) {
|
||||
@ -2855,6 +3028,13 @@ void GMainWindow::OnConfigure() {
|
||||
SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
|
||||
}
|
||||
#endif
|
||||
if (UISettings::values.enable_dev_ipc_server.GetValue() != old_ipc_server) {
|
||||
if (UISettings::values.enable_dev_ipc_server.GetValue()) {
|
||||
dev_ipc_server_->Start();
|
||||
} else {
|
||||
dev_ipc_server_->Stop();
|
||||
}
|
||||
}
|
||||
if (!multiplayer_state->IsHostingPublicRoom())
|
||||
multiplayer_state->UpdateCredentials();
|
||||
emit UpdateThemedIcons();
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
class AboutDialog;
|
||||
class QtConfig;
|
||||
class ClickableLabel;
|
||||
class DevIpcServer;
|
||||
class EmuThread;
|
||||
class GameList;
|
||||
enum class GameListOpenTarget;
|
||||
@ -121,6 +122,13 @@ public:
|
||||
void UninstallTitles(
|
||||
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles);
|
||||
|
||||
bool IsEmulationRunning() const {
|
||||
return emulation_running;
|
||||
}
|
||||
u64 GetGameTitleId() const {
|
||||
return game_title_id;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void OnAppFocusStateChanged(Qt::ApplicationState state);
|
||||
void OnLoadComplete();
|
||||
@ -315,6 +323,7 @@ private slots:
|
||||
#ifdef ENABLE_DEVELOPER_OPTIONS
|
||||
void StartLaunchStressTest(const QString& game_path);
|
||||
#endif
|
||||
void OnHotReloadRequested(const QString& file_path, bool purge, bool wipe_saves);
|
||||
|
||||
private:
|
||||
Q_INVOKABLE void OnMoviePlaybackCompleted();
|
||||
@ -444,6 +453,11 @@ private:
|
||||
|
||||
std::shared_ptr<Camera::QtMultimediaCameraHandlerFactory> qt_cameras;
|
||||
|
||||
DevIpcServer* dev_ipc_server_ = nullptr;
|
||||
bool hot_reload_pending_ = false;
|
||||
QString hot_reload_cia_path_;
|
||||
Service::AM::InstallStatus hot_reload_install_status_{};
|
||||
|
||||
#ifdef ENABLE_QT_UPDATE_CHECKER
|
||||
// Prompt shown when update check succeeds
|
||||
QFuture<QString> update_future;
|
||||
|
||||
@ -828,6 +828,7 @@ void QtConfig::ReadUIValues() {
|
||||
ReadBasicSetting(UISettings::values.enable_discord_presence);
|
||||
#endif
|
||||
ReadBasicSetting(UISettings::values.screenshot_resolution_factor);
|
||||
ReadBasicSetting(UISettings::values.enable_dev_ipc_server);
|
||||
|
||||
ReadUILayoutValues();
|
||||
ReadUIGameListValues();
|
||||
@ -1356,6 +1357,7 @@ void QtConfig::SaveUIValues() {
|
||||
WriteBasicSetting(UISettings::values.enable_discord_presence);
|
||||
#endif
|
||||
WriteBasicSetting(UISettings::values.screenshot_resolution_factor);
|
||||
WriteBasicSetting(UISettings::values.enable_dev_ipc_server);
|
||||
|
||||
SaveUILayoutValues();
|
||||
SaveUIGameListValues();
|
||||
|
||||
@ -110,6 +110,8 @@ void ConfigureDebug::SetConfiguration() {
|
||||
#ifndef ENABLE_SCRIPTING
|
||||
ui->enable_rpc_server->setVisible(false);
|
||||
#endif // !ENABLE_SCRIPTING
|
||||
ui->enable_dev_ipc_server->setChecked(
|
||||
UISettings::values.enable_dev_ipc_server.GetValue());
|
||||
ui->toggle_unique_data_console_type->setChecked(
|
||||
Settings::values.toggle_unique_data_console_type.GetValue());
|
||||
|
||||
@ -151,6 +153,7 @@ void ConfigureDebug::ApplyConfiguration() {
|
||||
Settings::values.deterministic_async_operations =
|
||||
ui->deterministic_async_operations->isChecked();
|
||||
Settings::values.enable_rpc_server = ui->enable_rpc_server->isChecked();
|
||||
UISettings::values.enable_dev_ipc_server = ui->enable_dev_ipc_server->isChecked();
|
||||
Settings::values.toggle_unique_data_console_type =
|
||||
ui->toggle_unique_data_console_type->isChecked();
|
||||
Settings::values.renderer_debug = ui->toggle_renderer_debug->isChecked();
|
||||
|
||||
@ -299,6 +299,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="enable_dev_ipc_server">
|
||||
<property name="text">
|
||||
<string>Enable developer IPC server</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Enables a local IPC server for hot-reload and remote control via azahar-ctl. Takes effect immediately.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
238
src/citra_qt/dev_ipc_server.cpp
Normal file
238
src/citra_qt/dev_ipc_server.cpp
Normal file
@ -0,0 +1,238 @@
|
||||
// Copyright Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QFile>
|
||||
#include "citra_qt/dev_ipc_server.h"
|
||||
#include "citra_qt/citra_qt.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace {
|
||||
#ifdef AZAHAR_IPC_SOCKET_NAME
|
||||
constexpr const char* SOCKET_NAME = AZAHAR_IPC_SOCKET_NAME;
|
||||
#else
|
||||
constexpr const char* SOCKET_NAME = "azahar-dev-ipc";
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
DevIpcServer::DevIpcServer(GMainWindow* main_window, QObject* parent)
|
||||
: QObject(parent), main_window_(main_window) {
|
||||
server_ = new QLocalServer(this);
|
||||
connect(server_, &QLocalServer::newConnection, this, &DevIpcServer::OnNewConnection);
|
||||
}
|
||||
|
||||
DevIpcServer::~DevIpcServer() {
|
||||
if (server_ && server_->isListening()) {
|
||||
server_->close();
|
||||
}
|
||||
}
|
||||
|
||||
bool DevIpcServer::Start() {
|
||||
if (server_->isListening()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QLocalServer::removeServer(QString::fromLatin1(SOCKET_NAME));
|
||||
|
||||
if (!server_->listen(QString::fromLatin1(SOCKET_NAME))) {
|
||||
LOG_ERROR(Frontend, "DevIpcServer: Failed to listen: {}",
|
||||
server_->errorString().toStdString());
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
LOG_INFO(Frontend, "DevIpcServer: Listening on \\\\.\\pipe\\{}", SOCKET_NAME);
|
||||
#else
|
||||
LOG_INFO(Frontend, "DevIpcServer: Listening on {}", SOCKET_NAME);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void DevIpcServer::Stop() {
|
||||
if (server_->isListening()) {
|
||||
server_->close();
|
||||
LOG_INFO(Frontend, "DevIpcServer: Stopped");
|
||||
}
|
||||
pending_reload_socket_ = nullptr;
|
||||
reload_in_progress_ = false;
|
||||
read_buffers_.clear();
|
||||
}
|
||||
|
||||
void DevIpcServer::OnNewConnection() {
|
||||
while (QLocalSocket* socket = server_->nextPendingConnection()) {
|
||||
if (read_buffers_.size() >= MAX_CONNECTIONS) {
|
||||
LOG_WARNING(Frontend, "DevIpcServer: Rejecting connection (limit {})",
|
||||
MAX_CONNECTIONS);
|
||||
socket->disconnectFromServer();
|
||||
socket->deleteLater();
|
||||
continue;
|
||||
}
|
||||
read_buffers_[socket] = {};
|
||||
connect(socket, &QLocalSocket::readyRead, this, &DevIpcServer::OnReadyRead);
|
||||
connect(socket, &QLocalSocket::disconnected, this, &DevIpcServer::OnDisconnected);
|
||||
}
|
||||
}
|
||||
|
||||
void DevIpcServer::OnReadyRead() {
|
||||
auto* socket = qobject_cast<QLocalSocket*>(sender());
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray& buffer = read_buffers_[socket];
|
||||
buffer.append(socket->readAll());
|
||||
|
||||
if (buffer.size() > MAX_BUFFER_SIZE) {
|
||||
LOG_WARNING(Frontend, "DevIpcServer: Client exceeded buffer limit, disconnecting");
|
||||
read_buffers_.remove(socket);
|
||||
socket->disconnectFromServer();
|
||||
socket->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
while (buffer.contains('\n')) {
|
||||
const int newline_pos = buffer.indexOf('\n');
|
||||
const QByteArray line_bytes = buffer.left(newline_pos).trimmed();
|
||||
buffer.remove(0, newline_pos + 1);
|
||||
|
||||
if (!line_bytes.isEmpty()) {
|
||||
HandleCommand(socket, QString::fromUtf8(line_bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DevIpcServer::OnDisconnected() {
|
||||
auto* socket = qobject_cast<QLocalSocket*>(sender());
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket == pending_reload_socket_) {
|
||||
pending_reload_socket_ = nullptr;
|
||||
LOG_WARNING(Frontend, "DevIpcServer: Hot-reload client disconnected before completion");
|
||||
}
|
||||
|
||||
read_buffers_.remove(socket);
|
||||
socket->deleteLater();
|
||||
}
|
||||
|
||||
QString DevIpcServer::ParseFlags(const QString& args, bool& purge, bool& wipe) {
|
||||
const QStringList tokens = args.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
QStringList path_parts;
|
||||
purge = false;
|
||||
wipe = false;
|
||||
|
||||
for (const auto& token : tokens) {
|
||||
if (token == QStringLiteral("--purge")) {
|
||||
purge = true;
|
||||
} else if (token == QStringLiteral("--wipe")) {
|
||||
wipe = true;
|
||||
} else {
|
||||
path_parts.append(token);
|
||||
}
|
||||
}
|
||||
|
||||
return path_parts.join(QLatin1Char(' '));
|
||||
}
|
||||
|
||||
void DevIpcServer::HandleCommand(QLocalSocket* socket, const QString& command) {
|
||||
LOG_DEBUG(Frontend, "DevIpcServer: Received command: {}", command.toStdString());
|
||||
|
||||
if (command == QStringLiteral("PING")) {
|
||||
SendResponse(socket, QStringLiteral("OK"));
|
||||
} else if (command == QStringLiteral("STATUS")) {
|
||||
SendResponse(socket, GetStatus());
|
||||
} else if (command == QStringLiteral("SHUTDOWN")) {
|
||||
if (main_window_->IsEmulationRunning()) {
|
||||
emit ShutdownRequested();
|
||||
SendResponse(socket, QStringLiteral("OK"));
|
||||
} else {
|
||||
SendResponse(socket, QStringLiteral("ERR:not-running"));
|
||||
}
|
||||
} else if (command.startsWith(QStringLiteral("HOT_RELOAD "))) {
|
||||
if (reload_in_progress_) {
|
||||
SendResponse(socket, QStringLiteral("ERR:reload-in-progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
bool purge, wipe;
|
||||
const QString file_path = ParseFlags(command.mid(11), purge, wipe);
|
||||
|
||||
if (file_path.isEmpty()) {
|
||||
SendResponse(socket, QStringLiteral("ERR:missing-file-path"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QFile::exists(file_path)) {
|
||||
SendResponse(socket, QStringLiteral("ERR:file-not-found"));
|
||||
return;
|
||||
}
|
||||
|
||||
pending_reload_path_ = file_path;
|
||||
pending_reload_purge_ = purge;
|
||||
pending_reload_wipe_ = wipe;
|
||||
|
||||
reload_in_progress_ = true;
|
||||
pending_reload_socket_ = socket;
|
||||
emit HotReloadRequested(file_path, purge, wipe);
|
||||
} else if (command == QStringLiteral("HOT_RELOAD_LAST")) {
|
||||
if (reload_in_progress_) {
|
||||
SendResponse(socket, QStringLiteral("ERR:reload-in-progress"));
|
||||
return;
|
||||
}
|
||||
if (last_reload_path_.isEmpty()) {
|
||||
SendResponse(socket, QStringLiteral("ERR:no-previous-reload"));
|
||||
return;
|
||||
}
|
||||
if (!QFile::exists(last_reload_path_)) {
|
||||
SendResponse(socket, QStringLiteral("ERR:file-not-found"));
|
||||
return;
|
||||
}
|
||||
|
||||
pending_reload_path_ = last_reload_path_;
|
||||
pending_reload_purge_ = last_reload_purge_;
|
||||
pending_reload_wipe_ = last_reload_wipe_;
|
||||
|
||||
reload_in_progress_ = true;
|
||||
pending_reload_socket_ = socket;
|
||||
emit HotReloadRequested(last_reload_path_, last_reload_purge_, last_reload_wipe_);
|
||||
} else {
|
||||
SendResponse(socket, QStringLiteral("ERR:unknown-command"));
|
||||
}
|
||||
}
|
||||
|
||||
void DevIpcServer::OnHotReloadComplete(bool success, const QString& error) {
|
||||
reload_in_progress_ = false;
|
||||
|
||||
if (success) {
|
||||
last_reload_path_ = pending_reload_path_;
|
||||
last_reload_purge_ = pending_reload_purge_;
|
||||
last_reload_wipe_ = pending_reload_wipe_;
|
||||
}
|
||||
|
||||
if (pending_reload_socket_ &&
|
||||
pending_reload_socket_->state() == QLocalSocket::ConnectedState) {
|
||||
if (success) {
|
||||
SendResponse(pending_reload_socket_, QStringLiteral("OK"));
|
||||
} else {
|
||||
SendResponse(pending_reload_socket_, QStringLiteral("ERR:") + error);
|
||||
}
|
||||
}
|
||||
pending_reload_socket_ = nullptr;
|
||||
}
|
||||
|
||||
void DevIpcServer::SendResponse(QLocalSocket* socket, const QString& response) {
|
||||
if (!socket || socket->state() != QLocalSocket::ConnectedState) {
|
||||
return;
|
||||
}
|
||||
socket->write((response + QStringLiteral("\n")).toUtf8());
|
||||
socket->flush();
|
||||
}
|
||||
|
||||
QString DevIpcServer::GetStatus() const {
|
||||
if (main_window_->IsEmulationRunning()) {
|
||||
return QStringLiteral("RUNNING %1")
|
||||
.arg(main_window_->GetGameTitleId(), 16, 16, QLatin1Char('0'));
|
||||
}
|
||||
return QStringLiteral("IDLE");
|
||||
}
|
||||
68
src/citra_qt/dev_ipc_server.h
Normal file
68
src/citra_qt/dev_ipc_server.h
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
class GMainWindow;
|
||||
|
||||
/// Developer IPC server for hot-reload and remote control via azahar-ctl.
|
||||
class DevIpcServer : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DevIpcServer(GMainWindow* main_window, QObject* parent = nullptr);
|
||||
~DevIpcServer() override;
|
||||
|
||||
bool Start();
|
||||
void Stop();
|
||||
|
||||
bool IsReloadInProgress() const {
|
||||
return reload_in_progress_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void HotReloadRequested(const QString& file_path, bool purge, bool wipe_saves);
|
||||
void ShutdownRequested();
|
||||
|
||||
public slots:
|
||||
void OnHotReloadComplete(bool success, const QString& error);
|
||||
|
||||
private slots:
|
||||
void OnNewConnection();
|
||||
void OnReadyRead();
|
||||
void OnDisconnected();
|
||||
|
||||
private:
|
||||
void HandleCommand(QLocalSocket* socket, const QString& command);
|
||||
void SendResponse(QLocalSocket* socket, const QString& response);
|
||||
QString GetStatus() const;
|
||||
|
||||
static QString ParseFlags(const QString& args, bool& purge, bool& wipe);
|
||||
|
||||
static constexpr int MAX_BUFFER_SIZE = 65536;
|
||||
static constexpr int MAX_CONNECTIONS = 16;
|
||||
|
||||
QLocalServer* server_ = nullptr;
|
||||
GMainWindow* main_window_ = nullptr;
|
||||
QHash<QLocalSocket*, QByteArray> read_buffers_;
|
||||
QPointer<QLocalSocket> pending_reload_socket_;
|
||||
bool reload_in_progress_ = false;
|
||||
|
||||
// In-flight reload params, committed to last_reload_* on success.
|
||||
QString pending_reload_path_;
|
||||
bool pending_reload_purge_ = false;
|
||||
bool pending_reload_wipe_ = false;
|
||||
|
||||
// Last successful reload params for HOT_RELOAD_LAST.
|
||||
QString last_reload_path_;
|
||||
bool last_reload_purge_ = false;
|
||||
bool last_reload_wipe_ = false;
|
||||
};
|
||||
@ -102,6 +102,8 @@ struct Values {
|
||||
Settings::Setting<bool> enable_discord_presence{true, "enable_discord_presence"};
|
||||
#endif
|
||||
|
||||
Settings::Setting<bool> enable_dev_ipc_server{false, "enable_dev_ipc_server"};
|
||||
|
||||
// Game List
|
||||
Settings::Setting<GameListIconSize> game_list_icon_size{GameListIconSize::LargeIcon,
|
||||
"iconSize"};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user