#include "updater.h" #include "Engine/Core/Json/json.h" #include "Engine/Core/Log/logging.h" #include "Engine/Core/Platform/launcher_support.h" #include "launcher_update_config.h" #include "text_encoding.h" #include "Engine/Core/Platform/path_text.h" #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace dingosdk::launcher_update { namespace { constexpr std::uint64_t max_config_bytes = 256 * 1024; constexpr std::uint64_t max_download_bytes = 512ull * 1024 * 1024; struct Handle { HANDLE value{INVALID_HANDLE_VALUE}; ~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); } }; struct Internet { HINTERNET value{}; ~Internet() { if (value) WinHttpCloseHandle(value); } }; using launcher_text::utf8; std::wstring widen(std::string_view value) { if (value.empty()) return {}; const auto length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0); if (length <= 0) throw std::runtime_error("Launcher config contains invalid UTF-8"); std::wstring output(static_cast(length), L'\0'); MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), output.data(), length); return output; } [[noreturn]] void fail(const std::string& message) { throw std::runtime_error(message); } void info(std::string_view message) { logging::write(logging::Level::info, logging::Channel::launcher, message); } std::wstring quote_argument(std::wstring_view value) { if (!value.empty() && value.find_first_of(L" \t\n\v\"") == std::wstring_view::npos) return std::wstring(value); std::wstring output(1, L'\"'); std::size_t slashes{}; for (const auto character : value) { if (character == L'\\') { ++slashes; } else if (character == L'\"') { output.append(slashes * 2 + 1, L'\\'); output.push_back(character); slashes = 0; } else { output.append(slashes, L'\\'); slashes = 0; output.push_back(character); } } output.append(slashes * 2, L'\\'); output.push_back(L'\"'); return output; } // Streams an HTTPS GET into `sink`, following up to five HTTPS redirects. void http_get(const std::wstring& url, std::uint64_t limit, int timeout_ms, const std::function& sink) { const auto win32 = [](std::string_view action) { fail(std::format("{} failed (Windows error {})", action, GetLastError())); }; Internet session{WinHttpOpen(L"ReSkate-Launcher/1", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0)}; if (!session.value) win32("WinHttpOpen"); if (!WinHttpSetTimeouts(session.value, timeout_ms, timeout_ms, timeout_ms, timeout_ms)) win32("WinHttpSetTimeouts"); std::wstring current = url; for (int hop = 0; hop < 6; ++hop) { if (current.empty() || current.size() >= 4096 || current.find_first_of(L"\r\n\t #") != std::wstring::npos) fail("Download URL is invalid"); URL_COMPONENTS parts{}; parts.dwStructSize = sizeof(parts); parts.dwHostNameLength = parts.dwUrlPathLength = parts.dwExtraInfoLength = parts.dwUserNameLength = parts.dwPasswordLength = static_cast(-1); if (!WinHttpCrackUrl(current.c_str(), static_cast(current.size()), 0, &parts) || parts.nScheme != INTERNET_SCHEME_HTTPS || !parts.dwHostNameLength || parts.dwUserNameLength || parts.dwPasswordLength) fail("Download URL must be HTTPS: " + utf8(current)); const std::wstring host(parts.lpszHostName, parts.dwHostNameLength); std::wstring resource = parts.dwUrlPathLength ? std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) : L"/"; if (parts.dwExtraInfoLength) resource.append(parts.lpszExtraInfo, parts.dwExtraInfoLength); Internet connection{WinHttpConnect(session.value, host.c_str(), parts.nPort, 0)}; if (!connection.value) win32("WinHttpConnect"); Internet request{WinHttpOpenRequest(connection.value, L"GET", resource.c_str(), nullptr, nullptr, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)}; if (!request.value) win32("WinHttpOpenRequest"); DWORD redirects = WINHTTP_DISABLE_REDIRECTS; DWORD logon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH; if (!WinHttpSetOption(request.value, WINHTTP_OPTION_DISABLE_FEATURE, &redirects, sizeof(redirects)) || !WinHttpSetOption(request.value, WINHTTP_OPTION_AUTOLOGON_POLICY, &logon, sizeof(logon))) win32("WinHttpSetOption"); std::wstring headers = L"Cache-Control: no-cache\r\n"; if (host == L"api.github.com") headers += L"X-GitHub-Api-Version: 2022-11-28\r\nAccept: application/vnd.github+json\r\n"; if (!WinHttpSendRequest(request.value, headers.c_str(), static_cast(-1L), nullptr, 0, 0, 0) || !WinHttpReceiveResponse(request.value, nullptr)) win32("HTTPS request"); DWORD status{}, size = sizeof(status); if (!WinHttpQueryHeaders(request.value, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, nullptr, &status, &size, nullptr)) win32("WinHttpQueryHeaders"); if (status == 301 || status == 302 || status == 303 || status == 307 || status == 308) { DWORD bytes{}; WinHttpQueryHeaders(request.value, WINHTTP_QUERY_LOCATION, WINHTTP_HEADER_NAME_BY_INDEX, nullptr, &bytes, nullptr); std::wstring location(bytes / sizeof(wchar_t), L'\0'); if (!bytes || !WinHttpQueryHeaders(request.value, WINHTTP_QUERY_LOCATION, WINHTTP_HEADER_NAME_BY_INDEX, location.data(), &bytes, nullptr)) fail(std::format("{} redirected without a location", utf8(current))); location.resize(bytes / sizeof(wchar_t)); current = std::move(location); continue; } // Unauthenticated, each player may ask GitHub's API 60 times an hour. if (host == L"api.github.com" && (status == 403 || status == 429)) fail(std::format("GitHub is limiting release checks from this network (HTTP {}); try again later", status)); if (status != 200) fail(std::format("{} returned HTTP {}", utf8(current), status)); std::array buffer{}; std::uint64_t total{}; for (;;) { DWORD read{}; if (!WinHttpReadData(request.value, buffer.data(), static_cast(buffer.size()), &read)) win32("WinHttpReadData"); if (!read) break; if (total + read > limit) fail("Download is larger than expected: " + utf8(current)); sink(buffer.data(), read); total += read; } return; } fail("Too many redirects: " + utf8(url)); } bool is_sha256(std::string_view value) { return value.size() == 64 && value.find_first_not_of("0123456789abcdef") == std::string_view::npos; } bool is_decimal(std::string_view value) { return !value.empty() && value.size() <= 20 && value.find_first_not_of("0123456789") == std::string_view::npos; } RemoteFile parse_file(const Json& root, std::string_view key, bool required) { RemoteFile file; if (!root.contains(key) || root.at(key).is_null()) { if (required) fail(std::format("Launcher config is missing \"{}\"", key)); return file; } const auto& section = root.at(key); file.version = section.value("version", ""); file.url = widen(section.at("url").string()); file.sha256 = section.at("sha256").string(); file.size = section.at("size").get(); if (!file.url.starts_with(L"https://") && !file.url.starts_with(L"asset:")) fail(std::format("Launcher config \"{}.url\" must be HTTPS or asset:", key)); if (!is_sha256(file.sha256)) fail(std::format("Launcher config \"{}.sha256\" must be 64 lowercase hex digits", key)); if (!file.size || file.size > max_download_bytes) fail(std::format("Launcher config \"{}.size\" is out of range", key)); return file; } fs::path local_app_data() { std::array local{}; const auto length = GetEnvironmentVariableW(L"LOCALAPPDATA", local.data(), static_cast(local.size())); if (!length || length >= local.size()) fail("LOCALAPPDATA is unavailable"); return fs::path(local.data()); } struct Zip { std::vector bytes; mz_zip_archive value{}; explicit Zip(const fs::path& archive) { const auto length = fs::file_size(archive); std::ifstream input(archive, std::ios::binary); bytes.resize(static_cast(length)); if (!input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) fail("Cannot read " + utf8(archive.filename().wstring())); if (!mz_zip_reader_init_mem(&value, bytes.data(), bytes.size(), 0)) fail(utf8(archive.filename().wstring()) + " is not a ZIP"); } ~Zip() { mz_zip_reader_end(&value); } Zip(const Zip&) = delete; Zip& operator=(const Zip&) = delete; }; // Writes one ZIP entry to `output`. void extract_index(Zip& zip, mz_uint index, const mz_zip_archive_file_stat& entry, const fs::path& output) { Handle file{CreateFileW(output.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; if (file.value == INVALID_HANDLE_VALUE) fail("Cannot create " + path_utf8(output)); struct Sink { HANDLE file; std::uint64_t count{}; } sink{file.value}; const auto write = [](void* opaque, mz_uint64 offset, const void* buffer, size_t count) -> size_t { auto& target = *static_cast(opaque); DWORD written{}; if (offset != target.count || !WriteFile(target.file, buffer, static_cast(count), &written, nullptr) || written != count) return 0; target.count += count; return count; }; if (!mz_zip_reader_extract_to_callback(&zip.value, index, write, &sink, 0) || sink.count != entry.m_uncomp_size || !FlushFileBuffers(file.value)) fail(std::format("Cannot extract {}", entry.m_filename)); } // Writes exactly one named ZIP entry to `output`; entry names never become paths. void extract_entry(const fs::path& archive, const char* name, const fs::path& output) { Zip zip(archive); const int index = mz_zip_reader_locate_file(&zip.value, name, nullptr, 0); mz_zip_archive_file_stat entry{}; if (index < 0 || !mz_zip_reader_file_stat(&zip.value, static_cast(index), &entry) || entry.m_is_directory || entry.m_is_encrypted) fail(std::format("DepotDownloader archive has no {}", name)); extract_index(zip, static_cast(index), entry, output); } // A ZIP entry name as a path inside the install folder, or empty when it // could escape it (absolute, drive-qualified, or with . or .. parts). fs::path safe_entry_path(std::string_view name) { if (name.empty() || name.size() > 260 || name.front() == '/' || name.front() == '\\' || name.find(':') != std::string_view::npos) return {}; fs::path result; std::size_t start{}; for (;;) { const auto end = name.find_first_of("/\\", start); const auto part = name.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start); if (part.empty() || part == "." || part == "..") return {}; result /= fs::path(widen(part)); if (end == std::string_view::npos) return result; start = end + 1; } } } // namespace void http_stream(const std::wstring& url, std::uint64_t limit, int timeout_ms, const std::function& sink) { http_get(url, limit, timeout_ms, sink); } bool binary_updates_enabled() noexcept { return launcher_binary_updates; } Config parse_config(std::string_view text) { const auto root = Json::parse(text, JsonLimits{max_config_bytes, 8, 4096}); if (!root.is_object()) fail("Launcher config must be a JSON object"); const auto schema = root.value("schema", 0); if (schema != 1) fail(std::format("Launcher config schema {} is not supported; update the launcher manually", schema)); Config config; config.launcher = parse_file(root, "launcher", false); config.runtime = parse_file(root, "runtime", false); config.depot_downloader = parse_file(root, "depot_downloader", true); config.server = parse_file(root, "server", false); if (!config.server.url.empty()) { config.server_exe_sha256 = root.at("server").at("exe_sha256").string(); if (!is_sha256(config.server_exe_sha256)) fail("Launcher config \"server.exe_sha256\" must be 64 lowercase hex digits"); } const auto& game = root.at("game"); config.game.app_id = game.at("app_id").get(); config.game.depot_id = game.at("depot_id").get(); config.game.manifest_id = game.at("manifest_id").string(); config.game.build_id = game.value("build_id", ""); config.game.skate_sha256 = game.at("skate_sha256").string(); if (!config.game.app_id || !config.game.depot_id || !is_decimal(config.game.manifest_id)) fail("Launcher config \"game\" needs app_id, depot_id and a decimal manifest_id string"); if (!is_sha256(config.game.skate_sha256)) fail("Launcher config \"game.skate_sha256\" must be 64 lowercase hex digits"); return config; } std::optional fetch_config() { std::array local{}; const auto local_length = GetEnvironmentVariableW(L"RESKATE_LAUNCHER_CONFIG_FILE", local.data(), static_cast(local.size())); if (local_length && local_length < local.size()) { // Test an unpublished config before attaching it to a release. const fs::path path(std::wstring(local.data(), local_length)); try { std::ifstream input(path, std::ios::binary); const std::string text{std::istreambuf_iterator(input), std::istreambuf_iterator()}; if (!input && !input.eof()) fail("Cannot read " + utf8(path.wstring())); logging::write(logging::Level::info, logging::Channel::launcher, L"Using local launcher config: " + path.wstring()); return parse_config(text); } catch (const std::exception& exception) { logging::log(logging::Level::warning, logging::Channel::launcher, "Local launcher config rejected: {}", exception.what()); return std::nullopt; } } try { // Public releases: the newest release carries launcher.json and the files it pins. Only // this lookup uses GitHub's rate-limited API; the files come from the release's download links. const auto latest = std::wstring(L"https://api.github.com/repos/") + launcher_release_repo + L"/releases/latest"; logging::write(logging::Level::info, logging::Channel::launcher, L"Checking GitHub releases: " + std::wstring(launcher_release_repo)); std::string text; http_get(latest, 4 * 1024 * 1024, 8000, [&](const char* data, DWORD count) { text.append(data, count); }); const auto release = Json::parse(text); std::map> assets; if (release.contains("assets")) for (const auto& asset : release.at("assets")) assets[asset.value("name", "")] = widen(asset.value("browser_download_url", "")); const auto manifest = assets.find("launcher.json"); if (manifest == assets.end()) fail("The latest release has no launcher.json asset"); std::string json; http_get(manifest->second, max_config_bytes, 8000, [&](const char* data, DWORD count) { json.append(data, count); }); auto config = parse_config(json); for (auto* file : {&config.launcher, &config.runtime, &config.depot_downloader, &config.server}) { if (!file->url.starts_with(L"asset:")) continue; const auto found = assets.find(utf8(std::wstring_view(file->url).substr(6))); if (found == assets.end()) fail("The latest release is missing asset " + utf8(file->url.substr(6))); file->url = found->second; } logging::log(logging::Level::info, logging::Channel::launcher, "Release {}: launcher {}, runtime {}, game depot {} manifest {}.", release.value("tag_name", "?"), config.launcher.version.empty() ? "-" : config.launcher.version, config.runtime.version.empty() ? "-" : config.runtime.version, config.game.depot_id, config.game.manifest_id); return config; } catch (const std::exception& exception) { logging::log(logging::Level::warning, logging::Channel::launcher, "Release check unavailable ({}); continuing with local files.", exception.what()); return std::nullopt; } } bool file_matches(const fs::path& path, const RemoteFile& file) { std::error_code error; return fs::is_regular_file(path, error) && fs::file_size(path, error) == file.size && !error && launcher::sha256_file(path) == file.sha256; } fs::path download_verified(const fs::path& target, const RemoteFile& file, const Progress& progress) { const fs::path temporary = target.wstring() + L".new"; DeleteFileW(temporary.c_str()); logging::write(logging::Level::info, logging::Channel::launcher, L"Downloading " + target.filename().wstring() + L" " + widen(file.version) + L" from " + file.url); { Handle output{CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; if (output.value == INVALID_HANDLE_VALUE) fail(std::format("Cannot write {} (Windows error {})", utf8(temporary.wstring()), GetLastError())); std::uint64_t received{}; if (progress) progress(0, file.size); http_get(file.url, file.size, 30000, [&](const char* data, DWORD count) { received += count; if (progress) progress(received, file.size); DWORD written{}; if (!WriteFile(output.value, data, count, &written, nullptr) || written != count) fail("Cannot write " + utf8(temporary.wstring())); }); if (!FlushFileBuffers(output.value)) fail("Cannot flush " + utf8(temporary.wstring())); } if (!file_matches(temporary, file)) { DeleteFileW(temporary.c_str()); fail("Downloaded " + utf8(target.filename().wstring()) + " does not match the SHA-256 in the launcher config"); } return temporary; } void replace_file(const fs::path& target, const RemoteFile& file, const Progress& progress) { const auto verified = download_verified(target, file, progress); if (!MoveFileExW(verified.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { const auto error = GetLastError(); DeleteFileW(verified.c_str()); fail(std::format("Cannot replace {} (Windows error {}); is Skate still running?", utf8(target.filename().wstring()), error)); } } void replace_running_launcher(const fs::path& self, const RemoteFile& file, const Progress& progress) { const auto verified = download_verified(self, file, progress); const fs::path previous = self.wstring() + L".old"; DeleteFileW(previous.c_str()); // Windows allows renaming, but not overwriting, a running executable. if (!MoveFileExW(self.c_str(), previous.c_str(), MOVEFILE_WRITE_THROUGH)) { const auto error = GetLastError(); DeleteFileW(verified.c_str()); fail(std::format("Cannot move the running launcher aside (Windows error {})", error)); } if (!MoveFileExW(verified.c_str(), self.c_str(), MOVEFILE_WRITE_THROUGH)) { const auto error = GetLastError(); MoveFileExW(previous.c_str(), self.c_str(), MOVEFILE_WRITE_THROUGH); fail(std::format("Cannot install the launcher update (Windows error {})", error)); } } void remove_previous_launcher(const fs::path& self) noexcept { const std::wstring previous = self.wstring() + L".old"; // The previous process may still be exiting; a later launch retries. for (int attempt = 0; attempt < 20; ++attempt) { if (DeleteFileW(previous.c_str()) || GetLastError() == ERROR_FILE_NOT_FOUND) return; Sleep(100); } } std::vector install_archive(const fs::path& archive, const fs::path& directory) { Zip zip(archive); struct Staged { fs::path target, staged; std::string name; }; std::vector files; try { const auto count = mz_zip_reader_get_num_files(&zip.value); for (mz_uint index = 0; index < count; ++index) { mz_zip_archive_file_stat entry{}; if (!mz_zip_reader_file_stat(&zip.value, index, &entry)) fail("Cannot read the update archive"); const std::string_view name = entry.m_filename; if (entry.m_is_directory || name.ends_with('/') || name.ends_with('\\')) continue; if (entry.m_is_encrypted) fail(std::format("{} in the update is encrypted", entry.m_filename)); const auto relative = safe_entry_path(entry.m_filename); if (relative.empty()) fail(std::format("The update contains an unsafe path: {}", entry.m_filename)); const auto target = directory / relative; fs::create_directories(target.parent_path()); files.push_back({target, target.wstring() + L".update-new", entry.m_filename}); extract_index(zip, index, entry, files.back().staged); } } catch (...) { for (const auto& file : files) DeleteFileW(file.staged.c_str()); throw; } std::vector installed; for (std::size_t i = 0; i < files.size(); ++i) { const auto& file = files[i]; if (!MoveFileExW(file.staged.c_str(), file.target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { // Windows renames, but won't overwrite, a running exe or a loaded DLL. const fs::path aside = file.target.wstring() + L".update-old"; DeleteFileW(aside.c_str()); const bool moved_aside = MoveFileExW(file.target.c_str(), aside.c_str(), MOVEFILE_WRITE_THROUGH) != FALSE; if (!moved_aside || !MoveFileExW(file.staged.c_str(), file.target.c_str(), MOVEFILE_WRITE_THROUGH)) { const auto error = GetLastError(); // Put the current file back so a failed update never leaves it missing. if (moved_aside) MoveFileExW(aside.c_str(), file.target.c_str(), MOVEFILE_WRITE_THROUGH); for (std::size_t rest = i; rest < files.size(); ++rest) DeleteFileW(files[rest].staged.c_str()); fail(std::format("Cannot replace {} (Windows error {})", file.name, error)); } } installed.push_back(file.name); } return installed; } std::string archive_entry_sha256(const fs::path& archive, const char* name) { Zip zip(archive); const int index = mz_zip_reader_locate_file(&zip.value, name, nullptr, 0); mz_zip_archive_file_stat entry{}; if (index < 0 || !mz_zip_reader_file_stat(&zip.value, static_cast(index), &entry) || entry.m_is_directory || entry.m_is_encrypted) return {}; const fs::path temporary = archive.wstring() + L".check"; struct Remove { const fs::path& path; ~Remove() { DeleteFileW(path.c_str()); } } remove{temporary}; extract_index(zip, static_cast(index), entry, temporary); return launcher::sha256_file(temporary); } void remove_replaced_files(const fs::path& directory) noexcept { std::error_code error; for (fs::recursive_directory_iterator it(directory, fs::directory_options::skip_permission_denied, error), end; !error && it != end; it.increment(error)) { if (it->is_regular_file(error) && it->path().extension() == L".update-old") DeleteFileW(it->path().c_str()); } } fs::path ensure_depot_downloader(const RemoteFile& file, const Progress& progress) { const auto directory = local_app_data() / L"ReSkate" / L"tools" / L"DepotDownloader" / widen(file.sha256.substr(0, 16)); const auto executable = directory / L"DepotDownloader.exe"; std::error_code error; if (fs::is_regular_file(executable, error)) return executable; fs::create_directories(directory); const auto archive = directory / L"DepotDownloader.zip"; const auto verified = download_verified(archive, file, progress); const fs::path unpacked = executable.wstring() + L".tmp"; try { extract_entry(verified, "DepotDownloader.exe", unpacked); } catch (...) { DeleteFileW(verified.c_str()); DeleteFileW(unpacked.c_str()); throw; } DeleteFileW(verified.c_str()); if (!MoveFileExW(unpacked.c_str(), executable.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) fail(std::format("Cannot install DepotDownloader (Windows error {})", GetLastError())); logging::write(logging::Level::info, logging::Channel::launcher, L"DepotDownloader ready: " + executable.wstring()); return executable; } DWORD run_depot_downloader(const fs::path& depot_downloader, const GameBuild& game, const fs::path& directory, bool validate, const SteamLogin& login, const std::function& on_line, const PromptHandler& on_prompt, const std::atomic& cancel) { std::wstring command = quote_argument(depot_downloader.wstring()) + std::format(L" -app {} -depot {} -manifest {} -dir ", game.app_id, game.depot_id, widen(game.manifest_id)) + quote_argument(directory.wstring()) + L" -max-downloads 16"; std::wstring user, options; if (login.username.empty()) options += L" -qr"; else { user = L" -username " + quote_argument(widen(login.username)); if (login.remember) options += L" -remember-password"; if (login.prefer_code) options += L" -no-mobile"; } if (validate) options += L" -validate"; // The log is attached to crash reports, so it leaves out the Steam login name. logging::write(logging::Level::info, logging::Channel::launcher, L"Running DepotDownloader: " + command + (user.empty() ? L"" : L" -username ") + options); command += user + options; SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE}; Handle read_pipe, write_pipe; if (!CreatePipe(&read_pipe.value, &write_pipe.value, &inherit, 0) || !SetHandleInformation(read_pipe.value, HANDLE_FLAG_INHERIT, 0)) fail("Cannot create the DepotDownloader pipe"); Handle input_read, input_write; if (!CreatePipe(&input_read.value, &input_write.value, &inherit, 0) || !SetHandleInformation(input_write.value, HANDLE_FLAG_INHERIT, 0)) fail("Cannot create the DepotDownloader pipe"); STARTUPINFOW startup{sizeof(startup)}; startup.dwFlags = STARTF_USESTDHANDLES; startup.hStdInput = input_read.value; startup.hStdOutput = startup.hStdError = write_pipe.value; std::vector buffer(command.begin(), command.end()); buffer.push_back(L'\0'); PROCESS_INFORMATION process{}; // The job ends DepotDownloader with the launcher, even if the launcher is killed. Handle job{CreateJobObjectW(nullptr, nullptr)}; JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; if (!job.value || !SetInformationJobObject(job.value, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) fail("Cannot create the DepotDownloader job"); if (!CreateProcessW(depot_downloader.c_str(), buffer.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, depot_downloader.parent_path().c_str(), &startup, &process)) fail(std::format("Cannot start DepotDownloader (Windows error {})", GetLastError())); Handle thread{process.hThread}; Handle child{process.hProcess}; if (!AssignProcessToJobObject(job.value, child.value)) { TerminateProcess(child.value, ERROR_CANCELLED); fail(std::format("Cannot start DepotDownloader (Windows error {})", GetLastError())); } ResumeThread(thread.value); CloseHandle(write_pipe.value); write_pipe.value = INVALID_HANDLE_VALUE; CloseHandle(input_read.value); input_read.value = INVALID_HANDLE_VALUE; std::string pending; bool rejected{}; // "\r\n" is one break even when a read splits it; an extra empty line would // end DepotOutput's QR code after its first row. bool after_cr{}; const auto emit = [&](bool flush) { for (;;) { const auto end = pending.find_first_of("\r\n"); if (end == std::string::npos) break; if (end == 0 && pending[0] == '\n' && after_cr) { after_cr = false; pending.erase(0, 1); continue; } after_cr = pending[end] == '\r'; const auto line = std::string_view(pending).substr(0, end); if (line.find("code you have provided is incorrect") != std::string_view::npos) rejected = true; on_line(line); pending.erase(0, end + 1); } if (flush && !pending.empty()) { on_line(pending); pending.clear(); } }; bool cancelled{}; // Prompts end in ": " without a newline, so look at the unfinished line. const auto answer_prompt = [&] { if (pending.size() < 2 || !pending.ends_with(": ")) return; Prompt prompt; if (pending.find("Enter account password") != std::string::npos) prompt.kind = PromptKind::password; else if (pending.find("authenticator app") != std::string::npos) prompt.kind = PromptKind::authenticator_code; else if (pending.find("auth code sent to the email") != std::string::npos) prompt.kind = PromptKind::email_code; else return; prompt.text = pending.substr(0, pending.size() - 2); prompt.retry = std::exchange(rejected, false); logging::write(logging::Level::info, logging::Channel::launcher, "DepotDownloader asks: " + prompt.text); pending.clear(); auto answer = on_prompt(prompt); if (!answer || cancel) { cancelled = true; TerminateProcess(child.value, ERROR_CANCELLED); return; } answer->push_back('\n'); DWORD written{}; WriteFile(input_write.value, answer->data(), static_cast(answer->size()), &written, nullptr); SecureZeroMemory(answer->data(), answer->size()); }; std::array chunk{}; for (;;) { DWORD available{}; const bool peeked = PeekNamedPipe(read_pipe.value, nullptr, 0, nullptr, &available, nullptr); if (peeked && available) { DWORD read{}; if (!ReadFile(read_pipe.value, chunk.data(), static_cast(chunk.size()), &read, nullptr) || !read) break; pending.append(chunk.data(), read); emit(false); answer_prompt(); continue; } // A broken pipe means every writer has exited and the output is drained. if (!peeked) break; if (cancel && !cancelled) { cancelled = true; TerminateProcess(child.value, ERROR_CANCELLED); } WaitForSingleObject(child.value, 50); } emit(true); WaitForSingleObject(child.value, INFINITE); DWORD exit_code{}; GetExitCodeProcess(child.value, &exit_code); logging::log(logging::Level::info, logging::Channel::launcher, "DepotDownloader exited with code {}{}", exit_code, cancelled ? " (cancelled)" : ""); return exit_code; } } // namespace dingosdk::launcher_update