diff --git a/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.cpp b/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.cpp index fd3771ec..1ca1dc1a 100644 --- a/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.cpp +++ b/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.cpp @@ -58,7 +58,9 @@ Mead, WA 99021 #include "pfConsoleCore/pfConsoleEngine.h" PF_CONSOLE_LINK_FILE(Core) +#include #include +#include plClientLauncher::ErrorFunc s_errorProc = nullptr; // don't even ask, cause I'm not happy about this. @@ -145,12 +147,69 @@ void plShardStatus::Update() // =================================================== +class plRedistUpdater : public hsThread +{ + bool fSuccess; + +public: + plClientLauncher* fParent; + plClientLauncher::InstallRedistFunc fInstallProc; + std::deque fRedistQueue; + + plRedistUpdater() + : fSuccess(true) + { } + + ~plRedistUpdater() + { + // If anything is left in the deque, it was not installed. + // We should unlink them so the next launch will redownload and install them. + std::for_each(fRedistQueue.begin(), fRedistQueue.end(), + [] (const plFileName& file) { + plFileSystem::Unlink(file); + } + ); + } + + virtual void OnQuit() + { + // If we succeeded, then we should launch the game client... + if (fSuccess) + fParent->LaunchClient(); + } + + virtual hsError Run() + { + while (!fRedistQueue.empty()) { + if (fInstallProc(fRedistQueue.back())) + fRedistQueue.pop_back(); + else { + s_errorProc(kNetErrInternalError, fRedistQueue.back().AsString()); + fSuccess = false; + return hsFail; + } + } + return hsOK; + } + + virtual void Start() + { + if (fRedistQueue.empty()) + OnQuit(); + else + hsThread::Start(); + } +}; + +// =================================================== + plClientLauncher::plClientLauncher() : fFlags(0), fServerIni("server.ini"), fPatcherFactory(nullptr), fClientExecutable(plManifest::ClientExecutable()), - fStatusThread(new plShardStatus()) + fStatusThread(new plShardStatus()), + fInstallerThread(new plRedistUpdater()) { pfPatcher::GetLog()->AddLine(plProduct::ProductString().c_str()); } @@ -188,9 +247,11 @@ void plClientLauncher::IOnPatchComplete(ENetError result, const plString& msg) // case 1 hsSetBits(fFlags, kHaveSelfPatched); PatchClient(); - } else - // cases 2 & 3 - fLaunchClientFunc(fClientExecutable, GetAppArgs()); + } else { + // cases 2 & 3 -- update any redistributables, then launch the client. + fInstallerThread->fParent = this; + fInstallerThread->Start(); + } } else if (s_errorProc) s_errorProc(result, msg); } @@ -203,6 +264,13 @@ bool plClientLauncher::IApproveDownload(const plFileName& file) return !path.AsString().IsEmpty(); } +void plClientLauncher::LaunchClient() const +{ + if (fStatusFunc) + fStatusFunc("Launching..."); + fLaunchClientFunc(fClientExecutable, GetAppArgs()); +} + void plClientLauncher::PatchClient() { if (fStatusFunc) { @@ -216,6 +284,7 @@ void plClientLauncher::PatchClient() pfPatcher* patcher = fPatcherFactory(); patcher->OnCompletion(std::bind(&plClientLauncher::IOnPatchComplete, this, std::placeholders::_1, std::placeholders::_2)); patcher->OnSelfPatch([&](const plFileName& file) { fClientExecutable = file; }); + patcher->OnRedistUpdate([&](const plFileName& file) { fInstallerThread->fRedistQueue.push_back(file); }); // If this is a repair, we need to approve the downloads... if (hsCheckBits(fFlags, kGameDataOnly)) @@ -388,6 +457,11 @@ void plClientLauncher::SetErrorProc(ErrorFunc proc) s_errorProc = proc; } +void plClientLauncher::SetInstallerProc(InstallRedistFunc proc) +{ + fInstallerThread->fInstallProc = proc; +} + void plClientLauncher::SetShardProc(StatusFunc proc) { fStatusThread->fShardFunc = proc; diff --git a/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.h b/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.h index 464d3082..d7ddc856 100644 --- a/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.h +++ b/Sources/Plasma/Apps/plUruLauncher/plClientLauncher.h @@ -54,6 +54,7 @@ class plClientLauncher public: typedef std::function CreatePatcherFunc; typedef std::function ErrorFunc; + typedef std::function InstallRedistFunc; typedef std::function LaunchClientFunc; typedef std::function StatusFunc; @@ -71,7 +72,9 @@ private: plFileName fServerIni; plFileName fClientExecutable; - std::unique_ptr fStatusThread; + + std::unique_ptr fStatusThread; + std::unique_ptr fInstallerThread; CreatePatcherFunc fPatcherFactory; LaunchClientFunc fLaunchClientFunc; @@ -86,6 +89,11 @@ public: plClientLauncher(); ~plClientLauncher(); + /** Launch whatever client we think is appropriate. Please note that you should not call this unless you know + * absolutely without question what you are doing! + */ + void LaunchClient() const; + /** Begin the next logical patch operation. We are internally tracking if this is a self patch or a client patch. * All you need to do is make certain the doggone callbacks are set so that your UI will update. In theory, you * should never call this from your UI code since we manage this state for you. @@ -129,6 +137,11 @@ public: */ void SetErrorProc(ErrorFunc proc); + /** Set a callback that will execute and wait for redistributable installers. + * \remarks This will be called from a worker thread. + */ + void SetInstallerProc(InstallRedistFunc proc); + /** Set a patcher factory. */ void SetPatcherFactory(CreatePatcherFunc factory) { fPatcherFactory = factory; } diff --git a/Sources/Plasma/Apps/plUruLauncher/winmain.cpp b/Sources/Plasma/Apps/plUruLauncher/winmain.cpp index 702018c6..d25404bb 100644 --- a/Sources/Plasma/Apps/plUruLauncher/winmain.cpp +++ b/Sources/Plasma/Apps/plUruLauncher/winmain.cpp @@ -52,6 +52,7 @@ Mead, WA 99021 #include "hsWindows.h" #include "resource.h" #include +#include #include // =================================================== @@ -207,6 +208,108 @@ static void IOnProgressTick(uint64_t curBytes, uint64_t totalBytes, const plStri // =================================================== +static void ISetDownloadStatus(const plString& status) +{ + SetDlgItemTextW(s_dialog, IDC_TEXT, status.ToWchar()); + + // consider this a reset of the download status... + IShowMarquee(); + SetDlgItemTextW(s_dialog, IDC_DLSIZE, L""); + SetDlgItemTextW(s_dialog, IDC_DLSPEED, L""); + + if (s_taskbar) + s_taskbar->SetProgressState(s_dialog, TBPF_INDETERMINATE); +} + + +static HANDLE ICreateProcess(const plFileName& exe, const plString& args) +{ + STARTUPINFOW si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof(si)); + memset(&pi, 0, sizeof(pi)); + si.cb = sizeof(si); + + // Create wchar things and stuff :/ + plString cmd = plString::Format("%s %s", exe.AsString().c_str(), args.c_str()); + plStringBuffer file = exe.AsString().ToWchar(); + plStringBuffer params = cmd.ToWchar(); + + // Guess what? CreateProcess isn't smart enough to throw up an elevation dialog... We need ShellExecute for that. + // But guess what? ShellExecute won't run ".exe.tmp" files. GAAAAAAAAHHHHHHHHH!!!!!!! + BOOL result = CreateProcessW( + file, + const_cast(params.GetData()), + nullptr, + nullptr, + FALSE, + DETACHED_PROCESS, + nullptr, + nullptr, + &si, + &pi + ); + + // So maybe it needs elevation... Or maybe everything arseploded. + if (result != FALSE) { + CloseHandle(pi.hThread); + return pi.hProcess; + } else if (GetLastError() == ERROR_ELEVATION_REQUIRED) { + SHELLEXECUTEINFOW info; + memset(&info, 0, sizeof(info)); + info.cbSize = sizeof(info); + info.lpFile = file.GetData(); + info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC; + info.lpParameters = args.ToWchar(); + hsAssert(ShellExecuteExW(&info), "ShellExecuteExW phailed"); + + return info.hProcess; + } else { + wchar_t buf[2048]; + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, + GetLastError(), + LANG_USER_DEFAULT, + buf, + arrsize(buf), + nullptr + ); + hsMessageBox(buf, L"Error", hsMessageBoxNormal, hsMessageBoxIconError); + } + + return nullptr; +} + +static bool IInstallRedist(const plFileName& exe) +{ + ISetDownloadStatus(plString::Format("Installing... %s", exe.AsString().c_str())); + Sleep(2500); // let's Sleep for a bit so the user can see that we're doing something before the UAC dialog pops up! + + // Try to guess some arguments... Unfortunately, the file manifest format is fairly immutable. + plStringStream ss; + if (exe.AsString().CompareI("oalinst.exe") == 0) + ss << "/s"; // rarg nonstandard + else + ss << "/q"; + if (exe.AsString().Find("vcredist", plString::kCaseInsensitive) != -1) + ss << " /norestart"; // I don't want to image the accusations of viruses and hacking if this happened... + + // Now fire up the process... + HANDLE process = ICreateProcess(exe, ss.GetString()); + if (process) { + WaitForSingleObject(process, INFINITE); + + // Get the exit code so we can indicate success/failure to the redist thread + DWORD code = PLASMA_OK; + hsAssert(GetExitCodeProcess(process, &code), "failed to get redist exit code"); + CloseHandle(process); + + return code != PLASMA_PHAILURE; + } + return PLASMA_PHAILURE; +} + static void ILaunchClientExecutable(const plFileName& exe, const plString& args) { // Once we start launching something, we no longer need to trumpet any taskbar status @@ -216,38 +319,16 @@ static void ILaunchClientExecutable(const plFileName& exe, const plString& args) // Only launch a client executable if we're given one. If not, that's probably a cue that we're // done with some service operation and need to go away. if (!exe.AsString().IsEmpty()) { - STARTUPINFOW si; - PROCESS_INFORMATION pi; - memset(&si, 0, sizeof(si)); - memset(&pi, 0, sizeof(pi)); - si.cb = sizeof(si); - - // This event will prevent the game from restarting the patcher HANDLE hEvent = CreateEventW(nullptr, TRUE, FALSE, L"UruPatcherEvent"); - - // Fire up ye olde new process - plString cmd = plString::Format("%s %s", exe.AsString().c_str(), args.c_str()); - CreateProcessW( - exe.AsString().ToWchar(), - const_cast(cmd.ToWchar().GetData()), // windows claims that it may modify this... let's hope that doesn't happen. - nullptr, - nullptr, - FALSE, - DETACHED_PROCESS, - nullptr, - plFileSystem::GetCWD().AsString().ToWchar(), - &si, - &pi - ); + HANDLE process = ICreateProcess(exe, args); // if this is the real game client, then we need to make sure it gets this event... if (plManifest::ClientExecutable().AsString().CompareI(exe.AsString()) == 0) { - WaitForInputIdle(pi.hProcess, 1000); + WaitForInputIdle(process, 1000); WaitForSingleObject(hEvent, INFINITE); } - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); + CloseHandle(process); CloseHandle(hEvent); } @@ -265,19 +346,6 @@ static void IOnNetError(ENetError result, const plString& msg) IQuit(PLASMA_PHAILURE); } -static void ISetDownloadStatus(const plString& status) -{ - SetDlgItemTextW(s_dialog, IDC_TEXT, status.ToWchar()); - - // consider this a reset of the download status... - IShowMarquee(); - SetDlgItemTextW(s_dialog, IDC_DLSIZE, L""); - SetDlgItemTextW(s_dialog, IDC_DLSPEED, L""); - - if (s_taskbar) - s_taskbar->SetProgressState(s_dialog, TBPF_INDETERMINATE); -} - static void ISetShardStatus(const plString& status) { SetDlgItemTextW(s_dialog, IDC_STATUS_TEXT, status.ToWchar()); @@ -299,6 +367,7 @@ int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdL // Let's initialize our plClientLauncher friend s_launcher.ParseArguments(); s_launcher.SetErrorProc(IOnNetError); + s_launcher.SetInstallerProc(IInstallRedist); s_launcher.SetLaunchClientProc(ILaunchClientExecutable); s_launcher.SetPatcherFactory(IPatcherFactory); s_launcher.SetShardProc(ISetShardStatus); diff --git a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp index 75b9615e..5ee9f474 100644 --- a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp +++ b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp @@ -90,9 +90,12 @@ struct pfPatcherWorker : public hsThread // Any file kFlagZipped = 1<<3, + // Executable flags + kRedistUpdate = 1<<4, + // Begin internal flags - kLastManifestFlag = 1<<4, - kSelfPatch = 1<<5, + kLastManifestFlag = 1<<5, + kSelfPatch = 1<<6, }; std::deque fRequests; @@ -108,6 +111,7 @@ struct pfPatcherWorker : public hsThread pfPatcher::FileDownloadFunc fFileDownloaded; pfPatcher::GameCodeDiscoverFunc fGameCodeDiscovered; pfPatcher::ProgressTickFunc fProgressTick; + pfPatcher::FileDownloadFunc fRedistUpdateDownloaded; pfPatcher::FileDownloadFunc fSelfPatch; pfPatcher* fParent; @@ -205,6 +209,7 @@ public: void Begin() { fDLStartTime = hsTimer::GetSysSeconds(); } plFileName GetFileName() const { return fFilename; } + bool IsRedistUpdate() const { return hsCheckBits(fFlags, pfPatcherWorker::kRedistUpdate); } bool IsSelfPatch() const { return hsCheckBits(fFlags, pfPatcherWorker::kSelfPatch); } void Unlink() const { plFileSystem::Unlink(fFilename); } }; @@ -315,6 +320,8 @@ static void IFileThingDownloadCB(ENetError result, void* param, const plFileName patcher->WhitelistFile(stream->GetFileName(), true); if (patcher->fSelfPatch && stream->IsSelfPatch()) patcher->fSelfPatch(stream->GetFileName()); + if (patcher->fRedistUpdateDownloaded && stream->IsRedistUpdate()) + patcher->fRedistUpdateDownloaded(stream->GetFileName()); patcher->IssueRequest(); } else { PatcherLogRed("\tDownloaded Failed: File '%s'", stream->GetFileName().AsString().c_str()); @@ -594,6 +601,11 @@ void pfPatcher::OnProgressTick(ProgressTickFunc cb) fWorker->fProgressTick = cb; } +void pfPatcher::OnRedistUpdate(FileDownloadFunc cb) +{ + fWorker->fRedistUpdateDownloaded = cb; +} + void pfPatcher::OnSelfPatch(FileDownloadFunc cb) { fWorker->fSelfPatch = cb; diff --git a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h index 3147cf6e..d5d9dfea 100644 --- a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h +++ b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h @@ -121,6 +121,12 @@ public: */ void OnProgressTick(ProgressTickFunc cb); + /** Set a callback that will be fired when the patcher downloads an updated redistributable. Such as + * the Visual C++ runtime (vcredist_x86.exe). You are responsible for installing it. + * \remarks This will be called from the network thread. + */ + void OnRedistUpdate(FileDownloadFunc cb); + /** This is called when the current application has been updated. */ void OnSelfPatch(FileDownloadFunc cb);