Merge pull request #70 from Jacoby6000/osrs-windows-support

Add launcher support for Runelite and HDOS on windows
This commit is contained in:
Adam
2024-10-02 14:11:22 +01:00
committed by GitHub
2 changed files with 334 additions and 7 deletions

View File

@@ -241,12 +241,17 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Tell git to ignore everything in the new build directory
file(GENERATE OUTPUT .gitignore CONTENT "*")
# define extra build and link args for windows non-msvc builds
# define extra build and link args for windows builds
if(WIN32)
list(APPEND BOLT_WINDOWS_NONMSVC_OPTIONS "-municode" "-mwin32")
list(APPEND BOLT_WINDOWS_NONMSVC_LINKOPTIONS ${BOLT_WINDOWS_NONMSVC_OPTIONS} "-s")
if(MSVC)
list(APPEND BOLT_WINDOWS_MSVC_OPTIONS "/utf-8")
else()
list(APPEND BOLT_WINDOWS_NONMSVC_OPTIONS "-municode" "-mwin32")
list(APPEND BOLT_WINDOWS_NONMSVC_LINKOPTIONS ${BOLT_WINDOWS_NONMSVC_OPTIONS} "-s")
endif()
endif()
# Build plugin library
if(NOT BOLT_SKIP_LIBRARIES)
add_subdirectory(src/library)
@@ -325,6 +330,7 @@ elseif(WIN32)
target_link_libraries(bolt PUBLIC "${CEF_ROOT}/${CMAKE_BUILD_TYPE}/libcef.lib")
target_link_libraries(bolt PUBLIC "${CEF_ROOT}/${CMAKE_BUILD_TYPE}/cef_sandbox.lib")
if(MSVC)
target_compile_options(bolt PUBLIC ${BOLT_WINDOWS_MSVC_OPTIONS})
set_property(TARGET bolt PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_compile_options(bolt PUBLIC ${BOLT_WINDOWS_NONMSVC_OPTIONS})

View File

@@ -3,11 +3,205 @@
#include <iostream>
#include <shellapi.h>
#include <fmt/core.h>
#include <fmt/xchar.h>
#if defined(BOLT_PLUGINS)
#include "../library/dll/stub_inject.h"
#endif
/**
* Attempts to find the java executable in the given `java_home` directory, or
* in the PATH environment variable if `java_home` is null. If the executable is
* found, the path to the executable is written to `out` and the function returns
* true. If the executable is not found, the function returns false.
*
* Javaw is used instead of java because it doesn't open a console window.
*
* @param java_home The path to the jdk/jre directory to search for the java executable.
* @param out A reference to a string that will be written to if the executable is found.
*
* @return True if the executable was found, false otherwise.
*/
bool FindJava(const wchar_t* java_home, std::wstring& out) {
if (java_home) {
std::filesystem::path java(java_home);
java.append("bin");
java.append("javaw.exe");
if (std::filesystem::exists(java)) {
out = java;
return true;
}
}
const char* path = getenv("PATH");
while (true) {
const char* next_semicolon = strchr(path, ';');
const bool is_last = next_semicolon == nullptr;
std::string_view path_item = is_last ? std::string_view(path) : std::string_view(path, (size_t)(next_semicolon - path));
std::filesystem::path java(path_item);
java.append("javaw.exe");
if (std::filesystem::exists(java)) {
out = java;
return true;
}
if (is_last) break;
path = next_semicolon + 1;
}
return false;
}
/**
* Creates a new environment block by adding the given additional environment
* variables to the current environment block.
*
* The caller is responsible for freeing the memory allocated for the new environment block.
*
* @param additional_env_vars A map of additional environment variables to add to the environment block.
*
* @return A pointer to a new environment block containing the additional environment variables. Null if the function fails.
*/
wchar_t* CreateEnvironmentString(const std::map<std::wstring, std::wstring>& additional_env_vars) {
wchar_t* currentEnv = GetEnvironmentStringsW();
if (currentEnv == nullptr) {
return nullptr;
}
std::vector<wchar_t> updatedEnvBlock;
wchar_t* envPtr = currentEnv;
while (*envPtr) {
size_t len = wcslen(envPtr) + 1;
updatedEnvBlock.insert(updatedEnvBlock.end(), envPtr, envPtr + len);
envPtr += len;
}
FreeEnvironmentStringsW(currentEnv);
for (const auto& [key, value] : additional_env_vars) {
std::wstring envVar = key + L"=" + value;
updatedEnvBlock.insert(updatedEnvBlock.end(), envVar.begin(), envVar.end());
updatedEnvBlock.push_back(L'\0');
}
// Append the final null terminator for the environment block. Environment blocks are double-null terminated.
updatedEnvBlock.push_back(L'\0');
wchar_t* resultEnv = new wchar_t[updatedEnvBlock.size()];
std::copy(updatedEnvBlock.begin(), updatedEnvBlock.end(), resultEnv);
return resultEnv;
}
/**
* Launches a java process using the java executable at `java` with the given
* jvm arguments, running the jar at `jar_path` with the given application
* arguments, and with the working directory set to `working_dir`.
*
* @param java The path to the jdk/jre java executable.
* @param jar_path The path to the jar file to be executed.
* @param working_dir The working directory for the new process.
* @param jvm_args The JVM arguments to be used. K/V pairs with nullopt values are treated as flags.
* @param application_args The arguments to be passed to the application. K/V pairs with nullopt values are treated as flags.
* @param env_vars The environment variables to be set for the new process
*
* @return An exit code for the new process. ULONG_MAX if the env vars could not be created.
*/
DWORD LaunchJavaProcess(
const std::wstring& java,
const std::wstring& jar_path,
const std::wstring& working_dir,
const std::map<std::wstring, std::optional<std::wstring>>& jvm_args,
const std::map<std::wstring, std::optional<std::wstring>>& application_args,
const std::map<std::wstring, std::wstring>& env_vars
) {
std::wstring jvm_args_string;
for (const auto& [key, maybe_value] : jvm_args) {
jvm_args_string += key;
if (maybe_value.has_value()) jvm_args_string += L"=" + maybe_value.value();
jvm_args_string += L" ";
}
std::wstring application_args_string;
for (const auto& [key, maybe_value] : application_args) {
application_args_string += key;
if(maybe_value.has_value()) application_args_string += L"=" + maybe_value.value();
application_args_string += L" ";
}
LPWSTR env_str(CreateEnvironmentString(env_vars));
if (env_str == nullptr) {
fmt::println(stderr, "Failed to create environment block for new process");
return ULONG_MAX;
}
const std::wstring command = std::format(L"\"{}\" {} -jar \"{}\" {}", java, jvm_args_string, jar_path, application_args_string);
fmt::print(L"Launching Java process with command: {}", command);
STARTUPINFOW si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(STARTUPINFOW);
DWORD creation_flags = CREATE_UNICODE_ENVIRONMENT;
// Copy the command, just in case CreateProcessW modifies it. This will help us log it later on.
std::wstring mutable_command = command;
bool create_process_result = CreateProcessW(
java.c_str(),
mutable_command.data(),
NULL,
NULL,
FALSE,
creation_flags,
(LPVOID) env_str,
working_dir.c_str(),
&si,
&pi
);
if (!create_process_result) {
DWORD err = GetLastError();
fmt::println(stderr, L"CreateProcess failed with error: {}\nCommand: {}\nWorking Directory: {}", err, command, working_dir);
delete[] env_str;
return err;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
std::wcout << L"Successfully spawned Java process with PID " << pi.dwProcessId << std::endl;
delete[] env_str;
return 0;
}
bool WriteStringToFile(const std::filesystem::path path, const std::wstring& data) {
std::wofstream file(path, std::ios::out | std::ios::binary);
if (file.fail()) {
return false;
}
file << data;
file.close();
return true;
}
bool WriteBytesToFile(const std::filesystem::path& path, const unsigned char* data, size_t size) {
std::ofstream file(path, std::ios::out | std::ios::binary);
if (file.fail()) {
return false;
}
file.write((const char*)data, size);
file.close();
return true;
}
CefRefPtr<CefResourceRequestHandler> Browser::Launcher::LaunchRs3Deb(CefRefPtr<CefRequest> request, std::string_view query) {
QSENDSTR("Elf binaries are not supported on this platform", 400);
}
@@ -172,12 +366,139 @@ CefRefPtr<CefResourceRequestHandler> Browser::Launcher::LaunchOsrsApp(CefRefPtr<
}
CefRefPtr<CefResourceRequestHandler> Browser::Launcher::LaunchRuneliteJar(CefRefPtr<CefRequest> request, std::string_view query, bool configure) {
QSENDSTR("JAR files not yet supported on Windows", 400);
const CefRefPtr<CefPostData> post_data = request->GetPostData();
const wchar_t* java_home = _wgetenv(L"JAVA_HOME");
std::wstring java;
if (!FindJava(java_home, java)) {
QSENDSTR("Couldn't find Java: JAVA_HOME does not point to a Java binary", 400);
}
std::wstring jar_path, id, jx_session_id, jx_character_id, jx_display_name;
bool
has_jar_path = false,
has_id = false,
has_jx_session_id = false,
has_jx_character_id = false,
has_jx_display_name = false;
this->ParseQuery(query, [&](const std::string_view& key, const std::string_view& val) {
PQSTRING(jar_path)
PQSTRING(id)
PQSTRING(jx_session_id)
PQSTRING(jx_character_id)
PQSTRING(jx_display_name)
});
// path to runelite.jar will either be a user-provided one or one in our data folder,
// which we may need to overwrite with a new user-provided file
std::filesystem::path rl_path;
if (has_jar_path) {
rl_path.assign(jar_path);
}
else {
rl_path = this->runelite_path;
if (has_id) {
QSENDBADREQUESTIF(post_data == nullptr || post_data->GetElementCount() != 1);
CefPostData::ElementVector vec;
post_data->GetElements(vec);
size_t jar_size = vec[0]->GetBytesCount();
std::unique_ptr<unsigned char[]> jar(new unsigned char[jar_size]);
vec[0]->GetBytes(jar_size, jar.get());
if (!WriteBytesToFile(rl_path, jar.get(), jar_size)) {
QSENDSTR("Failed to save JAR; if the game is already running, close it and try again", 500);
}
}
}
std::map<std::wstring, std::optional<std::wstring>> jvm_args, application_args;
std::map<std::wstring, std::wstring> env_vars;
if (has_jx_session_id) env_vars[L"JX_SESSION_ID"] = jx_session_id;
if (has_jx_character_id) env_vars[L"JX_CHARACTER_ID"] = jx_character_id;
if (has_jx_display_name) env_vars[L"JX_DISPLAY_NAME"] = jx_display_name;
if(configure) application_args[L"--configure"] = std::nullopt;
DWORD exit_code = LaunchJavaProcess(java, rl_path, this->data_dir, jvm_args, application_args, env_vars);
if (exit_code != 0) {
const std::string error_string = exit_code == ULONG_MAX ? "Failed to create environment block for new process" : "CreateProcess failed with error " + std::to_string(exit_code);
const std::string error_message = error_string + "\n";
return new ResourceHandler(error_message, 500, "text/plain");
}
// Save a file with the version number if it was provided
if (has_id && !WriteStringToFile(this->runelite_id_path, id)) {
QSENDSTR("OK, but unable to save version file", 200);
}
QSENDOK();
}
CefRefPtr<CefResourceRequestHandler> Browser::Launcher::LaunchHdosJar(CefRefPtr<CefRequest> request, std::string_view query) {
const CefRefPtr<CefPostData> post_data = request->GetPostData();
const wchar_t* java_home = _wgetenv(L"JAVA_HOME");
std::wstring java;
if (!FindJava(java_home, java)) {
QSENDSTR("Couldn't find Java: JAVA_HOME does not point to a Java binary", 400);
}
std::wstring version, jx_session_id, jx_character_id, jx_display_name;
bool
has_version = false,
has_jx_session_id = false,
has_jx_character_id = false,
has_jx_display_name = false;
this->ParseQuery(query, [&](const std::string_view& key, const std::string_view& val) {
PQSTRING(version)
PQSTRING(jx_session_id)
PQSTRING(jx_character_id)
PQSTRING(jx_display_name)
});
// If version is present in the query string, then we need to save the JAR file.
if (has_version) {
QSENDBADREQUESTIF(post_data == nullptr || post_data->GetElementCount() != 1);
CefPostData::ElementVector vec;
post_data->GetElements(vec);
size_t jar_size = vec[0]->GetBytesCount();
std::unique_ptr<unsigned char[]> jar(new unsigned char[jar_size]);
vec[0]->GetBytes(jar_size, jar.get());
if(!WriteBytesToFile(this->hdos_path, jar.get(), jar_size)) {
QSENDSTR("Failed to save JAR; if the game is already running, close it and try again", 500);
}
}
std::map<std::wstring, std::optional<std::wstring>> jvm_args, application_args;
std::map<std::wstring, std::wstring> env_vars;
if (has_jx_session_id) env_vars[L"JX_SESSION_ID"] = jx_session_id;
if (has_jx_character_id) env_vars[L"JX_CHARACTER_ID"] = jx_character_id;
if (has_jx_display_name) env_vars[L"JX_DISPLAY_NAME"] = jx_display_name;
DWORD exit_code = LaunchJavaProcess(java, this->hdos_path, this->data_dir, jvm_args, application_args, env_vars);
if (exit_code != 0) {
const std::string error_string = exit_code == ULONG_MAX ? "Failed to create environment block for new process" : "CreateProcess failed with error " + std::to_string(exit_code);
const std::string error_message = error_string + "\n";
return new ResourceHandler(error_message, 500, "text/plain");
}
// Save a file with the version number if it was provided
if (has_version && !WriteStringToFile(this->hdos_version_path, version)) {
QSENDSTR("OK, but unable to save version file", 200);
}
QSENDOK();
}
CefRefPtr<CefResourceRequestHandler> Browser::Launcher::LaunchHdosJar(CefRefPtr<CefRequest> request, std::string_view query) {
QSENDSTR("JAR files not yet supported on Windows", 400);
}
void Browser::Launcher::OpenExternalUrl(char* u) const {
const char* url = u;