mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-16 01:40:46 +08:00
2153 lines
73 KiB
C++
2153 lines
73 KiB
C++
// MAA.Updater.exe
|
|
// Applies a pending MAA update package after the main MAA process exits.
|
|
// Invoked by MaaWpfGui's PendingUpdateApplier; do NOT run manually.
|
|
//
|
|
// Usage:
|
|
// MAA.Updater.exe <ParentProcessId> <RootDir> <ExtractDir> <BackupDir>
|
|
// <PackagePath> <SuccessStatusFile> <FailureStatusFile>
|
|
// <RelaunchExecutablePath> <PlanFile>
|
|
// [--mutex-name <name>] [--show-console] [--no-progress-ui]
|
|
//
|
|
// Plan file format (UTF-8 JSON):
|
|
// { "packageType": "full|ota", "removeList": ["rel/path", ...], "moveList": ["rel/path", ...] }
|
|
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include <dwmapi.h>
|
|
#include <shellapi.h>
|
|
|
|
#include <cstdarg>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct PendingUpdatePlan;
|
|
struct UpdateProgressUi;
|
|
|
|
static bool TryConvertWideToUtf8(const std::wstring& wide, std::string& utf8);
|
|
static HANDLE GetConsoleStreamHandle(FILE* stream);
|
|
static bool IsSystemDarkModeEnabled();
|
|
static bool ShouldShowProgressUi();
|
|
static bool InitializeProgressUi();
|
|
static void DestroyProgressUi();
|
|
static void PumpProgressUiMessages();
|
|
static void RefreshProgressUiTheme();
|
|
static std::wstring BuildProgressCountText(int processedFileCount, int totalFileCount);
|
|
static void RefreshProgressUiCountText();
|
|
static void SetProgressUiTotalFileCount(int totalFileCount);
|
|
static void SetProgressUiStatus(const std::wstring& status, const std::wstring& detail);
|
|
static void AdvanceProgressUi(const std::wstring& status, const std::wstring& detail);
|
|
static void CompleteProgressUi(const std::wstring& status, const std::wstring& detail);
|
|
static void ShowProgressUiFailure(const std::wstring& failureReason);
|
|
static void WriteLog(const wchar_t* message);
|
|
static void WriteLogF(const wchar_t* fmt, ...);
|
|
static void WriteLogEntries(const std::wstring& title, const std::vector<std::wstring>& entries);
|
|
static void WriteConsoleText(FILE* stream, const std::wstring& text, bool appendNewline);
|
|
static void RotateLogIfNeeded();
|
|
static bool HasArgument(int argc, wchar_t* argv[], const wchar_t* argument);
|
|
|
|
static std::wstring EnsureTrailingSeparator(const std::wstring& path);
|
|
static std::wstring NormalizeRelativePath(const std::wstring& relativePath);
|
|
static bool EqualsIgnoreCase(const std::wstring& left, const wchar_t* right);
|
|
static bool IsDriveRootDirectory(const std::wstring& path);
|
|
static bool IsRecycleAndReplaceDirectory(const std::wstring& relativePath);
|
|
static bool TryResolvePathUnderRoot(
|
|
const std::wstring& rootPath,
|
|
const std::wstring& relativePath,
|
|
std::wstring& out);
|
|
|
|
static bool PathExistsW(const std::wstring& path);
|
|
static bool IsDirectory(const std::wstring& path);
|
|
static bool CopyDirectoryRecursive(const std::wstring& sourceDir, const std::wstring& destinationDir);
|
|
static bool MovePathToRecycleBin(const std::wstring& path);
|
|
static bool CopyPathEntry(const std::wstring& sourcePath, const std::wstring& destinationPath);
|
|
static void EnsureParentDirectory(const std::wstring& path);
|
|
static std::wstring CreateArchivedPath(const std::wstring& base);
|
|
static bool MovePathEntry(const std::wstring& src, const std::wstring& dst);
|
|
static void PrepareBackupDestination(const std::wstring& backupPath);
|
|
static bool MoveExistingPathToBackup(const std::wstring& src, const std::wstring& backup);
|
|
static bool RecycleAndBackupDirectory(const std::wstring& sourcePath, const std::wstring& backupPath);
|
|
static bool RecycleAndBackupPath(const std::wstring& sourcePath, const std::wstring& backupPath);
|
|
|
|
static bool WriteUtf8File(const std::wstring& path, const char* content);
|
|
static bool WriteUtf8File(const std::wstring& path, const std::string& content);
|
|
|
|
static std::wstring BuildFileIoFailureReason(const wchar_t* action, const std::wstring& path, DWORD errorCode);
|
|
static bool TryReadUtf8File(const std::wstring& path, std::string& content, std::wstring& failureReason);
|
|
static std::wstring Utf8ToWide(const std::string& s);
|
|
static std::string ParseJsonString(const std::string& json, size_t& pos);
|
|
static size_t SkipJsonWhitespace(const std::string& json, size_t pos);
|
|
static size_t SkipJsonValue(const std::string& json, size_t pos);
|
|
static size_t FindTopLevelJsonValueStartByKey(const std::string& json, const char* key);
|
|
static std::vector<std::wstring> ParseJsonStringArray(const std::string& json, const char* key);
|
|
static std::wstring ParseJsonStringProperty(const std::string& json, const char* key);
|
|
static bool LoadPendingUpdatePlan(
|
|
const std::wstring& planFile,
|
|
PendingUpdatePlan& outPlan,
|
|
std::wstring& failureReason,
|
|
std::string* rawJson = nullptr);
|
|
static void PrintPlanEntries(const std::wstring& title, const std::vector<std::wstring>& entries);
|
|
static int RunPlanParserTest(const std::wstring& initialPlanFile);
|
|
static HANDLE AcquireUpdateMutex(const std::wstring& mutexName);
|
|
static void ReleaseUpdateMutex(HANDLE hMutex);
|
|
|
|
// Retry and force-delete helpers
|
|
static bool RetryFileOp(const std::function<bool()>& op, int maxAttempts = 5, DWORD initialDelayMs = 200);
|
|
static std::wstring RenameLockedFile(const std::wstring& path);
|
|
static bool ForceDeleteFile(const std::wstring& path);
|
|
static bool ForceRemoveDirectoryRecursive(const std::wstring& dir);
|
|
static bool InstallFileAtomic(const std::wstring& sourcePath, const std::wstring& targetPath);
|
|
static void CleanupPendingDeleteFiles(const std::wstring& rootDir);
|
|
|
|
struct UpdateProgressUi
|
|
{
|
|
bool enabled = false;
|
|
HWND window = nullptr;
|
|
HWND statusLabel = nullptr;
|
|
HWND detailLabel = nullptr;
|
|
HWND countLabel = nullptr;
|
|
HWND progressBar = nullptr;
|
|
int processedFileCount = 0;
|
|
int totalFileCount = 0;
|
|
};
|
|
|
|
struct ProgressUiTheme
|
|
{
|
|
bool isDarkMode = false;
|
|
COLORREF backgroundColor = RGB(255, 255, 255);
|
|
COLORREF primaryTextColor = RGB(32, 32, 32);
|
|
COLORREF secondaryTextColor = RGB(96, 96, 96);
|
|
COLORREF progressTrackColor = RGB(232, 234, 237);
|
|
COLORREF progressBarColor = RGB(0, 120, 212);
|
|
HBRUSH backgroundBrush = nullptr;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Logging
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::wstring g_logFile;
|
|
static bool g_writeConsoleLog = false;
|
|
// 用户可通过 --no-progress-ui 显式关闭更新进度窗口
|
|
static bool g_suppressProgressUi = false;
|
|
static UpdateProgressUi g_progressUi;
|
|
static ProgressUiTheme g_progressUiTheme;
|
|
|
|
static constexpr wchar_t PROGRESS_WINDOW_CLASS_NAME[] = L"MaaUpdaterProgressWindow";
|
|
static constexpr int PROGRESS_WINDOW_WIDTH = 540;
|
|
static constexpr int PROGRESS_WINDOW_HEIGHT = 190;
|
|
static constexpr int PROGRESS_STATUS_CONTROL_ID = 1001;
|
|
static constexpr int PROGRESS_DETAIL_CONTROL_ID = 1002;
|
|
static constexpr int PROGRESS_COUNT_CONTROL_ID = 1003;
|
|
static constexpr int PROGRESS_BAR_CONTROL_ID = 1004;
|
|
static constexpr DWORD LEGACY_DWMWA_USE_IMMERSIVE_DARK_MODE = 19;
|
|
static constexpr wchar_t MUTEX_NAME_ARG[] = L"--mutex-name";
|
|
static constexpr DWORD UPDATE_MUTEX_TIMEOUT_MS = 3000;
|
|
#define PENDING_DELETE_SUFFIX L".pendingdelete"
|
|
static constexpr int FILE_OP_MAX_RETRIES = 5;
|
|
static constexpr DWORD FILE_OP_INITIAL_DELAY_MS = 200;
|
|
|
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
|
#endif
|
|
|
|
static bool TryConvertWideToUtf8(const std::wstring& wide, std::string& utf8)
|
|
{
|
|
int utf8Len = WideCharToMultiByte(
|
|
CP_UTF8,
|
|
0,
|
|
wide.c_str(),
|
|
static_cast<int>(wide.size()),
|
|
nullptr,
|
|
0,
|
|
nullptr,
|
|
nullptr);
|
|
if (utf8Len <= 0) {
|
|
utf8.clear();
|
|
return false;
|
|
}
|
|
|
|
utf8.assign(static_cast<size_t>(utf8Len), '\0');
|
|
return WideCharToMultiByte(
|
|
CP_UTF8,
|
|
0,
|
|
wide.c_str(),
|
|
static_cast<int>(wide.size()),
|
|
utf8.data(),
|
|
utf8Len,
|
|
nullptr,
|
|
nullptr) == utf8Len;
|
|
}
|
|
|
|
static HANDLE GetConsoleStreamHandle(FILE* stream)
|
|
{
|
|
DWORD stdHandle = stream == stderr
|
|
? STD_ERROR_HANDLE
|
|
: stream == stdout
|
|
? STD_OUTPUT_HANDLE
|
|
: static_cast<DWORD>(-1);
|
|
if (stdHandle == static_cast<DWORD>(-1)) {
|
|
return nullptr;
|
|
}
|
|
|
|
HANDLE handle = GetStdHandle(stdHandle);
|
|
return handle == INVALID_HANDLE_VALUE ? nullptr : handle;
|
|
}
|
|
|
|
static bool IsSystemDarkModeEnabled()
|
|
{
|
|
DWORD value = 1;
|
|
DWORD valueSize = sizeof(value);
|
|
LSTATUS status = RegGetValueW(
|
|
HKEY_CURRENT_USER,
|
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
|
L"AppsUseLightTheme",
|
|
RRF_RT_REG_DWORD,
|
|
nullptr,
|
|
&value,
|
|
&valueSize);
|
|
return status == ERROR_SUCCESS && value == 0;
|
|
}
|
|
|
|
static void RefreshProgressUiTheme()
|
|
{
|
|
g_progressUiTheme.isDarkMode = IsSystemDarkModeEnabled();
|
|
|
|
if (g_progressUiTheme.isDarkMode) {
|
|
g_progressUiTheme.backgroundColor = RGB(32, 32, 32);
|
|
g_progressUiTheme.primaryTextColor = RGB(241, 241, 241);
|
|
g_progressUiTheme.secondaryTextColor = RGB(200, 200, 200);
|
|
g_progressUiTheme.progressTrackColor = RGB(58, 58, 58);
|
|
g_progressUiTheme.progressBarColor = RGB(76, 194, 255);
|
|
} else {
|
|
g_progressUiTheme.backgroundColor = RGB(255, 255, 255);
|
|
g_progressUiTheme.primaryTextColor = RGB(32, 32, 32);
|
|
g_progressUiTheme.secondaryTextColor = RGB(96, 96, 96);
|
|
g_progressUiTheme.progressTrackColor = RGB(232, 234, 237);
|
|
g_progressUiTheme.progressBarColor = RGB(0, 120, 212);
|
|
}
|
|
|
|
if (g_progressUiTheme.backgroundBrush != nullptr) {
|
|
DeleteObject(g_progressUiTheme.backgroundBrush);
|
|
g_progressUiTheme.backgroundBrush = nullptr;
|
|
}
|
|
g_progressUiTheme.backgroundBrush = CreateSolidBrush(g_progressUiTheme.backgroundColor);
|
|
|
|
if (!g_progressUi.enabled || g_progressUi.window == nullptr) {
|
|
return;
|
|
}
|
|
|
|
BOOL useDarkMode = g_progressUiTheme.isDarkMode ? TRUE : FALSE;
|
|
DwmSetWindowAttribute(
|
|
g_progressUi.window,
|
|
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
|
&useDarkMode,
|
|
sizeof(useDarkMode));
|
|
DwmSetWindowAttribute(
|
|
g_progressUi.window,
|
|
LEGACY_DWMWA_USE_IMMERSIVE_DARK_MODE,
|
|
&useDarkMode,
|
|
sizeof(useDarkMode));
|
|
|
|
if (g_progressUi.progressBar != nullptr) {
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETBKCOLOR, 0, g_progressUiTheme.progressTrackColor);
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETBARCOLOR, 0, g_progressUiTheme.progressBarColor);
|
|
}
|
|
|
|
RedrawWindow(
|
|
g_progressUi.window,
|
|
nullptr,
|
|
nullptr,
|
|
RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
|
|
}
|
|
|
|
static LRESULT CALLBACK UpdateProgressWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (message) {
|
|
case WM_SETTINGCHANGE:
|
|
case WM_THEMECHANGED:
|
|
RefreshProgressUiTheme();
|
|
return 0;
|
|
case WM_ERASEBKGND: {
|
|
HDC dc = reinterpret_cast<HDC>(wParam);
|
|
RECT clientRect {};
|
|
GetClientRect(hwnd, &clientRect);
|
|
FillRect(
|
|
dc,
|
|
&clientRect,
|
|
g_progressUiTheme.backgroundBrush != nullptr
|
|
? g_progressUiTheme.backgroundBrush
|
|
: reinterpret_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
|
|
return 1;
|
|
}
|
|
case WM_CTLCOLORSTATIC: {
|
|
HDC dc = reinterpret_cast<HDC>(wParam);
|
|
HWND control = reinterpret_cast<HWND>(lParam);
|
|
COLORREF textColor = control == g_progressUi.detailLabel
|
|
? g_progressUiTheme.secondaryTextColor
|
|
: g_progressUiTheme.primaryTextColor;
|
|
SetTextColor(dc, textColor);
|
|
SetBkColor(dc, g_progressUiTheme.backgroundColor);
|
|
SetBkMode(dc, TRANSPARENT);
|
|
return reinterpret_cast<INT_PTR>(
|
|
g_progressUiTheme.backgroundBrush != nullptr
|
|
? g_progressUiTheme.backgroundBrush
|
|
: reinterpret_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
|
|
}
|
|
case WM_CLOSE:
|
|
return 0;
|
|
case WM_DESTROY:
|
|
return 0;
|
|
default:
|
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
static void ApplyDefaultWindowFont(HWND hwnd)
|
|
{
|
|
HFONT font = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
|
if (font != nullptr) {
|
|
SendMessageW(hwnd, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
|
|
}
|
|
}
|
|
|
|
static bool EnsureProgressWindowClassRegistered()
|
|
{
|
|
static bool isRegistered = false;
|
|
if (isRegistered) {
|
|
return true;
|
|
}
|
|
|
|
WNDCLASSEXW windowClass {};
|
|
windowClass.cbSize = sizeof(windowClass);
|
|
windowClass.lpfnWndProc = UpdateProgressWindowProc;
|
|
windowClass.hInstance = GetModuleHandleW(nullptr);
|
|
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
windowClass.hbrBackground = nullptr;
|
|
windowClass.lpszClassName = PROGRESS_WINDOW_CLASS_NAME;
|
|
|
|
if (RegisterClassExW(&windowClass) == 0) {
|
|
return GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
|
|
}
|
|
|
|
isRegistered = true;
|
|
return true;
|
|
}
|
|
|
|
static bool ShouldShowProgressUi()
|
|
{
|
|
// 控制台输出和进度窗口是两个独立选项,可同时显示;用户可通过 --no-progress-ui 显式关闭进度窗口
|
|
return !g_suppressProgressUi;
|
|
}
|
|
|
|
static void PumpProgressUiMessages()
|
|
{
|
|
if (!g_progressUi.enabled || g_progressUi.window == nullptr) {
|
|
return;
|
|
}
|
|
|
|
MSG message {};
|
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&message);
|
|
DispatchMessageW(&message);
|
|
}
|
|
}
|
|
|
|
static bool InitializeProgressUi()
|
|
{
|
|
if (!ShouldShowProgressUi()) {
|
|
return false;
|
|
}
|
|
|
|
if (!EnsureProgressWindowClassRegistered()) {
|
|
return false;
|
|
}
|
|
|
|
RefreshProgressUiTheme();
|
|
|
|
INITCOMMONCONTROLSEX controls {};
|
|
controls.dwSize = sizeof(controls);
|
|
controls.dwICC = ICC_PROGRESS_CLASS;
|
|
InitCommonControlsEx(&controls);
|
|
|
|
int originX = (GetSystemMetrics(SM_CXSCREEN) - PROGRESS_WINDOW_WIDTH) / 2;
|
|
int originY = (GetSystemMetrics(SM_CYSCREEN) - PROGRESS_WINDOW_HEIGHT) / 2;
|
|
|
|
HWND window = CreateWindowExW(
|
|
WS_EX_TOPMOST | WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME,
|
|
PROGRESS_WINDOW_CLASS_NAME,
|
|
L"MAA 正在更新 | MAA Updating",
|
|
WS_CAPTION,
|
|
originX,
|
|
originY,
|
|
PROGRESS_WINDOW_WIDTH,
|
|
PROGRESS_WINDOW_HEIGHT,
|
|
nullptr,
|
|
nullptr,
|
|
GetModuleHandleW(nullptr),
|
|
nullptr);
|
|
if (window == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
HWND statusLabel = CreateWindowExW(
|
|
0,
|
|
L"STATIC",
|
|
L"正在准备更新... | Preparing update...",
|
|
WS_CHILD | WS_VISIBLE,
|
|
20,
|
|
18,
|
|
PROGRESS_WINDOW_WIDTH - 40,
|
|
24,
|
|
window,
|
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(PROGRESS_STATUS_CONTROL_ID)),
|
|
GetModuleHandleW(nullptr),
|
|
nullptr);
|
|
HWND detailLabel = CreateWindowExW(
|
|
0,
|
|
L"STATIC",
|
|
L"请稍候... | Please wait...",
|
|
WS_CHILD | WS_VISIBLE,
|
|
20,
|
|
48,
|
|
PROGRESS_WINDOW_WIDTH - 40,
|
|
42,
|
|
window,
|
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(PROGRESS_DETAIL_CONTROL_ID)),
|
|
GetModuleHandleW(nullptr),
|
|
nullptr);
|
|
HWND countLabel = CreateWindowExW(
|
|
0,
|
|
L"STATIC",
|
|
L"已处理文件 0/0 | Files processed 0/0",
|
|
WS_CHILD | WS_VISIBLE,
|
|
20,
|
|
96,
|
|
PROGRESS_WINDOW_WIDTH - 40,
|
|
20,
|
|
window,
|
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(PROGRESS_COUNT_CONTROL_ID)),
|
|
GetModuleHandleW(nullptr),
|
|
nullptr);
|
|
HWND progressBar = CreateWindowExW(
|
|
0,
|
|
PROGRESS_CLASSW,
|
|
nullptr,
|
|
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
|
|
20,
|
|
124,
|
|
PROGRESS_WINDOW_WIDTH - 40,
|
|
20,
|
|
window,
|
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(PROGRESS_BAR_CONTROL_ID)),
|
|
GetModuleHandleW(nullptr),
|
|
nullptr);
|
|
|
|
if (statusLabel == nullptr || detailLabel == nullptr || countLabel == nullptr || progressBar == nullptr) {
|
|
DestroyWindow(window);
|
|
return false;
|
|
}
|
|
|
|
ApplyDefaultWindowFont(statusLabel);
|
|
ApplyDefaultWindowFont(detailLabel);
|
|
ApplyDefaultWindowFont(countLabel);
|
|
SendMessageW(progressBar, PBM_SETRANGE32, 0, 1);
|
|
SendMessageW(progressBar, PBM_SETPOS, 0, 0);
|
|
|
|
g_progressUi.enabled = true;
|
|
g_progressUi.window = window;
|
|
g_progressUi.statusLabel = statusLabel;
|
|
g_progressUi.detailLabel = detailLabel;
|
|
g_progressUi.countLabel = countLabel;
|
|
g_progressUi.progressBar = progressBar;
|
|
g_progressUi.processedFileCount = 0;
|
|
g_progressUi.totalFileCount = 0;
|
|
|
|
RefreshProgressUiTheme();
|
|
RefreshProgressUiCountText();
|
|
|
|
ShowWindow(window, SW_SHOWNORMAL);
|
|
UpdateWindow(window);
|
|
PumpProgressUiMessages();
|
|
return true;
|
|
}
|
|
|
|
static void DestroyProgressUi()
|
|
{
|
|
if (g_progressUi.window != nullptr) {
|
|
DestroyWindow(g_progressUi.window);
|
|
}
|
|
|
|
g_progressUi = {};
|
|
|
|
if (g_progressUiTheme.backgroundBrush != nullptr) {
|
|
DeleteObject(g_progressUiTheme.backgroundBrush);
|
|
g_progressUiTheme.backgroundBrush = nullptr;
|
|
}
|
|
}
|
|
|
|
static std::wstring BuildProgressCountText(int processedFileCount, int totalFileCount)
|
|
{
|
|
return L"已处理文件 " + std::to_wstring(processedFileCount) + L"/" + std::to_wstring(totalFileCount) +
|
|
L" | Files processed " + std::to_wstring(processedFileCount) + L"/" + std::to_wstring(totalFileCount);
|
|
}
|
|
|
|
static void RefreshProgressUiCountText()
|
|
{
|
|
if (!g_progressUi.enabled || g_progressUi.countLabel == nullptr) {
|
|
return;
|
|
}
|
|
|
|
std::wstring countText = BuildProgressCountText(g_progressUi.processedFileCount, g_progressUi.totalFileCount);
|
|
SetWindowTextW(g_progressUi.countLabel, countText.c_str());
|
|
}
|
|
|
|
static void SetProgressUiTotalFileCount(int totalFileCount)
|
|
{
|
|
if (!g_progressUi.enabled || g_progressUi.progressBar == nullptr) {
|
|
return;
|
|
}
|
|
|
|
g_progressUi.totalFileCount = totalFileCount > 0 ? totalFileCount : 0;
|
|
if (g_progressUi.processedFileCount > g_progressUi.totalFileCount) {
|
|
g_progressUi.processedFileCount = g_progressUi.totalFileCount;
|
|
}
|
|
|
|
int progressBarMaximum = g_progressUi.totalFileCount > 0 ? g_progressUi.totalFileCount : 1;
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETRANGE32, 0, progressBarMaximum);
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETPOS, g_progressUi.processedFileCount, 0);
|
|
RefreshProgressUiCountText();
|
|
PumpProgressUiMessages();
|
|
}
|
|
|
|
static void SetProgressUiStatus(const std::wstring& status, const std::wstring& detail)
|
|
{
|
|
if (!g_progressUi.enabled) {
|
|
return;
|
|
}
|
|
|
|
if (g_progressUi.statusLabel != nullptr) {
|
|
SetWindowTextW(g_progressUi.statusLabel, status.c_str());
|
|
}
|
|
if (g_progressUi.detailLabel != nullptr) {
|
|
SetWindowTextW(g_progressUi.detailLabel, detail.c_str());
|
|
}
|
|
|
|
RefreshProgressUiCountText();
|
|
PumpProgressUiMessages();
|
|
}
|
|
|
|
static void AdvanceProgressUi(const std::wstring& status, const std::wstring& detail)
|
|
{
|
|
if (!g_progressUi.enabled) {
|
|
return;
|
|
}
|
|
|
|
if (g_progressUi.processedFileCount < g_progressUi.totalFileCount) {
|
|
++g_progressUi.processedFileCount;
|
|
}
|
|
|
|
SetProgressUiStatus(status, detail);
|
|
if (g_progressUi.progressBar != nullptr) {
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETPOS, g_progressUi.processedFileCount, 0);
|
|
}
|
|
PumpProgressUiMessages();
|
|
}
|
|
|
|
static void CompleteProgressUi(const std::wstring& status, const std::wstring& detail)
|
|
{
|
|
if (!g_progressUi.enabled) {
|
|
return;
|
|
}
|
|
|
|
g_progressUi.processedFileCount = g_progressUi.totalFileCount;
|
|
SetProgressUiStatus(status, detail);
|
|
if (g_progressUi.progressBar != nullptr) {
|
|
int progressBarPosition = g_progressUi.totalFileCount > 0 ? g_progressUi.processedFileCount : 1;
|
|
SendMessageW(g_progressUi.progressBar, PBM_SETPOS, progressBarPosition, 0);
|
|
}
|
|
PumpProgressUiMessages();
|
|
}
|
|
|
|
static void ShowProgressUiFailure(const std::wstring& failureReason)
|
|
{
|
|
if (!g_progressUi.enabled) {
|
|
return;
|
|
}
|
|
|
|
SetProgressUiStatus(L"更新失败 | Update failed", failureReason);
|
|
MessageBoxW(
|
|
g_progressUi.window,
|
|
failureReason.c_str(),
|
|
L"MAA 更新失败 | MAA Update Failed",
|
|
MB_OK | MB_ICONERROR);
|
|
}
|
|
|
|
static void WriteLog(const wchar_t* message)
|
|
{
|
|
SYSTEMTIME st {};
|
|
GetLocalTime(&st);
|
|
|
|
wchar_t buf[64];
|
|
_snwprintf_s(buf, _countof(buf), _TRUNCATE,
|
|
L"[%04d-%02d-%02d %02d:%02d:%02d.%03d] ",
|
|
st.wYear, st.wMonth, st.wDay,
|
|
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
|
|
|
|
std::wstring timestampedLine = buf;
|
|
timestampedLine += message;
|
|
|
|
if (g_writeConsoleLog) {
|
|
WriteConsoleText(stdout, timestampedLine, true);
|
|
}
|
|
|
|
if (g_logFile.empty()) return;
|
|
|
|
std::wstring line = timestampedLine;
|
|
line += L"\r\n";
|
|
|
|
// Ensure parent directory exists
|
|
size_t sep = g_logFile.rfind(L'\\');
|
|
if (sep != std::wstring::npos) {
|
|
std::wstring dir = g_logFile.substr(0, sep);
|
|
CreateDirectoryW(dir.c_str(), nullptr);
|
|
}
|
|
|
|
HANDLE hFile = CreateFileW(
|
|
g_logFile.c_str(),
|
|
FILE_APPEND_DATA, FILE_SHARE_READ,
|
|
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (hFile == INVALID_HANDLE_VALUE) return;
|
|
|
|
// Write UTF-8
|
|
std::string utf8;
|
|
if (TryConvertWideToUtf8(line, utf8)) {
|
|
DWORD written = 0;
|
|
WriteFile(hFile, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
|
}
|
|
CloseHandle(hFile);
|
|
}
|
|
|
|
static void WriteLogF(const wchar_t* fmt, ...)
|
|
{
|
|
wchar_t buf[2048];
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
_vsnwprintf_s(buf, _countof(buf), _TRUNCATE, fmt, args);
|
|
va_end(args);
|
|
WriteLog(buf);
|
|
}
|
|
|
|
static void WriteLogEntries(const std::wstring& title, const std::vector<std::wstring>& entries)
|
|
{
|
|
WriteLogF(L"%s (%zu)", title.c_str(), entries.size());
|
|
for (const std::wstring& entry : entries) {
|
|
std::wstring line = L" - " + entry;
|
|
WriteLog(line.c_str());
|
|
}
|
|
}
|
|
|
|
static void WriteConsoleText(FILE* stream, const std::wstring& text, bool appendNewline)
|
|
{
|
|
std::wstring outputText = text;
|
|
if (appendNewline) {
|
|
outputText += L"\n";
|
|
}
|
|
|
|
HANDLE consoleHandle = GetConsoleStreamHandle(stream);
|
|
DWORD consoleMode = 0;
|
|
if (consoleHandle != nullptr && GetConsoleMode(consoleHandle, &consoleMode)) {
|
|
DWORD written = 0;
|
|
WriteConsoleW(
|
|
consoleHandle,
|
|
outputText.c_str(),
|
|
static_cast<DWORD>(outputText.size()),
|
|
&written,
|
|
nullptr);
|
|
return;
|
|
}
|
|
|
|
std::string utf8;
|
|
if (TryConvertWideToUtf8(outputText, utf8)) {
|
|
fwrite(utf8.data(), 1, utf8.size(), stream);
|
|
}
|
|
fflush(stream);
|
|
}
|
|
|
|
static constexpr LONGLONG MAX_UPDATER_LOG_SIZE = 4LL * 1024 * 1024;
|
|
|
|
static void RotateLogIfNeeded()
|
|
{
|
|
if (g_logFile.empty() || !PathExistsW(g_logFile)) {
|
|
return;
|
|
}
|
|
|
|
HANDLE hFile = CreateFileW(
|
|
g_logFile.c_str(),
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
nullptr);
|
|
if (hFile == INVALID_HANDLE_VALUE) {
|
|
return;
|
|
}
|
|
|
|
LARGE_INTEGER size {};
|
|
bool shouldRotate = GetFileSizeEx(hFile, &size) && size.QuadPart > MAX_UPDATER_LOG_SIZE;
|
|
CloseHandle(hFile);
|
|
|
|
if (!shouldRotate) {
|
|
return;
|
|
}
|
|
|
|
std::wstring bakFile = g_logFile + L".bak";
|
|
DeleteFileW(bakFile.c_str());
|
|
MoveFileExW(g_logFile.c_str(), bakFile.c_str(), MOVEFILE_REPLACE_EXISTING);
|
|
}
|
|
|
|
static bool HasArgument(int argc, wchar_t* argv[], const wchar_t* argument)
|
|
{
|
|
for (int index = 1; index < argc; ++index) {
|
|
if (_wcsicmp(argv[index], argument) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Path utilities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::wstring EnsureTrailingSeparator(const std::wstring& path)
|
|
{
|
|
if (!path.empty() && path.back() != L'\\')
|
|
return path + L'\\';
|
|
return path;
|
|
}
|
|
|
|
static std::wstring NormalizeRelativePath(const std::wstring& relativePath)
|
|
{
|
|
std::wstring normalized = relativePath;
|
|
for (wchar_t& c : normalized) {
|
|
if (c == L'/') {
|
|
c = L'\\';
|
|
}
|
|
}
|
|
|
|
while (!normalized.empty() && (normalized.back() == L'\\' || normalized.back() == L' ' || normalized.back() == L'\t')) {
|
|
normalized.pop_back();
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
static bool EqualsIgnoreCase(const std::wstring& left, const wchar_t* right)
|
|
{
|
|
return _wcsicmp(left.c_str(), right) == 0;
|
|
}
|
|
|
|
static bool IsDriveRootDirectory(const std::wstring& path)
|
|
{
|
|
if (path.empty()) {
|
|
return false;
|
|
}
|
|
|
|
wchar_t full[MAX_PATH * 4];
|
|
DWORD len = GetFullPathNameW(path.c_str(), _countof(full), full, nullptr);
|
|
if (len == 0 || len >= _countof(full)) {
|
|
return false;
|
|
}
|
|
|
|
std::wstring normalized = full;
|
|
while (!normalized.empty() && (normalized.back() == L'\\' || normalized.back() == L'/')) {
|
|
normalized.pop_back();
|
|
}
|
|
|
|
if (normalized.size() < 2) {
|
|
return false;
|
|
}
|
|
|
|
wchar_t drive = normalized[0];
|
|
bool hasDriveLetter = (drive >= L'A' && drive <= L'Z') || (drive >= L'a' && drive <= L'z');
|
|
return hasDriveLetter && normalized.size() == 2 && normalized[1] == L':';
|
|
}
|
|
|
|
static bool IsRecycleAndReplaceDirectory(const std::wstring& relativePath)
|
|
{
|
|
std::wstring normalized = NormalizeRelativePath(relativePath);
|
|
return EqualsIgnoreCase(normalized, L"resource") || EqualsIgnoreCase(normalized, L"externals");
|
|
}
|
|
|
|
// Resolves `relativePath` under `rootPath`, writes result to `out`.
|
|
// Returns false if the path is rooted, empty, or escapes the root.
|
|
static bool TryResolvePathUnderRoot(
|
|
const std::wstring& rootPath,
|
|
const std::wstring& relativePath,
|
|
std::wstring& out)
|
|
{
|
|
out.clear();
|
|
|
|
// Reject empty or whitespace
|
|
if (relativePath.empty()) return false;
|
|
bool allSpace = true;
|
|
for (wchar_t c : relativePath)
|
|
if (c != L' ' && c != L'\t') { allSpace = false; break; }
|
|
if (allSpace) return false;
|
|
|
|
// Normalise slashes
|
|
std::wstring rel = relativePath;
|
|
for (wchar_t& c : rel)
|
|
if (c == L'/') c = L'\\';
|
|
|
|
// Reject absolute paths
|
|
if (rel.size() >= 2 && rel[1] == L':') return false;
|
|
if (!rel.empty() && rel[0] == L'\\') return false;
|
|
|
|
// Build candidate and canonicalise
|
|
std::wstring combined = rootPath + L'\\' + rel;
|
|
wchar_t full[MAX_PATH * 4];
|
|
DWORD len = GetFullPathNameW(combined.c_str(), _countof(full), full, nullptr);
|
|
if (len == 0 || len >= _countof(full)) return false;
|
|
|
|
std::wstring candidate = full;
|
|
|
|
// Root must be canonical too
|
|
DWORD rootLen = GetFullPathNameW(rootPath.c_str(), _countof(full), full, nullptr);
|
|
if (rootLen == 0 || rootLen >= _countof(full)) return false;
|
|
std::wstring normalRoot = EnsureTrailingSeparator(std::wstring(full));
|
|
|
|
// Case-insensitive prefix check
|
|
if (candidate.size() < normalRoot.size()) return false;
|
|
if (_wcsnicmp(candidate.c_str(), normalRoot.c_str(), normalRoot.size()) != 0) return false;
|
|
|
|
out = candidate;
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// File / directory helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool PathExistsW(const std::wstring& path)
|
|
{
|
|
return GetFileAttributesW(path.c_str()) != INVALID_FILE_ATTRIBUTES;
|
|
}
|
|
|
|
static bool IsDirectory(const std::wstring& path)
|
|
{
|
|
DWORD attr = GetFileAttributesW(path.c_str());
|
|
return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
static bool CopyDirectoryRecursive(const std::wstring& sourceDir, const std::wstring& destinationDir)
|
|
{
|
|
DWORD attr = GetFileAttributesW(sourceDir.c_str());
|
|
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
return false;
|
|
}
|
|
|
|
CreateDirectoryW(destinationDir.c_str(), nullptr);
|
|
|
|
std::wstring pattern = sourceDir + L"\\*";
|
|
WIN32_FIND_DATAW fd {};
|
|
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
return true;
|
|
}
|
|
|
|
do {
|
|
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0) {
|
|
continue;
|
|
}
|
|
|
|
std::wstring sourceChild = sourceDir + L"\\" + fd.cFileName;
|
|
std::wstring destinationChild = destinationDir + L"\\" + fd.cFileName;
|
|
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
if (!CopyDirectoryRecursive(sourceChild, destinationChild)) {
|
|
FindClose(hFind);
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
EnsureParentDirectory(destinationChild);
|
|
if (!CopyFileW(sourceChild.c_str(), destinationChild.c_str(), FALSE)) {
|
|
FindClose(hFind);
|
|
return false;
|
|
}
|
|
} while (FindNextFileW(hFind, &fd));
|
|
|
|
FindClose(hFind);
|
|
return true;
|
|
}
|
|
|
|
static bool MovePathToRecycleBin(const std::wstring& path)
|
|
{
|
|
std::wstring doubleNullPath = path;
|
|
doubleNullPath.push_back(L'\0');
|
|
|
|
SHFILEOPSTRUCTW op {};
|
|
op.wFunc = FO_DELETE;
|
|
op.pFrom = doubleNullPath.c_str();
|
|
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;
|
|
|
|
int result = SHFileOperationW(&op);
|
|
return result == 0 && !op.fAnyOperationsAborted;
|
|
}
|
|
|
|
static bool CopyPathEntry(const std::wstring& sourcePath, const std::wstring& destinationPath)
|
|
{
|
|
DWORD attr = GetFileAttributesW(sourcePath.c_str());
|
|
if (attr == INVALID_FILE_ATTRIBUTES) {
|
|
return false;
|
|
}
|
|
|
|
if (attr & FILE_ATTRIBUTE_DIRECTORY) {
|
|
return CopyDirectoryRecursive(sourcePath, destinationPath);
|
|
}
|
|
|
|
EnsureParentDirectory(destinationPath);
|
|
return CopyFileW(sourcePath.c_str(), destinationPath.c_str(), FALSE) != FALSE;
|
|
}
|
|
|
|
static void EnsureParentDirectory(const std::wstring& path)
|
|
{
|
|
size_t sep = path.rfind(L'\\');
|
|
if (sep == std::wstring::npos) return;
|
|
std::wstring parent = path.substr(0, sep);
|
|
if (parent.empty()) return;
|
|
|
|
// Recursively create
|
|
EnsureParentDirectory(parent);
|
|
CreateDirectoryW(parent.c_str(), nullptr);
|
|
}
|
|
|
|
static std::wstring CreateArchivedPath(const std::wstring& base)
|
|
{
|
|
SYSTEMTIME st {};
|
|
GetLocalTime(&st);
|
|
wchar_t ts[32];
|
|
_snwprintf_s(ts, _countof(ts), _TRUNCATE,
|
|
L".%04d%02d%02d%02d%02d%02d.",
|
|
st.wYear, st.wMonth, st.wDay,
|
|
st.wHour, st.wMinute, st.wSecond);
|
|
|
|
int index = 0;
|
|
while (true) {
|
|
wchar_t idx[16];
|
|
_itow_s(index, idx, _countof(idx), 10);
|
|
std::wstring candidate = base + ts + idx;
|
|
if (!PathExistsW(candidate)) return candidate;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
// Move a file or directory entry (handles cross-volume by CopyFile+Delete for files).
|
|
// Uses retry and rename fallback for locked files.
|
|
static bool MovePathEntry(const std::wstring& src, const std::wstring& dst)
|
|
{
|
|
EnsureParentDirectory(dst);
|
|
|
|
DWORD attr = GetFileAttributesW(src.c_str());
|
|
if (attr == INVALID_FILE_ATTRIBUTES) return false;
|
|
|
|
if (attr & FILE_ATTRIBUTE_DIRECTORY) {
|
|
auto moveOp = [&]() -> bool {
|
|
return MoveFileExW(src.c_str(), dst.c_str(), 0) != FALSE;
|
|
};
|
|
return RetryFileOp(moveOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS);
|
|
}
|
|
|
|
// For files: try atomic move first; fall back to copy+delete
|
|
{
|
|
auto moveOp = [&]() -> bool {
|
|
return MoveFileExW(src.c_str(), dst.c_str(), MOVEFILE_REPLACE_EXISTING) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(moveOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Move failed (likely the source file is locked). Try copy+force-delete.
|
|
WriteLogF(L"MovePathEntry: move failed, falling back to copy+delete for: %ls", src.c_str());
|
|
{
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyFileW(src.c_str(), dst.c_str(), FALSE) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(copyOp, 3, 500)) {
|
|
// Copy succeeded; force-delete the source
|
|
ForceDeleteFile(src);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Copy also failed. As a last resort, try to rename the source out of the way
|
|
// and then do a fresh copy to the destination.
|
|
WriteLogF(L"MovePathEntry: copy+delete failed, trying rename-and-copy for: %ls", src.c_str());
|
|
{
|
|
std::wstring renamed = RenameLockedFile(src);
|
|
if (!renamed.empty()) {
|
|
// The original path is now free; copy the renamed file to destination
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyFileW(renamed.c_str(), dst.c_str(), FALSE) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(copyOp, 3, 500)) {
|
|
WriteLogF(L"MovePathEntry: rename-and-copy succeeded: %ls", src.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
WriteLogF(L"MovePathEntry: all strategies failed for src=%ls, dst=%ls", src.c_str(), dst.c_str());
|
|
return false;
|
|
}
|
|
|
|
static void PrepareBackupDestination(const std::wstring& backupPath)
|
|
{
|
|
EnsureParentDirectory(backupPath);
|
|
if (!PathExistsW(backupPath)) return;
|
|
|
|
std::wstring archived = CreateArchivedPath(backupPath);
|
|
MovePathEntry(backupPath, archived);
|
|
}
|
|
|
|
static bool MoveExistingPathToBackup(const std::wstring& src, const std::wstring& backup)
|
|
{
|
|
PrepareBackupDestination(backup);
|
|
|
|
// First try: normal move with retry
|
|
if (MovePathEntry(src, backup)) {
|
|
return true;
|
|
}
|
|
|
|
// Second try: copy to backup, then force-delete the source
|
|
WriteLogF(L"MoveExistingPathToBackup: move failed, trying copy+delete for: %ls", src.c_str());
|
|
{
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyPathEntry(src, backup) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(copyOp, 3, 500)) {
|
|
// Copy succeeded; now remove the source
|
|
ForceDeleteFile(src);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
WriteLogF(L"MoveExistingPathToBackup: all strategies failed for: %ls", src.c_str());
|
|
return false;
|
|
}
|
|
|
|
static bool RecycleAndBackupDirectory(const std::wstring& sourcePath, const std::wstring& backupPath)
|
|
{
|
|
PrepareBackupDestination(backupPath);
|
|
|
|
{
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyDirectoryRecursive(sourcePath, backupPath) != FALSE;
|
|
};
|
|
if (!RetryFileOp(copyOp, 3, 500)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try recycle bin first, fall back to force-delete
|
|
if (MovePathToRecycleBin(sourcePath)) {
|
|
return true;
|
|
}
|
|
|
|
return ForceRemoveDirectoryRecursive(sourcePath);
|
|
}
|
|
|
|
static bool RecycleAndBackupPath(const std::wstring& sourcePath, const std::wstring& backupPath)
|
|
{
|
|
PrepareBackupDestination(backupPath);
|
|
|
|
{
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyPathEntry(sourcePath, backupPath) != FALSE;
|
|
};
|
|
if (!RetryFileOp(copyOp, 3, 500)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try recycle bin first, fall back to force-delete
|
|
if (MovePathToRecycleBin(sourcePath)) {
|
|
return true;
|
|
}
|
|
|
|
return ForceDeleteFile(sourcePath);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Write a small UTF-8 text file
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool WriteUtf8File(const std::wstring& path, const char* content)
|
|
{
|
|
return WriteUtf8File(path, std::string(content));
|
|
}
|
|
|
|
static bool WriteUtf8File(const std::wstring& path, const std::string& content)
|
|
{
|
|
EnsureParentDirectory(path);
|
|
HANDLE hFile = CreateFileW(
|
|
path.c_str(),
|
|
GENERIC_WRITE, 0,
|
|
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (hFile == INVALID_HANDLE_VALUE) return false;
|
|
|
|
DWORD written = 0;
|
|
DWORD len = static_cast<DWORD>(content.size());
|
|
bool ok = WriteFile(hFile, content.data(), len, &written, nullptr) != FALSE;
|
|
CloseHandle(hFile);
|
|
return ok && written == len;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal JSON parser for the plan file
|
|
// { "packageType": "full|ota", "removeList": [ "...", ... ], "moveList": [ "...", ... ] }
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::wstring BuildFileIoFailureReason(const wchar_t* action, const std::wstring& path, DWORD errorCode)
|
|
{
|
|
return std::wstring(action) + L": " + path + L" (error=" + std::to_wstring(errorCode) + L")";
|
|
}
|
|
|
|
static bool TryReadUtf8File(const std::wstring& path, std::string& content, std::wstring& failureReason)
|
|
{
|
|
content.clear();
|
|
failureReason.clear();
|
|
|
|
HANDLE hFile = CreateFileW(
|
|
path.c_str(),
|
|
GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (hFile == INVALID_HANDLE_VALUE) {
|
|
failureReason = BuildFileIoFailureReason(L"Failed to open file", path, GetLastError());
|
|
return false;
|
|
}
|
|
|
|
std::string buf;
|
|
char chunk[4096];
|
|
while (true) {
|
|
DWORD chunkRead = 0;
|
|
if (!ReadFile(hFile, chunk, static_cast<DWORD>(sizeof(chunk)), &chunkRead, nullptr)) {
|
|
failureReason = BuildFileIoFailureReason(L"Failed to read file", path, GetLastError());
|
|
CloseHandle(hFile);
|
|
return false;
|
|
}
|
|
|
|
if (chunkRead == 0) {
|
|
break;
|
|
}
|
|
|
|
buf.append(chunk, chunkRead);
|
|
}
|
|
|
|
CloseHandle(hFile);
|
|
content.swap(buf);
|
|
return true;
|
|
}
|
|
|
|
// Convert a UTF-8 JSON string value to std::wstring.
|
|
// Handles basic \uXXXX, \n, \r, \t, \\, \/
|
|
static std::wstring Utf8ToWide(const std::string& s)
|
|
{
|
|
if (s.empty()) return {};
|
|
int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
|
|
if (len <= 1) return {};
|
|
std::wstring out(len - 1, L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, out.data(), len);
|
|
return out;
|
|
}
|
|
|
|
// Parse a JSON string literal starting at pos (after the opening quote).
|
|
// Advances pos past the closing quote. Returns the raw UTF-8 content.
|
|
static std::string ParseJsonString(const std::string& json, size_t& pos)
|
|
{
|
|
std::string result;
|
|
while (pos < json.size()) {
|
|
char c = json[pos++];
|
|
if (c == '"') break;
|
|
if (c == '\\' && pos < json.size()) {
|
|
char esc = json[pos++];
|
|
switch (esc) {
|
|
case '"': result += '"'; break;
|
|
case '\\': result += '\\'; break;
|
|
case '/': result += '/'; break;
|
|
case 'n': result += '\n'; break;
|
|
case 'r': result += '\r'; break;
|
|
case 't': result += '\t'; break;
|
|
case 'u': {
|
|
if (pos + 4 <= json.size()) {
|
|
char hex[5] = {};
|
|
memcpy(hex, json.c_str() + pos, 4);
|
|
pos += 4;
|
|
unsigned cp = static_cast<unsigned>(strtoul(hex, nullptr, 16));
|
|
// Encode code point as UTF-8
|
|
if (cp < 0x80) {
|
|
result += static_cast<char>(cp);
|
|
} else if (cp < 0x800) {
|
|
result += static_cast<char>(0xC0 | (cp >> 6));
|
|
result += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else {
|
|
result += static_cast<char>(0xE0 | (cp >> 12));
|
|
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
result += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: result += esc; break;
|
|
}
|
|
} else {
|
|
result += c;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static size_t SkipJsonWhitespace(const std::string& json, size_t pos)
|
|
{
|
|
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||
|
|
json[pos] == '\r' || json[pos] == '\n'))
|
|
{
|
|
++pos;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
// Skips one JSON value starting at `pos` and returns the position right after it.
|
|
static size_t SkipJsonValue(const std::string& json, size_t pos)
|
|
{
|
|
pos = SkipJsonWhitespace(json, pos);
|
|
if (pos >= json.size()) return pos;
|
|
|
|
char c = json[pos];
|
|
if (c == '"') {
|
|
++pos;
|
|
ParseJsonString(json, pos);
|
|
return pos;
|
|
}
|
|
|
|
if (c == '{' || c == '[') {
|
|
char open = c;
|
|
char close = (c == '{') ? '}' : ']';
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool escaped = false;
|
|
|
|
for (; pos < json.size(); ++pos) {
|
|
char ch = json[pos];
|
|
if (inString) {
|
|
if (escaped) {
|
|
escaped = false;
|
|
} else if (ch == '\\') {
|
|
escaped = true;
|
|
} else if (ch == '"') {
|
|
inString = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (ch == '"') {
|
|
inString = true;
|
|
continue;
|
|
}
|
|
|
|
if (ch == open) {
|
|
++depth;
|
|
} else if (ch == close) {
|
|
--depth;
|
|
if (depth == 0) {
|
|
++pos;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
// number / true / false / null
|
|
while (pos < json.size()) {
|
|
char ch = json[pos];
|
|
if (ch == ',' || ch == '}' || ch == ']' || ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
|
|
break;
|
|
}
|
|
++pos;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
// Finds `"key": <value>` in the top-level JSON object and returns the start of `<value>`.
|
|
static size_t FindTopLevelJsonValueStartByKey(const std::string& json, const char* key)
|
|
{
|
|
size_t pos = SkipJsonWhitespace(json, 0);
|
|
if (pos >= json.size() || json[pos] != '{') return std::string::npos;
|
|
++pos;
|
|
|
|
while (pos < json.size()) {
|
|
pos = SkipJsonWhitespace(json, pos);
|
|
if (pos >= json.size()) return std::string::npos;
|
|
|
|
if (json[pos] == ',') {
|
|
++pos;
|
|
continue;
|
|
}
|
|
|
|
if (json[pos] == '}') {
|
|
return std::string::npos;
|
|
}
|
|
|
|
if (json[pos] != '"') {
|
|
++pos;
|
|
continue;
|
|
}
|
|
|
|
++pos;
|
|
std::string currentKey = ParseJsonString(json, pos);
|
|
|
|
pos = SkipJsonWhitespace(json, pos);
|
|
if (pos >= json.size() || json[pos] != ':') {
|
|
return std::string::npos;
|
|
}
|
|
|
|
++pos;
|
|
pos = SkipJsonWhitespace(json, pos);
|
|
if (pos >= json.size()) return std::string::npos;
|
|
|
|
if (currentKey == key) {
|
|
return pos;
|
|
}
|
|
|
|
pos = SkipJsonValue(json, pos);
|
|
}
|
|
|
|
return std::string::npos;
|
|
}
|
|
|
|
// Find a JSON array by key name and return its string elements.
|
|
static std::vector<std::wstring> ParseJsonStringArray(
|
|
const std::string& json,
|
|
const char* key)
|
|
{
|
|
std::vector<std::wstring> result;
|
|
|
|
size_t pos = FindTopLevelJsonValueStartByKey(json, key);
|
|
if (pos == std::string::npos) return result;
|
|
|
|
if (pos >= json.size() || json[pos] != '[') return result;
|
|
pos++; // consume '['
|
|
|
|
// Parse array elements
|
|
while (pos < json.size()) {
|
|
// Skip whitespace
|
|
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||
|
|
json[pos] == '\r' || json[pos] == '\n' ||
|
|
json[pos] == ','))
|
|
pos++;
|
|
|
|
if (pos >= json.size()) break;
|
|
if (json[pos] == ']') break;
|
|
if (json[pos] == '"') {
|
|
pos++; // consume opening quote
|
|
std::string val = ParseJsonString(json, pos);
|
|
result.push_back(Utf8ToWide(val));
|
|
} else {
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static std::wstring ParseJsonStringProperty(const std::string& json, const char* key)
|
|
{
|
|
size_t pos = FindTopLevelJsonValueStartByKey(json, key);
|
|
if (pos == std::string::npos) return {};
|
|
|
|
if (pos >= json.size() || json[pos] != '"') return {};
|
|
|
|
pos++;
|
|
return Utf8ToWide(ParseJsonString(json, pos));
|
|
}
|
|
|
|
struct PendingUpdatePlan
|
|
{
|
|
std::wstring packageType;
|
|
std::vector<std::wstring> removeList;
|
|
std::vector<std::wstring> moveList;
|
|
};
|
|
|
|
static bool LoadPendingUpdatePlan(
|
|
const std::wstring& planFile,
|
|
PendingUpdatePlan& outPlan,
|
|
std::wstring& failureReason,
|
|
std::string* rawJson)
|
|
{
|
|
outPlan = {};
|
|
failureReason.clear();
|
|
|
|
if (!PathExistsW(planFile)) {
|
|
failureReason = L"Plan file not found: " + planFile;
|
|
return false;
|
|
}
|
|
|
|
std::string planJson;
|
|
if (!TryReadUtf8File(planFile, planJson, failureReason)) {
|
|
return false;
|
|
}
|
|
|
|
if (rawJson != nullptr) {
|
|
*rawJson = planJson;
|
|
}
|
|
|
|
if (planJson.empty()) {
|
|
failureReason = L"Plan file is empty: " + planFile;
|
|
return false;
|
|
}
|
|
|
|
outPlan.packageType = ParseJsonStringProperty(planJson, "packageType");
|
|
outPlan.removeList = ParseJsonStringArray(planJson, "removeList");
|
|
outPlan.moveList = ParseJsonStringArray(planJson, "moveList");
|
|
return true;
|
|
}
|
|
|
|
static void PrintPlanEntries(const std::wstring& title, const std::vector<std::wstring>& entries)
|
|
{
|
|
WriteConsoleText(stdout, title + L" (" + std::to_wstring(entries.size()) + L")", true);
|
|
for (const std::wstring& entry : entries) {
|
|
WriteConsoleText(stdout, L" - " + entry, true);
|
|
}
|
|
}
|
|
|
|
static int RunPlanParserTest(const std::wstring& initialPlanFile)
|
|
{
|
|
std::wstring planFile = initialPlanFile;
|
|
if (planFile.empty()) {
|
|
WriteConsoleText(stdout, L"请输入要解析的 plan 文件路径 | Enter the plan file path to parse: ", false);
|
|
std::getline(std::wcin, planFile);
|
|
}
|
|
|
|
if (planFile.empty()) {
|
|
WriteConsoleText(stderr, L"未提供 plan 文件路径。 | No plan file path was provided.", true);
|
|
return 1;
|
|
}
|
|
|
|
PendingUpdatePlan plan;
|
|
std::wstring failureReason;
|
|
std::string rawJson;
|
|
if (!LoadPendingUpdatePlan(planFile, plan, failureReason, &rawJson)) {
|
|
WriteConsoleText(stderr, failureReason, true);
|
|
return 2;
|
|
}
|
|
|
|
WriteConsoleText(stdout, L"文件读取成功: " + planFile + L" | Plan file loaded successfully: " + planFile, true);
|
|
WriteConsoleText(stdout, L"原始字节数: " + std::to_wstring(rawJson.size()) + L" | Raw byte count: " + std::to_wstring(rawJson.size()), true);
|
|
WriteConsoleText(stdout, L"包类型: " + (plan.packageType.empty() ? std::wstring(L"<空>") : plan.packageType) +
|
|
L" | Package type: " + (plan.packageType.empty() ? std::wstring(L"<empty>") : plan.packageType), true);
|
|
PrintPlanEntries(L"待删除文件列表 | Files to remove", plan.removeList);
|
|
PrintPlanEntries(L"待安装文件列表 | Files to install", plan.moveList);
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Update mutex helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Acquires a named system mutex to prevent new MAA instances from starting
|
|
// while the update is in progress. Returns nullptr if the mutex cannot be
|
|
// acquired (e.g. another MAA instance holds the lock).
|
|
static HANDLE AcquireUpdateMutex(const std::wstring& mutexName)
|
|
{
|
|
HANDLE hMutex = CreateMutexW(nullptr, TRUE, mutexName.c_str());
|
|
if (hMutex == nullptr) {
|
|
WriteLog((L"CreateMutexW failed, error=" + std::to_wstring(GetLastError())).c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
DWORD waitResult = WaitForSingleObject(hMutex, UPDATE_MUTEX_TIMEOUT_MS);
|
|
|
|
if (waitResult == WAIT_OBJECT_0) {
|
|
WriteLog((L"Mutex acquired: " + mutexName).c_str());
|
|
return hMutex;
|
|
}
|
|
|
|
if (waitResult == WAIT_ABANDONED) {
|
|
// Previous MAA instance terminated abnormally; we now own the mutex.
|
|
WriteLog((L"Mutex acquired after WAIT_ABANDONED (previous instance crashed): " + mutexName).c_str());
|
|
return hMutex;
|
|
}
|
|
|
|
if (waitResult == WAIT_TIMEOUT) {
|
|
WriteLog((L"Mutex acquisition timed out after " + std::to_wstring(UPDATE_MUTEX_TIMEOUT_MS) + L"ms: " + mutexName).c_str());
|
|
} else {
|
|
WriteLog((L"WaitForSingleObject failed, error=" + std::to_wstring(GetLastError())).c_str());
|
|
}
|
|
|
|
CloseHandle(hMutex);
|
|
return nullptr;
|
|
}
|
|
|
|
static void ReleaseUpdateMutex(HANDLE hMutex)
|
|
{
|
|
if (hMutex == nullptr || hMutex == INVALID_HANDLE_VALUE) {
|
|
return;
|
|
}
|
|
|
|
ReleaseMutex(hMutex);
|
|
CloseHandle(hMutex);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Retry and force-delete helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Retries a file operation with exponential backoff on lock-related errors.
|
|
// Returns true if the operation eventually succeeded.
|
|
// Does NOT retry on non-lock errors (ERROR_FILE_NOT_FOUND, etc.).
|
|
static bool RetryFileOp(const std::function<bool()>& op, int maxAttempts, DWORD initialDelayMs)
|
|
{
|
|
for (int attempt = 1; attempt <= maxAttempts; ++attempt) {
|
|
if (op()) {
|
|
return true;
|
|
}
|
|
|
|
DWORD error = GetLastError();
|
|
if (error != ERROR_SHARING_VIOLATION && error != ERROR_LOCK_VIOLATION &&
|
|
error != ERROR_ACCESS_DENIED) {
|
|
// Not a transient locking error — no point retrying.
|
|
return false;
|
|
}
|
|
|
|
if (attempt < maxAttempts) {
|
|
DWORD delay = initialDelayMs * static_cast<DWORD>(1 << (attempt - 1));
|
|
WriteLogF(L"RetryFileOp: attempt %d/%d failed (error=%u), retrying in %u ms",
|
|
attempt, maxAttempts, error, delay);
|
|
Sleep(delay);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Renames a locked file to a unique name with .pendingdelete suffix.
|
|
// This works on NTFS even when the file is open with FILE_SHARE_DELETE.
|
|
// Returns the new path on success, or an empty string on failure.
|
|
static std::wstring RenameLockedFile(const std::wstring& path)
|
|
{
|
|
// Generate a unique suffix using tick count + a counter
|
|
static volatile LONG s_renameCounter = 0;
|
|
DWORD tick = GetTickCount();
|
|
LONG counter = InterlockedIncrement(&s_renameCounter);
|
|
wchar_t suffix[64];
|
|
_snwprintf_s(suffix, _countof(suffix), _TRUNCATE,
|
|
L".%08x%04x" PENDING_DELETE_SUFFIX, tick, static_cast<WORD>(counter & 0xFFFF));
|
|
|
|
std::wstring newPath = path + suffix;
|
|
|
|
// Try the rename; if it fails (e.g. path already exists), append more entropy
|
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
|
if (MoveFileExW(path.c_str(), newPath.c_str(), MOVEFILE_REPLACE_EXISTING) != FALSE) {
|
|
WriteLogF(L"Renamed locked file: %ls -> %ls", path.c_str(), newPath.c_str());
|
|
return newPath;
|
|
}
|
|
|
|
// With MOVEFILE_REPLACE_EXISTING the only likely reason is sharing-violation
|
|
// on the source. Give it a brief retry.
|
|
Sleep(100);
|
|
std::wstring retryPath = newPath + L"." + std::to_wstring(attempt);
|
|
newPath = retryPath;
|
|
}
|
|
|
|
WriteLogF(L"Failed to rename locked file after 10 attempts: %ls", path.c_str());
|
|
return {};
|
|
}
|
|
|
|
// Attempts to delete a file through multiple escalation layers:
|
|
// 1. Normal DeleteFileW + retry
|
|
// 2. Rename the file out of the way, then delete
|
|
// 3. Schedule for deletion on next reboot (MOVEFILE_DELAY_UNTIL_REBOOT)
|
|
// Returns true if the file is no longer present at the original path.
|
|
static bool ForceDeleteFile(const std::wstring& path)
|
|
{
|
|
// --- Layer 1: Normal delete with retry ---
|
|
auto deleteOp = [&]() -> bool {
|
|
return DeleteFileW(path.c_str()) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(deleteOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS)) {
|
|
WriteLogF(L"Deleted file: %ls", path.c_str());
|
|
return true;
|
|
}
|
|
|
|
if (!PathExistsW(path)) {
|
|
return true; // already gone
|
|
}
|
|
|
|
// --- Layer 2: Rename then delete ---
|
|
std::wstring renamed = RenameLockedFile(path);
|
|
if (!renamed.empty()) {
|
|
// The rename succeeded; now try to delete the renamed file
|
|
RetryFileOp([&]() -> bool { return DeleteFileW(renamed.c_str()) != FALSE; },
|
|
3, 500);
|
|
if (PathExistsW(renamed)) {
|
|
WriteLogF(L"Renamed file could not be deleted immediately (will remain as .pendingdelete): %ls",
|
|
renamed.c_str());
|
|
}
|
|
// Even if we couldn't delete the renamed file, the original path is now free.
|
|
WriteLogF(L"File vacated via rename: %ls -> %ls", path.c_str(), renamed.c_str());
|
|
return true;
|
|
}
|
|
|
|
// --- Layer 3: Schedule for deletion on next reboot ---
|
|
if (MoveFileExW(path.c_str(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT) != FALSE) {
|
|
WriteLogF(L"Scheduled file for deletion on next reboot: %ls", path.c_str());
|
|
return true;
|
|
}
|
|
|
|
WriteLogF(L"All deletion strategies failed for: %ls (error=%u)", path.c_str(), GetLastError());
|
|
return false;
|
|
}
|
|
|
|
// Recursively removes a directory, using ForceDeleteFile for each file.
|
|
static bool ForceRemoveDirectoryRecursive(const std::wstring& dir)
|
|
{
|
|
std::wstring pattern = dir + L"\\*";
|
|
WIN32_FIND_DATAW fd {};
|
|
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
|
|
if (hFind != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0)
|
|
continue;
|
|
std::wstring child = dir + L"\\" + fd.cFileName;
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
ForceRemoveDirectoryRecursive(child);
|
|
} else {
|
|
ForceDeleteFile(child);
|
|
}
|
|
} while (FindNextFileW(hFind, &fd));
|
|
FindClose(hFind);
|
|
}
|
|
return RemoveDirectoryW(dir.c_str()) != FALSE || !PathExistsW(dir);
|
|
}
|
|
|
|
// Atomically installs a file from sourcePath to targetPath.
|
|
// Uses ReplaceFileW which handles locked target files better than
|
|
// a separate copy-then-delete cycle.
|
|
// If ReplaceFileW fails, falls back to copy-then-move with retry.
|
|
static bool InstallFileAtomic(const std::wstring& sourcePath,
|
|
const std::wstring& targetPath)
|
|
{
|
|
EnsureParentDirectory(targetPath);
|
|
|
|
// Generate a temporary path alongside the target
|
|
DWORD tick = GetTickCount();
|
|
std::wstring tempPath = targetPath + L"." + std::to_wstring(tick) + L".tmpinstall";
|
|
|
|
// Copy source to the temp location first
|
|
{
|
|
DWORD attr = GetFileAttributesW(sourcePath.c_str());
|
|
if (attr == INVALID_FILE_ATTRIBUTES) {
|
|
WriteLogF(L"InstallFileAtomic: source not found: %ls", sourcePath.c_str());
|
|
return false;
|
|
}
|
|
|
|
auto copyOp = [&]() -> bool {
|
|
return CopyFileW(sourcePath.c_str(), tempPath.c_str(), FALSE) != FALSE;
|
|
};
|
|
|
|
if (!RetryFileOp(copyOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS)) {
|
|
WriteLogF(L"InstallFileAtomic: failed to copy to temp: %ls (error=%u)",
|
|
tempPath.c_str(), GetLastError());
|
|
// Clean up the temp file if it exists
|
|
DeleteFileW(tempPath.c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// --- Try ReplaceFileW (atomic swap) ---
|
|
{
|
|
auto replaceOp = [&]() -> bool {
|
|
// ReplaceFileW(replaced, replacement, backup, flags, exclude, reserved)
|
|
return ReplaceFileW(targetPath.c_str(),
|
|
tempPath.c_str(),
|
|
nullptr, // no additional backup needed
|
|
REPLACEFILE_IGNORE_MERGE_ERRORS | REPLACEFILE_WRITE_THROUGH,
|
|
nullptr, nullptr) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(replaceOp, 3, 500)) {
|
|
WriteLogF(L"InstallFileAtomic: ReplaceFileW succeeded: %ls", targetPath.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// --- ReplaceFileW failed; fall back to copy-then-rename ---
|
|
WriteLogF(L"InstallFileAtomic: ReplaceFileW failed, falling back to MoveFileEx for: %ls",
|
|
targetPath.c_str());
|
|
|
|
// If the target exists and is locked, try to rename it out of the way
|
|
if (PathExistsW(targetPath)) {
|
|
std::wstring renamed = RenameLockedFile(targetPath);
|
|
if (!renamed.empty()) {
|
|
// Target path is now free; move temp into place
|
|
if (MoveFileExW(tempPath.c_str(), targetPath.c_str(), MOVEFILE_REPLACE_EXISTING) != FALSE) {
|
|
WriteLogF(L"InstallFileAtomic: installed after renaming locked target: %ls", targetPath.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Last resort: direct move with retry
|
|
auto moveOp = [&]() -> bool {
|
|
return MoveFileExW(tempPath.c_str(), targetPath.c_str(), MOVEFILE_REPLACE_EXISTING) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(moveOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS)) {
|
|
WriteLogF(L"InstallFileAtomic: move succeeded: %ls", targetPath.c_str());
|
|
return true;
|
|
}
|
|
|
|
// Clean up temp file
|
|
DeleteFileW(tempPath.c_str());
|
|
WriteLogF(L"InstallFileAtomic: all strategies failed for: %ls", targetPath.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Scans rootDir and deletes any remaining .pendingdelete files left
|
|
// from a previous interrupted update.
|
|
static void CleanupPendingDeleteFiles(const std::wstring& rootDir)
|
|
{
|
|
std::wstring pattern = rootDir + L"\\*" + PENDING_DELETE_SUFFIX;
|
|
int cleanedCount = 0;
|
|
|
|
WIN32_FIND_DATAW fd {};
|
|
HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd);
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
return;
|
|
}
|
|
|
|
do {
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
continue;
|
|
}
|
|
|
|
std::wstring filePath = rootDir + L"\\" + fd.cFileName;
|
|
auto deleteOp = [&]() -> bool {
|
|
return DeleteFileW(filePath.c_str()) != FALSE;
|
|
};
|
|
|
|
if (RetryFileOp(deleteOp, 3, 500)) {
|
|
++cleanedCount;
|
|
} else {
|
|
WriteLogF(L"Could not clean up pending-delete file: %ls", filePath.c_str());
|
|
}
|
|
} while (FindNextFileW(hFind, &fd));
|
|
|
|
FindClose(hFind);
|
|
|
|
if (cleanedCount > 0) {
|
|
WriteLogF(L"Cleaned up %d pending-delete file(s).", cleanedCount);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Entry point
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int wmain(int argc, wchar_t* argv[])
|
|
{
|
|
constexpr int REQUIRED_ARGS = 9; // excluding argv[0]
|
|
|
|
if (argc >= 2 && wcscmp(argv[1], L"--test-plan") == 0) {
|
|
return RunPlanParserTest(argc >= 3 ? argv[2] : L"");
|
|
}
|
|
|
|
if (argc - 1 < REQUIRED_ARGS) {
|
|
MessageBoxW(
|
|
nullptr,
|
|
L"MAA.Updater.exe 是 MAA 内部使用的更新程序,不应被手动启动。\n\n"
|
|
L"请直接运行 MAA.exe。\n\n"
|
|
L"MAA.Updater.exe is an updater used internally by MAA and should not be manually started.\n\n"
|
|
L"Please run MAA.exe directly.",
|
|
L"MAA 更新程序 | MAA Updater",
|
|
MB_OK | MB_ICONINFORMATION);
|
|
return 1;
|
|
}
|
|
|
|
DWORD parentPid = static_cast<DWORD>(_wtoi(argv[1]));
|
|
std::wstring rootDir = argv[2];
|
|
std::wstring extractDir = argv[3];
|
|
std::wstring backupDir = argv[4];
|
|
std::wstring packagePath = argv[5];
|
|
std::wstring successStatusFile = argv[6];
|
|
std::wstring failureStatusFile = argv[7];
|
|
std::wstring relaunchExecutable = argv[8];
|
|
std::wstring planFile = argv[9];
|
|
g_writeConsoleLog = HasArgument(argc, argv, L"--show-console");
|
|
g_suppressProgressUi = HasArgument(argc, argv, L"--no-progress-ui");
|
|
|
|
std::wstring mutexName;
|
|
for (int i = 1; i < argc - 1; ++i) {
|
|
if (_wcsicmp(argv[i], MUTEX_NAME_ARG) == 0) {
|
|
mutexName = argv[i + 1];
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_logFile = rootDir + L"\\debug\\pending-update-applier.log";
|
|
RotateLogIfNeeded();
|
|
InitializeProgressUi();
|
|
SetProgressUiStatus(
|
|
L"正在准备更新... | Preparing update...",
|
|
L"等待 MAA 主程序退出 | Waiting for the main MAA process to exit");
|
|
|
|
WriteLog(L"MAA.Updater started (C++ external updater).");
|
|
WriteLog((std::wstring(L"Console output: ") + (g_writeConsoleLog ? L"enabled" : L"disabled")).c_str());
|
|
WriteLog((L"Parent PID: " + std::to_wstring(parentPid) + L", root dir: " + rootDir).c_str());
|
|
WriteLog((L"Plan file: " + planFile + L", extract dir: " + extractDir).c_str());
|
|
|
|
bool shouldRelaunch = false;
|
|
bool success = false;
|
|
std::wstring failureReason;
|
|
HANDLE hUpdateMutex = nullptr;
|
|
|
|
// ------------------------------------------------------------------
|
|
// Wait for parent process to exit
|
|
// ------------------------------------------------------------------
|
|
HANDLE hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
|
|
if (hParent != nullptr) {
|
|
WriteLog((L"Waiting for parent process to exit, PID=" + std::to_wstring(parentPid)).c_str());
|
|
while (WaitForSingleObject(hParent, 100) == WAIT_TIMEOUT) {
|
|
PumpProgressUiMessages();
|
|
}
|
|
CloseHandle(hParent);
|
|
WriteLog(L"Parent process exited.");
|
|
SetProgressUiStatus(
|
|
L"正在准备更新... | Preparing update...",
|
|
L"已确认主程序退出,开始读取更新计划 | Parent process exited, reading update plan");
|
|
} else {
|
|
WriteLog((L"Could not open the parent process, it may have already exited, PID=" + std::to_wstring(parentPid) + L". Continuing.").c_str());
|
|
SetProgressUiStatus(
|
|
L"正在准备更新... | Preparing update...",
|
|
L"主程序已退出,开始读取更新计划 | Main process already exited, reading update plan");
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Acquire update mutex to prevent new MAA instances from starting
|
|
// ------------------------------------------------------------------
|
|
if (!mutexName.empty()) {
|
|
hUpdateMutex = AcquireUpdateMutex(mutexName);
|
|
if (hUpdateMutex == nullptr) {
|
|
failureReason =
|
|
L"检测到另一个 MAA 实例正在运行,无法执行更新。请关闭所有 MAA 窗口后重试。\n\n"
|
|
L"Another MAA instance is running. Please close all MAA windows and try again.";
|
|
WriteLog(failureReason.c_str());
|
|
SetProgressUiStatus(
|
|
L"无法继续更新 | Update blocked",
|
|
L"检测到另一个 MAA 实例正在运行 | Another MAA instance is running");
|
|
} else {
|
|
WriteLog(L"Update mutex acquired, preventing new MAA instances from starting.");
|
|
}
|
|
|
|
// Clean up any .pendingdelete files left from a previous interrupted update
|
|
CleanupPendingDeleteFiles(rootDir);
|
|
} else {
|
|
WriteLog(L"No mutex name provided, update mutex will not be used.");
|
|
}
|
|
|
|
if (IsDriveRootDirectory(rootDir)) {
|
|
failureReason =
|
|
L"检测到 MAA 安装在盘符根目录,已阻止更新继续执行。请先将 MAA 移动到独立文件夹后再重试。\n\n"
|
|
L"Detected MAA installed directly in a drive root. Update execution was blocked. Please move MAA into a dedicated folder and try again.";
|
|
WriteLog(failureReason.c_str());
|
|
SetProgressUiStatus(
|
|
L"无法继续更新 | Update blocked",
|
|
L"检测到盘符根目录安装 | Drive-root install detected");
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Read plan
|
|
// ------------------------------------------------------------------
|
|
do {
|
|
if (!failureReason.empty()) {
|
|
break;
|
|
}
|
|
|
|
PendingUpdatePlan plan;
|
|
if (!LoadPendingUpdatePlan(planFile, plan, failureReason)) {
|
|
WriteLog(failureReason.c_str());
|
|
break;
|
|
}
|
|
|
|
bool isFullPackage = EqualsIgnoreCase(plan.packageType, L"full");
|
|
const std::vector<std::wstring>& removeList = plan.removeList;
|
|
const std::vector<std::wstring>& moveList = plan.moveList;
|
|
|
|
SetProgressUiTotalFileCount(static_cast<int>(removeList.size() + moveList.size()));
|
|
SetProgressUiStatus(
|
|
L"正在分析更新内容... | Analyzing update contents...",
|
|
L"更新计划读取完成 | Update plan loaded");
|
|
|
|
WriteLog((L"Plan loaded, package type: " + plan.packageType + L", remove entries: " + std::to_wstring(removeList.size()) + L", install entries: " + std::to_wstring(moveList.size())).c_str());
|
|
WriteLogEntries(L"Files to remove", removeList);
|
|
WriteLogEntries(L"Files to install", moveList);
|
|
|
|
CreateDirectoryW(backupDir.c_str(), nullptr);
|
|
|
|
// ---- Remove entries ----
|
|
for (const std::wstring& rel : removeList) {
|
|
std::wstring targetPath;
|
|
if (!TryResolvePathUnderRoot(rootDir, rel, targetPath)) {
|
|
failureReason = L"Illegal path in removeList: " + rel;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
if (!PathExistsW(targetPath)) {
|
|
AdvanceProgressUi(
|
|
L"正在清理旧文件... | Cleaning old files...",
|
|
rel);
|
|
continue;
|
|
}
|
|
|
|
if (!isFullPackage && IsDirectory(targetPath)) {
|
|
WriteLog((L"Skipping directory removal for a non-full package, entry: " + rel + L", target: " + targetPath).c_str());
|
|
AdvanceProgressUi(
|
|
L"正在清理旧文件... | Cleaning old files...",
|
|
rel);
|
|
continue;
|
|
}
|
|
|
|
std::wstring backupPath;
|
|
if (!TryResolvePathUnderRoot(backupDir, rel, backupPath)) {
|
|
failureReason = L"Illegal backup path for removeList: " + rel;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
|
|
WriteLog((L"Removing and backing up: " + targetPath + L" -> " + backupPath).c_str());
|
|
bool backupOk = isFullPackage
|
|
? RecycleAndBackupPath(targetPath, backupPath)
|
|
: MoveExistingPathToBackup(targetPath, backupPath);
|
|
if (!backupOk) {
|
|
failureReason = L"Failed to move to backup: " + targetPath;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
|
|
AdvanceProgressUi(
|
|
L"正在清理旧文件... | Cleaning old files...",
|
|
rel);
|
|
}
|
|
|
|
// ---- Move/install entries ----
|
|
for (const std::wstring& rel : moveList) {
|
|
std::wstring sourcePath, targetPath, backupPath;
|
|
|
|
if (!TryResolvePathUnderRoot(extractDir, rel, sourcePath) ||
|
|
!TryResolvePathUnderRoot(rootDir, rel, targetPath) ||
|
|
!TryResolvePathUnderRoot(backupDir, rel, backupPath))
|
|
{
|
|
failureReason = L"Illegal path in moveList: " + rel;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
|
|
if (PathExistsW(targetPath)) {
|
|
if (!isFullPackage && IsDirectory(targetPath)) {
|
|
failureReason = L"Non-full package cannot replace directory: " + targetPath;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
|
|
WriteLog((L"Backing up existing entry: " + targetPath).c_str());
|
|
bool backupOk = IsRecycleAndReplaceDirectory(rel)
|
|
? RecycleAndBackupDirectory(targetPath, backupPath)
|
|
: MoveExistingPathToBackup(targetPath, backupPath);
|
|
if (!backupOk) {
|
|
failureReason = L"Failed to back up existing entry: " + targetPath;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
}
|
|
|
|
WriteLog((L"Installing new file: " + sourcePath + L" -> " + targetPath).c_str());
|
|
|
|
bool installOk = false;
|
|
DWORD sourceAttr = GetFileAttributesW(sourcePath.c_str());
|
|
bool isSourceFile = (sourceAttr != INVALID_FILE_ATTRIBUTES) &&
|
|
!(sourceAttr & FILE_ATTRIBUTE_DIRECTORY);
|
|
|
|
if (isSourceFile) {
|
|
// Use atomic file replacement for individual files
|
|
installOk = InstallFileAtomic(sourcePath, targetPath);
|
|
} else {
|
|
// Directories: use the original move logic
|
|
auto moveOp = [&]() -> bool {
|
|
return MoveFileExW(sourcePath.c_str(), targetPath.c_str(),
|
|
MOVEFILE_REPLACE_EXISTING) != FALSE;
|
|
};
|
|
installOk = RetryFileOp(moveOp, FILE_OP_MAX_RETRIES, FILE_OP_INITIAL_DELAY_MS);
|
|
}
|
|
|
|
if (!installOk) {
|
|
failureReason = L"Failed to move file into place: " + sourcePath;
|
|
WriteLog(failureReason.c_str());
|
|
goto apply_failed;
|
|
}
|
|
|
|
AdvanceProgressUi(
|
|
L"正在安装新文件... | Installing new files...",
|
|
rel);
|
|
}
|
|
|
|
// ---- Cleanup package ----
|
|
if (PathExistsW(packagePath)) {
|
|
DeleteFileW(packagePath.c_str());
|
|
WriteLog((L"Deleted update package: " + packagePath).c_str());
|
|
}
|
|
|
|
if (PathExistsW(failureStatusFile))
|
|
DeleteFileW(failureStatusFile.c_str());
|
|
|
|
SetProgressUiStatus(
|
|
L"正在完成更新... | Finalizing update...",
|
|
L"正在写入更新结果 | Writing update result");
|
|
if (WriteUtf8File(successStatusFile, "succeeded")) {
|
|
WriteLog((L"Wrote success status file: " + successStatusFile).c_str());
|
|
success = true;
|
|
shouldRelaunch = true;
|
|
} else {
|
|
failureReason = L"Failed to write success status file: " + successStatusFile;
|
|
WriteLog(failureReason.c_str());
|
|
}
|
|
|
|
break; // normal exit from do-while
|
|
|
|
apply_failed:
|
|
success = false;
|
|
|
|
// Attempt rollback: restore files that were already backed up
|
|
WriteLog(L"Update failed, attempting rollback from backup directory.");
|
|
for (const std::wstring& rel : removeList) {
|
|
std::wstring targetPath, backupPath;
|
|
if (!TryResolvePathUnderRoot(rootDir, rel, targetPath) ||
|
|
!TryResolvePathUnderRoot(backupDir, rel, backupPath)) {
|
|
continue;
|
|
}
|
|
if (PathExistsW(backupPath) && !PathExistsW(targetPath)) {
|
|
WriteLog((L"Rollback: restoring " + backupPath + L" -> " + targetPath).c_str());
|
|
MovePathEntry(backupPath, targetPath);
|
|
}
|
|
}
|
|
for (const std::wstring& rel : moveList) {
|
|
std::wstring targetPath, backupPath;
|
|
if (!TryResolvePathUnderRoot(rootDir, rel, targetPath) ||
|
|
!TryResolvePathUnderRoot(backupDir, rel, backupPath)) {
|
|
continue;
|
|
}
|
|
if (PathExistsW(backupPath) && !PathExistsW(targetPath)) {
|
|
WriteLog((L"Rollback: restoring " + backupPath + L" -> " + targetPath).c_str());
|
|
MovePathEntry(backupPath, targetPath);
|
|
}
|
|
}
|
|
} while (false);
|
|
|
|
// ------------------------------------------------------------------
|
|
// On failure: write failure status
|
|
// ------------------------------------------------------------------
|
|
if (!success && !failureReason.empty()) {
|
|
// Convert wstring reason to UTF-8 for file
|
|
std::string utf8Reason;
|
|
if (TryConvertWideToUtf8(failureReason, utf8Reason)) {
|
|
WriteUtf8File(failureStatusFile, utf8Reason);
|
|
}
|
|
if (PathExistsW(successStatusFile))
|
|
DeleteFileW(successStatusFile.c_str());
|
|
WriteLog((L"Update failed: " + failureReason).c_str());
|
|
ShowProgressUiFailure(failureReason);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Cleanup extract dir and plan file
|
|
// ------------------------------------------------------------------
|
|
if (PathExistsW(extractDir)) {
|
|
WriteLog((L"Cleaning extract directory: " + extractDir).c_str());
|
|
ForceRemoveDirectoryRecursive(extractDir);
|
|
}
|
|
|
|
if (PathExistsW(planFile))
|
|
DeleteFileW(planFile.c_str());
|
|
|
|
// ------------------------------------------------------------------
|
|
// Release update mutex
|
|
// ------------------------------------------------------------------
|
|
if (hUpdateMutex != nullptr) {
|
|
ReleaseUpdateMutex(hUpdateMutex);
|
|
hUpdateMutex = nullptr;
|
|
WriteLog(L"Update mutex released.");
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Relaunch MAA
|
|
// ------------------------------------------------------------------
|
|
if (shouldRelaunch && PathExistsW(relaunchExecutable)) {
|
|
CompleteProgressUi(
|
|
L"更新完成 | Update completed",
|
|
L"正在重新启动 MAA... | Relaunching MAA...");
|
|
WriteLog((L"Relaunching MAA: " + relaunchExecutable).c_str());
|
|
|
|
// Find working directory (parent of the executable)
|
|
std::wstring workDir = relaunchExecutable;
|
|
size_t sep = workDir.rfind(L'\\');
|
|
if (sep != std::wstring::npos) workDir = workDir.substr(0, sep);
|
|
|
|
STARTUPINFOW si {};
|
|
si.cb = sizeof(si);
|
|
PROCESS_INFORMATION pi {};
|
|
std::wstring cmdLine = L"\"" + relaunchExecutable + L"\"";
|
|
if (CreateProcessW(
|
|
relaunchExecutable.c_str(),
|
|
cmdLine.data(),
|
|
nullptr, nullptr, FALSE, 0, nullptr,
|
|
workDir.c_str(),
|
|
&si, &pi))
|
|
{
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
WriteLog(L"Relaunch succeeded.");
|
|
} else {
|
|
WriteLog((L"Relaunch failed, error=" + std::to_wstring(GetLastError())).c_str());
|
|
ShowProgressUiFailure(
|
|
L"更新已完成,但重新启动 MAA 失败,请手动启动 MAA。\n"
|
|
L"Update finished, but failed to relaunch MAA. Please start MAA manually.");
|
|
}
|
|
} else if (success) {
|
|
CompleteProgressUi(
|
|
L"更新完成 | Update completed",
|
|
L"更新已完成,请手动启动 MAA。 | Update completed. Please start MAA manually.");
|
|
}
|
|
|
|
WriteLog(L"MAA.Updater exiting.");
|
|
DestroyProgressUi();
|
|
return success ? 0 : 2;
|
|
}
|