From 60b108a6b7c9733c4360538835efb2f865689ffd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 7 Oct 2019 17:45:54 -0400 Subject: [PATCH] Rewrite SelfPatcher to include the ability to install updates. To register an update for install, there are three options: - List any executable or msi file in the [External|Internal]Patcher manifest. - As above, but in the *optional* DependencyPatcher manifest. - Flag a file with the bit flag 0x10 in either of the above manifests. This also fixes a bug that caused the status thread to deadlock in certain situations, causing the launcher to get stuck open. (cherry picked from commit e0e918084395f93170abcea2853ad25ae3012385) --- .../Plasma/Apps/plUruLauncher/Main.cpp | 82 +- .../Sources/Plasma/Apps/plUruLauncher/Pch.h | 1 + .../Plasma/Apps/plUruLauncher/SelfPatcher.cpp | 911 ++++++++++++++---- .../Apps/plUruLauncher/plLauncherInfo.h | 1 + 4 files changed, 788 insertions(+), 207 deletions(-) diff --git a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Main.cpp b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Main.cpp index 4f2d3ef0..313b5a26 100644 --- a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Main.cpp +++ b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Main.cpp @@ -157,13 +157,12 @@ static long s_terminationIssued; static bool s_terminated; static plLauncherInfo s_launcherInfo; static HANDLE s_thread; -static HANDLE s_event; +static CEvent s_shutdownDesiredEvent(kEventManualReset); static HINSTANCE s_hInstance; -static HWND s_dialog; static CEvent s_dialogCreateEvent(kEventManualReset); static CCritSect s_critsect; static LISTDECL(WndEvent, link) s_eventQ; -static CEvent s_shutdownEvent(kEventManualReset); +static CEvent s_shutdownDialogEvent(kEventManualReset); static wchar s_workingDir[MAX_PATH]; static CEvent s_statusEvent(kEventManualReset); @@ -311,7 +310,7 @@ static void TerminateGame () { //============================================================================ static void Recv_SetProgress (HWND hwnd, const SetProgressEvent &event) { - SendMessage(GetDlgItem(s_dialog, IDC_PROGRESS), PBM_SETPOS, event.progress, NULL); + SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_PROGRESS), PBM_SETPOS, event.progress, NULL); if (pTGApp) { @@ -329,7 +328,7 @@ static void Recv_SetProgress (HWND hwnd, const SetProgressEvent &event) { //============================================================================ static void Recv_SetText (HWND hwnd, const SetTextEvent &event) { - bool b = SendMessage(GetDlgItem(s_dialog, IDC_TEXT), WM_SETTEXT, 0, (LPARAM) event.text); + bool b = SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_TEXT), WM_SETTEXT, 0, (LPARAM) event.text); if (pTGApp) { @@ -347,7 +346,7 @@ static void Recv_SetText (HWND hwnd, const SetTextEvent &event) { //============================================================================ static void Recv_SetStatusText (HWND hwnd, const SetStatusTextEvent &event) { - bool b = SendMessage(GetDlgItem(s_dialog, IDC_STATUS_TEXT), WM_SETTEXT, 0, (LPARAM) event.text); + bool b = SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_STATUS_TEXT), WM_SETTEXT, 0, (LPARAM) event.text); } //============================================================================ @@ -359,7 +358,7 @@ static void Recv_SetTimeRemaining (HWND hwnd, const SetTimeRemainingEvent &event if(event.seconds == 0xffffffff) { - SendMessage(GetDlgItem(s_dialog, IDC_TIMEREMAINING), WM_SETTEXT, 0, (LPARAM) "estimating..."); + SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_TIMEREMAINING), WM_SETTEXT, 0, (LPARAM) "..."); return; } @@ -392,7 +391,7 @@ static void Recv_SetTimeRemaining (HWND hwnd, const SetTimeRemainingEvent &event StrPrintf(text, arrsize(text), "%s%d min ", text, minutes); if( seconds || !text[0]) StrPrintf(text, arrsize(text), "%s%d sec", text, seconds); - bool b = SendMessage(GetDlgItem(s_dialog, IDC_TIMEREMAINING), WM_SETTEXT, 0, (LPARAM) text); + bool b = SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_TIMEREMAINING), WM_SETTEXT, 0, (LPARAM) text); } //============================================================================ @@ -402,6 +401,11 @@ static void Recv_SetBytesRemaining (HWND hwnd, const SetBytesRemainingEvent &eve unsigned decimal; unsigned bytes = event.bytes; + if (bytes == 0xffffffff) { + SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_BYTESREMAINING), WM_SETTEXT, 0, (LPARAM) "..."); + return; + } + unsigned GB = bytes / 1000000000; if(GB) { @@ -416,7 +420,7 @@ static void Recv_SetBytesRemaining (HWND hwnd, const SetBytesRemainingEvent &eve decimal = bytes / 100000; // to one decimal place StrPrintf(text, arrsize(text), "%d.%d MB", MB, decimal); } - bool b = SendMessage(GetDlgItem(s_dialog, IDC_BYTESREMAINING), WM_SETTEXT, 0, (LPARAM) text); + bool b = SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_BYTESREMAINING), WM_SETTEXT, 0, (LPARAM) text); } //============================================================================ @@ -462,19 +466,19 @@ static void MessagePump (HWND hwnd) { // wait for a message or the shutdown event const DWORD result = MsgWaitForMultipleObjects( 1, - &s_event, + &s_shutdownDesiredEvent.Handle(), false, INFINITE, QS_ALLEVENTS ); - if (result == WAIT_OBJECT_0) - return; + if (result == WAIT_OBJECT_0) + PostQuitMessage(0); // process windows messages MSG msg; while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { - if (!IsDialogMessage(s_dialog, &msg)) { + if (!IsDialogMessage(s_launcherInfo.dialog, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } @@ -503,8 +507,8 @@ BOOL CALLBACK SplashDialogProc( HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM l if(!s_shutdown) { s_shutdown = true; - SendMessage(GetDlgItem(s_dialog, IDC_TEXT), WM_SETTEXT, 0, (LPARAM) "Shutting Down..."); - EnableWindow(GetDlgItem(s_dialog, IDCANCEL), false); + SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_TEXT), WM_SETTEXT, 0, (LPARAM) "Shutting Down..."); + EnableWindow(GetDlgItem(s_launcherInfo.dialog, IDCANCEL), false); } } break; @@ -538,12 +542,6 @@ BOOL CALLBACK SplashDialogProc( HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM l static void WindowThreadProc(void *) { InitCommonControls(); - s_event = CreateEvent( - (LPSECURITY_ATTRIBUTES) 0, - false, // auto reset - false, // initial state off - (LPCTSTR) 0 // name - ); if (TGIsCider) { @@ -553,22 +551,22 @@ static void WindowThreadProc(void *) { pTGApp = pTGLaunchUNIXApp (TG_OLD_DIALOG_POPEN_PATH, "w"); } - s_dialog = ::CreateDialog( s_hInstance, MAKEINTRESOURCE( IDD_DIALOG ), NULL, SplashDialogProc ); - SetWindowText(s_dialog, "URU Launcher"); + s_launcherInfo.dialog = ::CreateDialog( s_hInstance, MAKEINTRESOURCE( IDD_DIALOG ), NULL, SplashDialogProc ); + SetWindowText(s_launcherInfo.dialog, "URU Launcher"); - ::SetDlgItemText( s_dialog, IDC_TEXT, "Initializing patcher..."); - SetTimer(s_dialog, kEventTimer, 250, 0); + ::SetDlgItemText( s_launcherInfo.dialog, IDC_TEXT, "Initializing patcher..."); + SetTimer(s_launcherInfo.dialog, kEventTimer, 250, 0); char productString[256]; wchar productStringW[256]; ProductString(productStringW, arrsize(productStringW)); StrToAnsi(productString, productStringW, arrsize(productString)); - SendMessage(GetDlgItem(s_dialog, IDC_PRODUCTSTRING), WM_SETTEXT, 0, (LPARAM) productString); + SendMessage(GetDlgItem(s_launcherInfo.dialog, IDC_PRODUCTSTRING), WM_SETTEXT, 0, (LPARAM) productString); s_dialogCreateEvent.Signal(); - MessagePump(s_dialog); + MessagePump(s_launcherInfo.dialog); if (pTGApp) { @@ -577,9 +575,9 @@ static void WindowThreadProc(void *) { pTGApp = NULL; } - s_dialog = 0; + s_launcherInfo.dialog = 0; s_shutdown = true; - s_shutdownEvent.Signal(); + s_shutdownDialogEvent.Signal(); } //============================================================================ @@ -712,8 +710,12 @@ static void StatusCallback(void *) HINTERNET hConnect = 0; // update while we are running - while(!s_shutdown) + do { + DWORD result = WaitForSingleObject(s_shutdownDesiredEvent.Handle(), UPDATE_STATUSMSG_SECONDS * 1000); + if (result == WAIT_OBJECT_0) + break; + if(BuildTypeServerStatusPath()) { hSession = WinHttpOpen( @@ -744,12 +746,7 @@ static void StatusCallback(void *) WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); } - - for(unsigned i = 0; i < UPDATE_STATUSMSG_SECONDS && !s_shutdown; ++i) - { - Sleep(1000); - } - } + } while (!s_shutdown); s_statusEvent.Signal(); } @@ -1062,16 +1059,17 @@ int __stdcall WinMain ( } ShutdownAsyncCore(); + + + // Signal teardown of our junk and stuff. + s_shutdownDesiredEvent.Signal(); + + // Wait for the hwnd and status event to shutdown s_statusEvent.Wait(kEventWaitForever); - - PostMessage(s_dialog, WM_QUIT, 0, 0); // tell our window to shutdown - s_shutdownEvent.Wait(kEventWaitForever); // wait for our window to shutdown + s_shutdownDialogEvent.Wait(kEventWaitForever); SetConsoleCtrlHandler(CtrlHandler, FALSE); - if (s_event) - CloseHandle(s_event); - s_eventQ.Clear(); break; } diff --git a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Pch.h b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Pch.h index 114bded2..b51c36e4 100644 --- a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Pch.h +++ b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/Pch.h @@ -52,6 +52,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include #include +#include "hsThread.h" #include "pnUtils/pnUtils.h" #include "pnNetBase/pnNetBase.h" #include "pnAsyncCore/pnAsyncCore.h" diff --git a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/SelfPatcher.cpp b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/SelfPatcher.cpp index 8c2f46cf..8b0721a7 100644 --- a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/SelfPatcher.cpp +++ b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/SelfPatcher.cpp @@ -48,6 +48,11 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "Pch.h" #pragma hdrstop +#ifndef SEE_MASK_NOASYNC +#define SEE_MASK_NOASYNC 0x00000100 +#endif + +#define PATCHER_FLAG_INSTALLER 0x10 /***************************************************************************** * @@ -61,206 +66,182 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com static const wchar s_manifest[] = L"ExternalPatcher"; #endif +static const wchar s_depManifest[] = L"DependencyPatcher"; + class SelfPatcherStream : public plZlibStream { - public: +public: + SelfPatcherStream(); virtual UInt32 Write(UInt32 byteCount, const void* buffer); - static plLauncherInfo *info; static unsigned totalBytes; static unsigned progress; + static DWORD startTime; }; unsigned SelfPatcherStream::totalBytes = 0; unsigned SelfPatcherStream::progress = 0; +DWORD SelfPatcherStream::startTime = 0; -static bool s_downloadComplete; -static long s_numFiles; -static ENetError s_patchResult; -static bool s_updated; -static wchar s_newPatcherFile[MAX_PATH]; +//============================================================================ +class plSelfPatcher : public hsThread +{ + enum RequestType + { + kUndefined = -1, + kQuit, + + kRequestManifest, + kHash, + kDownload, + kVerify, + kInstall, + }; + + enum RequestFlags + { + kRequestBlocked = (1<<0), + kRequestOptionalManifest = (1<<1), + kRequestNewPatcher = (1<<2), + }; + + class PatcherWork + { + public: + LINK(PatcherWork) link; + + RequestType fType; + UInt32 fFlags; + union + { + NetCliFileManifestEntry fEntry; + wchar fFileName[MAX_PATH]; + }; + + PatcherWork() : fType(kUndefined), fFlags(0) { } + PatcherWork(RequestType type, const PatcherWork* cpy) + : fType(type), fFlags(0) + { + memcpy(&fEntry, &cpy->fEntry, sizeof(fEntry)); + } + }; + LISTDECL(PatcherWork, link) fReqs; + CEvent fQueueEvent; + CCritSect fMutex; + ENetError fResult; + wchar fNewPatcherFileName[MAX_PATH]; + UInt32 fInstallerCount; + UInt32 fInstallersExecuted; -/***************************************************************************** -* -* Private Functions -* -***/ + // Any thread + void IEnqueueFile(const NetCliFileManifestEntry& file); + void IEnqueueWork(PatcherWork*& wk, bool priority=false); + void IDequeueWork(PatcherWork*& wk); + void IFatalError(const wchar* msg); + void IReportServerBusy(); -//============================================================================ -static void NetErrorHandler (ENetProtocol protocol, ENetError error) { - REF(protocol); - LogMsg(kLogError, L"NetErr: %s", NetErrorToString(error)); - if (IS_NET_SUCCESS(s_patchResult)) - s_patchResult = error; - s_downloadComplete = true; - - switch(error) { - case kNetErrServerBusy: - MessageBox(0, "Due to the high demand, the server is currently busy. Please try again later, or for alternative download options visit: http://www.mystonline.com/play/", "UruLauncher", MB_OK); - s_patchResult = kNetErrServerBusy; - s_downloadComplete = true; - break; - } -} + // This worker thread + void ICheckAndRequest(PatcherWork*& wk); + void IDownloadFile(PatcherWork*& wk); + void IVerifyFile(PatcherWork*& wk); + void IIssueManifestRequest(PatcherWork*& wk); + HANDLE ICreateProcess(const wchar* path, const wchar* args, bool forceShell=false) const; + void IInstallDep(PatcherWork*& wk); + DWORD IWaitProcess(HANDLE hProcess); -//============================================================================ -static void DownloadCallback ( - ENetError result, - void * param, - const wchar filename[], - hsStream * writer -) { - REF(param); - REF(filename); - - if(IS_NET_ERROR(result)) { - switch (result) { - case kNetErrTimeout: - writer->Rewind(); - NetCliFileDownloadRequest(filename, writer, DownloadCallback, param); - break; - - default: - LogMsg(kLogError, L"Error getting patcher file: %s", NetErrorToString(result)); - if (IS_NET_SUCCESS(s_patchResult)) - s_patchResult = result; - break; - } - return; - } + void IRun(); + void IQuit(); - writer->Close(); - delete writer; - AtomicAdd(&s_numFiles, -1); +public: + plSelfPatcher(); - if(!s_numFiles) { - s_downloadComplete = true; - s_updated = true; - } -} + bool Active() const { return GetQuit() == 0; } + const wchar* GetNewPatcherFileName() const { return fNewPatcherFileName; } + ENetError GetResult() const { return fResult; } -//============================================================================ -static bool MD5Check (const char filename[], const wchar md5[]) { - // Do md5 check - char md5copy[MAX_PATH]; - plMD5Checksum existingMD5(filename); - plMD5Checksum latestMD5; - - StrToAnsi(md5copy, md5, arrsize(md5copy)); - latestMD5.SetFromHexString(md5copy); - return (existingMD5 == latestMD5); -} + void IssueManifestRequests(); -//============================================================================ -static void ManifestCallback ( - ENetError result, - void * param, - const wchar group[], - const NetCliFileManifestEntry manifest[], - unsigned entryCount -) { - REF(param); - REF(group); - - if(IS_NET_ERROR(result)) { - switch (result) { - case kNetErrTimeout: - NetCliFileManifestRequest(ManifestCallback, nil, s_manifest); - break; - - default: - LogMsg(kLogError, L"Error getting patcher manifest: %s", NetErrorToString(result)); - if (IS_NET_SUCCESS(s_patchResult)) - s_patchResult = result; - break; - } - return; - } + void Start(); // override; + hsError Run(); // override; + void Stop(); // override; - char ansi[MAX_PATH]; +public: + plLauncherInfo* fLauncherInfo; - // MD5 check current patcher against value in manifest - ASSERT(entryCount == 1); - wchar curPatcherFile[MAX_PATH]; - PathGetProgramName(curPatcherFile, arrsize(curPatcherFile)); - StrToAnsi(ansi, curPatcherFile, arrsize(ansi)); - if (!MD5Check(ansi, manifest[0].md5)) { -// MessageBox(GetTopWindow(nil), "MD5 failed", "Msg", MB_OK); - SelfPatcherStream::totalBytes += manifest[0].zipSize; +public: + // NetCli callbacks + static void NetErrorHandler(ENetProtocol protocol, ENetError error); + static void OnFileSrvIP(ENetError result, void* param, const wchar addr[]); + static void OnFileSrvManifest(ENetError result, void* param, const wchar group[], const NetCliFileManifestEntry manifest[], unsigned entryCount); + static void OnFileSrvDownload(ENetError result, void* param, const wchar filename[], hsStream* writer); - AtomicAdd(&s_numFiles, 1); - SetText("Downloading new patcher..."); +} s_selfPatcher; - StrToAnsi(ansi, s_newPatcherFile, arrsize(ansi)); - SelfPatcherStream * stream = NEWZERO(SelfPatcherStream); - if (!stream->Open(ansi, "wb")) - ErrorFatal(__LINE__, __FILE__, "Failed to create file: %s, errno: %u", ansi, errno); - NetCliFileDownloadRequest(manifest[0].downloadName, stream, DownloadCallback, nil); - } - else { - s_downloadComplete = true; - } -} +/***************************************************************************** +* +* Private Functions +* +***/ //============================================================================ -static void FileSrvIpAddressCallback ( - ENetError result, - void * param, - const wchar addr[] -) { - REF(param); - - NetCliGateKeeperDisconnect(); - - if (IS_NET_ERROR(result)) { - LogMsg(kLogDebug, L"FileSrvIpAddressRequest failed: %s", NetErrorToString(result)); - s_patchResult = result; - s_downloadComplete = true; - } - - // Start connecting to the server - NetCliFileStartConnect(&addr, 1, true); +static bool CheckMD5(const wchar* path, const wchar* hash) +{ + plMD5Checksum localMD5; + plMD5Checksum remoteMD5; + + hsUNIXStream s; + s.Open(path); + localMD5.CalcFromStream(&s); + s.Close(); + + // Some silly goose decided to send an md5 hash as UCS-2 instead of ASCII + char ansi[33]; + StrToAnsi(ansi, hash, arrsize(ansi)); + remoteMD5.SetFromHexString(ansi); + + return localMD5 == remoteMD5; +} - PathGetProgramDirectory(s_newPatcherFile, arrsize(s_newPatcherFile)); - GetTempFileNameW(s_newPatcherFile, kPatcherExeFilename, 0, s_newPatcherFile); - PathDeleteFile(s_newPatcherFile); +//============================================================================ +static wchar* FormatSystemError() +{ + wchar* error; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)& error, + 0, + NULL); + return error; +} - NetCliFileManifestRequest(ManifestCallback, nil, s_manifest); +//============================================================================ +static bool IsPatcherFile(const wchar* filename) +{ + wchar progPath[MAX_PATH]; + PathGetProgramName(progPath, arrsize(progPath)); + wchar* progFilename = PathFindFilename(progPath); + return StrCmpI(filename, progFilename) == 0; } //============================================================================ static bool SelfPatcherProc (bool * abort, plLauncherInfo *info) { bool patched = false; - s_downloadComplete = false; - s_patchResult = kNetSuccess; - NetClientInitialize(); - NetClientSetErrorHandler(NetErrorHandler); - - const wchar ** addrs; - unsigned count; - - count = GetGateKeeperSrvHostnames(&addrs); - - // Start connecting to the server - NetCliGateKeeperStartConnect(addrs, count); - - // request a file server ip address - NetCliGateKeeperFileSrvIpAddressRequest(FileSrvIpAddressCallback, nil, true); - - while(!s_downloadComplete && !*abort) { + s_selfPatcher.fLauncherInfo = info; + s_selfPatcher.Start(); + while(s_selfPatcher.Active() && !*abort) { NetClientUpdate(); AsyncSleep(10); } + s_selfPatcher.Stop(); - NetCliFileDisconnect(); - NetClientUpdate(); - - // Shutdown the client/server networking subsystem - NetClientDestroy(); + if (s_selfPatcher.GetResult() == kNetPending) + *abort = true; - if (s_downloadComplete && !*abort && s_updated && IS_NET_SUCCESS(s_patchResult)) { + if (!*abort && *s_selfPatcher.GetNewPatcherFileName() && IS_NET_SUCCESS(s_selfPatcher.GetResult())) { // launch new patcher STARTUPINFOW si; @@ -270,7 +251,7 @@ static bool SelfPatcherProc (bool * abort, plLauncherInfo *info) { si.cb = sizeof(si); wchar cmdline[MAX_PATH]; - StrPrintf(cmdline, arrsize(cmdline), L"%s %s", s_newPatcherFile, info->cmdLine); + StrPrintf(cmdline, arrsize(cmdline), L"%s %s", s_selfPatcher.GetNewPatcherFileName(), info->cmdLine); // we have only successfully patched if we actually launch the new version of the patcher patched = CreateProcessW( @@ -301,14 +282,614 @@ static bool SelfPatcherProc (bool * abort, plLauncherInfo *info) { * ***/ +//============================================================================ +SelfPatcherStream::SelfPatcherStream() + : plZlibStream() +{ + if (startTime == 0) + startTime = TimeGetSecondsSince2001Utc(); +} + //============================================================================ UInt32 SelfPatcherStream::Write(UInt32 byteCount, const void* buffer) { progress += byteCount; - float p = (float)progress / (float)totalBytes * 100; // progress + float p = (float)progress / (float)totalBytes * 1000; // progress SetProgress( (int)p ); + + if (progress >= totalBytes) { + SetBytesRemaining(0); + SetTimeRemaining(0); + } else { + SetBytesRemaining(totalBytes - byteCount); + DWORD bytesPerSec = (progress) / max(TimeGetSecondsSince2001Utc() - startTime, 1); + SetTimeRemaining((totalBytes - progress) / max(bytesPerSec, 1)); + } + return plZlibStream::Write(byteCount, buffer); } +/***************************************************************************** +* +* SelfPatcher Methods +* +***/ + +//============================================================================ +plSelfPatcher::plSelfPatcher() + : fQueueEvent(kEventAutoReset), fResult(kNetPending), fLauncherInfo(nil), + fInstallerCount(0), fInstallersExecuted(0) +{ + memset(fNewPatcherFileName, 0, sizeof(fNewPatcherFileName)); +} + +//============================================================================ +void plSelfPatcher::IEnqueueFile(const NetCliFileManifestEntry& file) +{ + LogMsg(kLogDebug, L"plSelfPatcher::IEnqueueFile: Enqueueing hash check of '%s'", file.downloadName); + + PatcherWork* wk = NEW(PatcherWork); + wk->fType = kHash; + memcpy(&wk->fEntry, &file, sizeof(NetCliFileManifestEntry)); + + // Kludge: any EXE we have here that isn't the launcher is clearly an installer. + if (StrCmpI(file.clientName, kPatcherExeFilename) != 0) { + const wchar* extension = PathFindExtension(file.clientName); + if (extension && (StrCmpI(extension, L".exe") == 0 || StrCmpI(extension, L".msi") == 0)) + wk->fEntry.flags |= PATCHER_FLAG_INSTALLER; + } + + IEnqueueWork(wk); +} + +//============================================================================ +void plSelfPatcher::IEnqueueWork(PatcherWork*& wk, bool priority) +{ + fMutex.Enter(); + wk->fFlags &= ~kRequestBlocked; + fReqs.Link(wk, priority ? kListHead : kListTail); + fMutex.Leave(); + fQueueEvent.Signal(); + + // WHY?! You ask? + // If we don't, IRun() will reblock any reused requests. Also, from an ownership standpoint, + // the worker queue now owns the work, not whoever enqueued it. + wk = NULL; +} + +//============================================================================ +void plSelfPatcher::IDequeueWork(PatcherWork*& wk) +{ + ASSERT(wk->link.IsLinked()); + + fMutex.Enter(); + fReqs.Unlink(wk); + fMutex.Leave(); + fQueueEvent.Signal(); + + DEL(wk); + wk = NULL; +} + +//============================================================================ +void plSelfPatcher::IFatalError(const wchar* msg) +{ +#ifdef PLASMA_EXTERNAL_RELEASE + MessageBoxW(NULL, msg, L"URU Launcher", MB_OK | MB_ICONERROR); + IQuit(); +#else + wchar finalmsg[1024]; + StrPrintf(finalmsg, arrsize(finalmsg), L"%s Continue?", msg); + if (MessageBoxW(NULL, finalmsg, L"URU Launcher", MB_YESNO | MB_ICONERROR) == IDNO) { + IQuit(); + } +#endif +} + +//============================================================================ +void plSelfPatcher::IReportServerBusy() +{ + MessageBoxA(NULL, + "Due to the high demand, the server is currently busy. Please try again later, or for alternative download options visit: http://www.mystonline.com/play/", + "URU Launcher", + MB_OK | MB_ICONINFORMATION); + fResult = kNetPending; // Don't show the unhandled error box. + IQuit(); +} + +//============================================================================ +void plSelfPatcher::IssueManifestRequests() +{ + { + PatcherWork* wk = NEW(PatcherWork); + wk->fType = kRequestManifest; + StrCopy(wk->fFileName, s_manifest, arrsize(wk->fFileName)); + IEnqueueWork(wk); + } + + { + PatcherWork* wk = NEW(PatcherWork); + wk->fType = kRequestManifest; + wk->fFlags |= kRequestOptionalManifest; + StrCopy(wk->fFileName, s_depManifest, arrsize(wk->fFileName)); + IEnqueueWork(wk); + } +} + +//============================================================================ +void plSelfPatcher::ICheckAndRequest(PatcherWork*& wk) +{ + // Patcher thread, can be as slow as molasses. + if (PathDoesFileExist(wk->fEntry.clientName)) { + if (CheckMD5(wk->fEntry.clientName, wk->fEntry.md5)) { + LogMsg(kLogDebug, L"plSelfPatcher::ICheckAndRequest: File '%s' appears to be up-to-date.", wk->fEntry.clientName); + IDequeueWork(wk); + return; + } + } + + // New patchers need to be downloaded FIRST, and we want to re-run the entire thing before + // continuing with the process. + bool isPatcher = IsPatcherFile(wk->fEntry.clientName); + if (isPatcher) + wk->fFlags |= kRequestNewPatcher; + + LogMsg(kLogDebug, L"plSelfPatcher::ICheckAndRequest: File '%s' needs to be downloaded.", wk->fEntry.clientName); + wk->fType = kDownload; + IEnqueueWork(wk, isPatcher); +} + +//============================================================================ +void plSelfPatcher::IDownloadFile(PatcherWork*& wk) +{ + // The patcher downloads to a temporary file. + if (wk->fFlags & kRequestNewPatcher) { + PathGetProgramDirectory(fNewPatcherFileName, arrsize(fNewPatcherFileName)); + GetTempFileNameW(fNewPatcherFileName, kPatcherExeFilename, 0, fNewPatcherFileName); + PathDeleteFile(fNewPatcherFileName); + StrCopy(wk->fEntry.clientName, fNewPatcherFileName, arrsize(wk->fEntry.clientName)); + + SetText("Downloading new patcher..."); + LogMsg(kLogDebug, L"plSelfPatcher::IDownloadFile: New patcher will be downloaded as '%s'", fNewPatcherFileName); + } else { + if (wk->fEntry.flags & PATCHER_FLAG_INSTALLER) + SetText("Downloading update installer..."); + else + SetText("Downloading update..."); + } + + SelfPatcherStream::totalBytes += (wk->fEntry.zipSize != 0) ? wk->fEntry.zipSize : wk->fEntry.fileSize; + SelfPatcherStream* s = NEWZERO(SelfPatcherStream); + if (!s->Open(wk->fEntry.clientName, L"wb")) { + LogMsg(kLogError, L"plSelfPatcher::IDownloadFile: Failed to create file: %s, errno: %u", wk->fEntry.clientName, errno); + IFatalError(L"Failed to create file."); + } else { + LogMsg(kLogDebug, L"plSelfPatcher::IDownloadFile: Downloading file '%s'.", wk->fEntry.downloadName); + NetCliFileDownloadRequest(wk->fEntry.downloadName, s, OnFileSrvDownload, wk); + } +} + +//============================================================================ +void plSelfPatcher::IVerifyFile(PatcherWork*& wk) +{ + if (!CheckMD5(wk->fEntry.clientName, wk->fEntry.md5)) { + LogMsg(kLogError, L"plSelfPatcher::IVerifyFile: Hash mismatch on file '%s'. Expected: %s", + wk->fEntry.clientName, wk->fEntry.md5); + IFatalError(L"File download verification failed."); + return; + } + + // If this is a redistributable dependency, it needs to be installed. + if (wk->fEntry.flags & PATCHER_FLAG_INSTALLER) { + LogMsg(kLogPerf, L"plSelfPatcher::IVerifyFile: Downloaded valid dependency installer '%s'", wk->fEntry.clientName); + s_selfPatcher.fInstallerCount++; + + wk->fType = kInstall; + s_selfPatcher.IEnqueueWork(wk); + } else if (wk->fFlags & kRequestNewPatcher) { + LogMsg(kLogPerf, L"plSelfPatcher::IVerifyFile: Downloaded a new patcher! '%s'", wk->fEntry.clientName); + // Need to restart here w/new patcher. + fResult = kNetSuccess; + IQuit(); + } else { + LogMsg(kLogPerf, L"plSelfPatcher::IVerifyFile: Downloaded valid standard file '%s'", wk->fEntry.clientName); + s_selfPatcher.IDequeueWork(wk); + } +} + +//============================================================================ +void plSelfPatcher::IIssueManifestRequest(PatcherWork*& wk) +{ + LogMsg(kLogDebug, L"plSelfPatcher::IIssueManifestRequest: Issuing manifest request '%s'.", wk->fFileName); + NetCliFileManifestRequest(OnFileSrvManifest, wk, wk->fFileName); +} + +//============================================================================ +HANDLE plSelfPatcher::ICreateProcess(const wchar* path, const wchar* args, bool forceShell) const +{ + // Generally speaking, we would *like* to use CreateProcessW. Unfortunately, we cannot do that + // becuase on Windows Vista and above (read: what the world SHOULD be using...) CreateProcessW + // will not handle UAC split tokens and can fail with ERROR_ELEVATION_REQUIRED. For bonus fun, + // that error isn't even defined in the Platform SDK we're using here. The "official" solution + // is to use ShellExecuteEx with the verb "runas" so there you go. (See also: "runas.exe") + // Bonus chatter: on Windows XP, there is no "runas" feature because there are no split tokens + // or UAC. Also, Windows XP does not have the SEE_MASK_NOASYNC flag (it does have the DDEWAIT flag + // whose value is the same but functions slightly differently), which causes any dialogs + // launched by Windows (such as errors) to deadlock the UI quite horribly. Further, + // ShellExecuteExW pops up that weird "do you want to run this file you downloaded from the internet?" + // box, which we can't actually interact with due to the above. + + wchar exePath[MAX_PATH]; + PathGetCurrentDirectory(exePath, arrsize(exePath)); + PathAddFilename(exePath, exePath, path, arrsize(exePath)); + if (!forceShell) { + STARTUPINFOW si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + si.cb = sizeof(si); + + wchar cmdline[MAX_PATH]; + const wchar* exeFilename = PathFindFilename(path); + StrPrintf(cmdline, arrsize(cmdline), L"%s %s", exeFilename, args); + BOOL result = CreateProcessW(exePath, + cmdline, + NULL, + NULL, + FALSE, + DETACHED_PROCESS, + NULL, + NULL, + &si, + &pi); + CloseHandle(pi.hThread); + if (result != FALSE) { + free(exePath); + return pi.hProcess; + } else { + wchar* error = FormatSystemError(); + LogMsg(kLogError, L"plSelfPatcher::ICreateProcess: CreateProcessW failed for '%s': %u %s", + exePath, GetLastError(), error); + LocalFree(error); + // Purposefully falling through to ShellExecuteExW + } + } + + SHELLEXECUTEINFOW info; + memset(&info, 0, sizeof(info)); + info.cbSize = sizeof(info); + info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI; + info.hwnd = fLauncherInfo->dialog; + // Not explicitly setting lpVerb to L"runas" because this seemingly breaks msiexec. + info.lpFile = exePath; + info.lpParameters = args; + + if (ShellExecuteExW(&info) == FALSE) { + wchar* error = FormatSystemError(); + LogMsg(kLogError, L"plSelfPatcher::ICreateProcess: ShellExecuteExW failed for '%s': %u %s", + exePath, GetLastError(), error); + LocalFree(error); + } + free(exePath); + return info.hProcess; +} + +//============================================================================ +void plSelfPatcher::IInstallDep(PatcherWork*& wk) +{ + // Due to our dependence on Visual Studio .NET 2003, we cannot use the indeterminate/marquee + // progress bar from there. So, we'll have to dome some skullduggery to guesstimate a really + // crummy progress meter. + fInstallersExecuted++; + float progress = (float)fInstallersExecuted / ((float)fInstallerCount + 1.f) * 1000.f; + SetProgress((unsigned)progress); + + // Best I can do for indeterminant progress. + SetTimeRemaining(-1); + SetBytesRemaining(-1); + + // We are about to do something that MAY cause a UAC dialog to appear. + // So, let's at least pretend to be a good citizen and write something in the UI about that... + SetText("Installing updates..."); + AsyncSleep(100); + + bool forceShell = false; + wchar* extension = PathFindExtension(wk->fFileName); + wchar* process; + wchar args[MAX_PATH]; + args[0] = 0; + + // Apply arguments to the process to ensure it doesn't do weird stuff like start a big UI + // Creative OpenAL (oalinst.exe) uses '/s' for silent. + // The small DirectX 9.0c web installer (dxwebsetup.exe) uses "/q" and pops up an error on invalid args. + // The full monty DirectX 9.0c isntaller (dxsetup.exe) uses "/silent" and pops up an error on invalid args. + // The Visual C++ redist (vcredist_x86.exe and vcredist_x64.exe) may optionally restart the + // computer WITHOUT prompting when in quiet mode. + if (extension && StrCmpI(extension, L".exe") == 0) { + wchar* filename = PathFindFilename(wk->fFileName); + if (StrCmpI(filename, L"oalinst.exe") == 0) + StrPack(args, L"/s", arrsize(args)); + else if (StrCmpI(filename, L"dxsetup.exe") == 0) + StrPack(args, L"/silent", arrsize(args)); + else + StrPack(args, L"/q", arrsize(args)); + + if (StrStrI(filename, L"vcredist")) + StrPack(args, L" /norestart", arrsize(args)); + process = wk->fFileName; + } else if (extension && StrCmpI(extension, L".msi") == 0) { + StrPrintf(args, arrsize(args), L"/i %s /qr /norestart", wk->fFileName); + process = L"msiexec"; + forceShell = true; + } else { + LogMsg(kLogError, L"plSelfPatcher::IInstallDep: Invalid extension '%s' for installer '%s'", + extension ? extension : L"(NULL)", wk->fFileName); + IDequeueWork(wk); + return; + } + + LogMsg(kLogDebug, L"plSelfPatcher::IInstallDep: Installing '%s %s'.", process, args); + HANDLE hProcess = ICreateProcess(process, args, forceShell); + if (hProcess) { + if (IWaitProcess(hProcess) != ERROR_SUCCESS) + PathDeleteFile(wk->fFileName); + CloseHandle(hProcess); + IDequeueWork(wk); + } else { + IFatalError(L"Failed to run installer."); + } +} + +//============================================================================ +DWORD plSelfPatcher::IWaitProcess(HANDLE hProcess) +{ + DWORD returncode = ERROR_SUCCESS; + + // Since we have taken over the worker thread, we need to listen for any very very important + // requests added to the queue. The only one we care about is quit, the rest can just go to + // HEY HEY! and we're safe to just swallow the notifies. We delete our own request to resume + // the main proc. + enum { kWaitQueue, kWaitProcess }; + HANDLE waitH[] = { fQueueEvent.Handle(), hProcess }; + do { + DWORD waitStatus = WaitForMultipleObjects(arrsize(waitH), waitH, FALSE, INFINITE); + ASSERT(waitStatus != WAIT_FAILED); + + if (waitStatus >= WAIT_OBJECT_0 && waitStatus <= (WAIT_OBJECT_0 + arrsize(waitH))) { + DWORD idx = waitStatus - WAIT_OBJECT_0; + if (idx == kWaitQueue) { + fMutex.Enter(); + PatcherWork* quitWk = fReqs.Head(); + fMutex.Leave(); + if (quitWk->fType == kQuit) { + LogMsg(kLogPerf, "plSelfPatcher::IWaitProcess: Got shutdown during wait, attempting to terminate process."); + TerminateProcess(hProcess, 1); + returncode = -1; + break; + } + } else if (idx == kWaitProcess) { + GetExitCodeProcess(hProcess, &returncode); + switch (returncode) { + case ERROR_SUCCESS: + case ERROR_PRODUCT_VERSION: // It's already installed... + case ERROR_SUCCESS_REBOOT_REQUIRED: // LMFTFY s/REQUIRED/DESIRED/ + case ERROR_SUCCESS_RESTART_REQUIRED: + LogMsg(kLogDebug, "plSelfPatcher::IWaitProcess: Process finished successfully!"); + returncode = ERROR_SUCCESS; // makes life easier for us. + break; + default: + LogMsg(kLogError, "plSelfPatcher::IWaitProcess: Process failed! Returncode: %u", returncode); + IFatalError(L"Failed to install update."); + break; + } + break; + } else { + FATAL("Invalid wait index"); + } + } else if (waitStatus == WAIT_FAILED) { + wchar* error = FormatSystemError(); + LogMsg(kLogError, "plSelfPatcher::IWaitProcess: WaitForMultipleObjects failed! %s", error); + LocalFree(error); + IFatalError(L"Internal Error."); + returncode = -1; + break; + } + + AsyncSleep(10); + } while (1); + + return returncode; +} + +//============================================================================ +hsError plSelfPatcher::Run() +{ + do { + fQueueEvent.Wait(-1); + IRun(); + } while (Active()); + return hsOK; +} + +//============================================================================ +void plSelfPatcher::IRun() +{ + do { + fMutex.Enter(); + PatcherWork* wk = fReqs.Head(); + fMutex.Leave(); + + if (!wk) { + LogMsg(kLogDebug, "plSelfPatcher::IRun: No work in queue, exiting."); + if (!IS_NET_ERROR(fResult)) + fResult = kNetSuccess; + SetQuit(1); + return; + } + + if (wk->fFlags & kRequestBlocked) + return; + + switch (wk->fType) { + case kQuit: + LogMsg(kLogDebug, "plSelfPatcher::IRun: Explicit quit request."); + // An explicit quit should manage its own result code. + SetQuit(1); + return; + + case kRequestManifest: + IIssueManifestRequest(wk); + break; + case kHash: + ICheckAndRequest(wk); + break; + case kDownload: + IDownloadFile(wk); + break; + case kInstall: + IInstallDep(wk); + break; + case kVerify: + IVerifyFile(wk); + break; + + DEFAULT_FATAL(wk.fType); + } + + if (wk) { + // this "blocks" the worker thread on a dependent task like a file download that is + // completing asyncrhonously, do not remove this request... The block is removed + // by some callback calling IDequeueWork() or, worse case, DEL(wk). + LogMsg(kLogDebug, L"plSelfPatcher::IRun: Worker thread is now blocked on '%s'.", wk->fFileName); + wk->fFlags |= kRequestBlocked; + break; + } + } while (1); +} + +//============================================================================ +void plSelfPatcher::IQuit() +{ + PatcherWork* wk = NEW(PatcherWork); + wk->fType = kQuit; + IEnqueueWork(wk, true); +} + +//============================================================================ +void plSelfPatcher::Start() +{ + NetClientInitialize(); + NetClientSetErrorHandler(NetErrorHandler); + + const wchar** addrs; + unsigned count; + count = GetGateKeeperSrvHostnames(&addrs); + NetCliGateKeeperStartConnect(addrs, count); + + // request a file server ip address + NetCliGateKeeperFileSrvIpAddressRequest(OnFileSrvIP, NULL, true); + + hsThread::Start(); +} + +//============================================================================ +void plSelfPatcher::Stop() +{ + // Post a quit message and wait for the thread to stop. + if (Active()) + IQuit(); + hsThread::Stop(); + + NetCliFileDisconnect(); + NetClientUpdate(); + + // Shutdown the client/server networking subsystem + NetClientDestroy(); +} + +//============================================================================ +void plSelfPatcher::NetErrorHandler(ENetProtocol protocol, ENetError error) +{ + REF(protocol); + + LogMsg(kLogError, L"plSelfPatcher::NetErrorHandler: %s", NetErrorToString(error)); + if (IS_NET_SUCCESS(s_selfPatcher.fResult)) + s_selfPatcher.fResult = error; + if (error == kNetErrServerBusy) + s_selfPatcher.IReportServerBusy(); + else + s_selfPatcher.IQuit(); +} + +//============================================================================ +void plSelfPatcher::OnFileSrvIP(ENetError error, void* param, const wchar addr[]) +{ + NetCliGateKeeperDisconnect(); + if (IS_NET_ERROR(error)) { + LogMsg(kLogError, L"plSelfPatcher::OnFileSrvIP: %s", NetErrorToString(error)); + s_selfPatcher.fResult = error; + s_selfPatcher.IQuit(); + return; + } + + NetCliFileStartConnect(&addr, 1, true); + s_selfPatcher.IssueManifestRequests(); +} + +//============================================================================ +void plSelfPatcher::OnFileSrvManifest(ENetError result, void* param, const wchar group[], + const NetCliFileManifestEntry manifest[], unsigned entryCount) +{ + PatcherWork* wk = (PatcherWork*)param; + + switch (result) { + case kNetErrTimeout: + NetCliFileManifestRequest(OnFileSrvManifest, param, group); + return; + case kNetErrServerBusy: + s_selfPatcher.IReportServerBusy(); + return; + } + + if (IS_NET_ERROR(result) && !(wk->fFlags & kRequestOptionalManifest)) { + s_selfPatcher.fResult = result; + s_selfPatcher.IQuit(); + return; + } + + for (unsigned i = 0; i < entryCount; ++i) + s_selfPatcher.IEnqueueFile(manifest[i]); + s_selfPatcher.IDequeueWork(wk); +} + +//============================================================================ +void plSelfPatcher::OnFileSrvDownload(ENetError result, void* param, + const wchar filename[], hsStream* writer) +{ + switch (result) { + case kNetErrTimeout: + writer->Rewind(); + NetCliFileDownloadRequest(filename, writer, OnFileSrvDownload, param); + return; + case kNetErrServerBusy: + s_selfPatcher.IReportServerBusy(); + writer->Close(); + DEL(writer); + return; + } + + writer->Close(); + DEL(writer); + + if (IS_NET_ERROR(result)) { + LogMsg(kLogError, L"plSelfPatcher::OnFileSrvDownload: Error downloading '%s': %s", filename, NetErrorToString(result)); + s_selfPatcher.fResult = result; + s_selfPatcher.IQuit(); + } else { + PatcherWork* wk = (PatcherWork*)param; + wk->fType = kVerify; + s_selfPatcher.IEnqueueWork(wk, (wk->fFlags & kRequestNewPatcher)); + } +} /***************************************************************************** * @@ -324,7 +905,7 @@ bool SelfPatch (bool noSelfPatch, bool * abort, ENetError * result, plLauncherIn SetText("Checking for patcher update..."); patched = SelfPatcherProc(abort, info); } - *result = s_patchResult; + *result = s_selfPatcher.GetResult(); return patched; } diff --git a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/plLauncherInfo.h b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/plLauncherInfo.h index acb77f1c..f8dc2fc4 100644 --- a/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/plLauncherInfo.h +++ b/MOULOpenSourceClientPlugin/Plasma20/Sources/Plasma/Apps/plUruLauncher/plLauncherInfo.h @@ -87,6 +87,7 @@ struct plLauncherInfo { PatchInfo patchInfo; bool IsTGCider; DWORD returnCode; // used so we can pass a new process id back to gametap. That way gametap wont think uru has exited when the patcher quits. + HWND dialog; };