|
|
@@ -0,0 +1,1361 @@
|
|
|
+/******************************************************************************
|
|
|
+ Copyright (C) 2017 Hugh Bailey <[email protected]>
|
|
|
+
|
|
|
+ This program is free software; you can redistribute it and/or modify
|
|
|
+ it under the terms of the GNU General Public License as published by
|
|
|
+ the Free Software Foundation; either version 2 of the License, or
|
|
|
+ (at your option) any later version.
|
|
|
+
|
|
|
+ This program is distributed in the hope that it will be useful,
|
|
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+ GNU General Public License for more details.
|
|
|
+
|
|
|
+ You should have received a copy of the GNU General Public License
|
|
|
+ along with this program; if not, write to the Free Software
|
|
|
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
|
|
|
+******************************************************************************/
|
|
|
+
|
|
|
+#include "updater.hpp"
|
|
|
+
|
|
|
+#include <util/windows/CoTaskMemPtr.hpp>
|
|
|
+
|
|
|
+#include <future>
|
|
|
+#include <vector>
|
|
|
+#include <string>
|
|
|
+#include <mutex>
|
|
|
+
|
|
|
+using namespace std;
|
|
|
+
|
|
|
+/* ----------------------------------------------------------------------- */
|
|
|
+
|
|
|
+HANDLE cancelRequested = nullptr;
|
|
|
+HANDLE updateThread = nullptr;
|
|
|
+HINSTANCE hinstMain = nullptr;
|
|
|
+HWND hwndMain = nullptr;
|
|
|
+HCRYPTPROV hProvider = 0;
|
|
|
+
|
|
|
+static bool bExiting = false;
|
|
|
+static bool updateFailed = false;
|
|
|
+static bool is32bit = false;
|
|
|
+
|
|
|
+static bool downloadThreadFailure = false;
|
|
|
+
|
|
|
+int totalFileSize = 0;
|
|
|
+int completedFileSize = 0;
|
|
|
+static int completedUpdates = 0;
|
|
|
+
|
|
|
+struct LastError {
|
|
|
+ DWORD code;
|
|
|
+ inline LastError() { code = GetLastError(); }
|
|
|
+};
|
|
|
+
|
|
|
+void FreeWinHttpHandle(HINTERNET handle)
|
|
|
+{
|
|
|
+ WinHttpCloseHandle(handle);
|
|
|
+}
|
|
|
+
|
|
|
+/* ----------------------------------------------------------------------- */
|
|
|
+
|
|
|
+// http://www.codeproject.com/Articles/320748/Haephrati-Elevating-during-runtime
|
|
|
+static bool IsAppRunningAsAdminMode()
|
|
|
+{
|
|
|
+ BOOL fIsRunAsAdmin = FALSE;
|
|
|
+ DWORD dwError = ERROR_SUCCESS;
|
|
|
+ PSID pAdministratorsGroup = nullptr;
|
|
|
+
|
|
|
+ /* Allocate and initialize a SID of the administrators group. */
|
|
|
+ SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
|
|
|
+ if (!AllocateAndInitializeSid(&NtAuthority,
|
|
|
+ 2,
|
|
|
+ SECURITY_BUILTIN_DOMAIN_RID,
|
|
|
+ DOMAIN_ALIAS_RID_ADMINS,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ &pAdministratorsGroup)) {
|
|
|
+ dwError = GetLastError();
|
|
|
+ goto Cleanup;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Determine whether the SID of administrators group is enabled in the
|
|
|
+ * primary access token of the process. */
|
|
|
+ if (!CheckTokenMembership(nullptr, pAdministratorsGroup,
|
|
|
+ &fIsRunAsAdmin)) {
|
|
|
+ dwError = GetLastError();
|
|
|
+ goto Cleanup;
|
|
|
+ }
|
|
|
+
|
|
|
+Cleanup:
|
|
|
+ /* Centralized cleanup for all allocated resources. */
|
|
|
+ if (pAdministratorsGroup) {
|
|
|
+ FreeSid(pAdministratorsGroup);
|
|
|
+ pAdministratorsGroup = nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Throw the error if something failed in the function. */
|
|
|
+ if (ERROR_SUCCESS != dwError)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ return !!fIsRunAsAdmin;
|
|
|
+}
|
|
|
+
|
|
|
+static void Status(const wchar_t *fmt, ...)
|
|
|
+{
|
|
|
+ wchar_t str[512];
|
|
|
+
|
|
|
+ va_list argptr;
|
|
|
+ va_start(argptr, fmt);
|
|
|
+
|
|
|
+ StringCbVPrintf(str, sizeof(str), fmt, argptr);
|
|
|
+
|
|
|
+ SetDlgItemText(hwndMain, IDC_STATUS, str);
|
|
|
+
|
|
|
+ va_end(argptr);
|
|
|
+}
|
|
|
+
|
|
|
+static void CreateFoldersForPath(const wchar_t *path)
|
|
|
+{
|
|
|
+ wchar_t *p = (wchar_t *)path;
|
|
|
+
|
|
|
+ while (*p) {
|
|
|
+ if (*p == '\\' || *p == '/') {
|
|
|
+ *p = 0;
|
|
|
+ CreateDirectory(path, nullptr);
|
|
|
+ *p = '\\';
|
|
|
+ }
|
|
|
+ p++;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static bool MyCopyFile(const wchar_t *src, const wchar_t *dest)
|
|
|
+try {
|
|
|
+ WinHandle hSrc;
|
|
|
+ WinHandle hDest;
|
|
|
+
|
|
|
+ hSrc = CreateFile(src, GENERIC_READ, 0, nullptr, OPEN_EXISTING,
|
|
|
+ FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
|
|
|
+ if (!hSrc.Valid())
|
|
|
+ throw LastError();
|
|
|
+
|
|
|
+ hDest = CreateFile(dest, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
|
|
+ 0, nullptr);
|
|
|
+ if (!hDest.Valid())
|
|
|
+ throw LastError();
|
|
|
+
|
|
|
+ BYTE buf[65536];
|
|
|
+ DWORD read, wrote;
|
|
|
+
|
|
|
+ for (;;) {
|
|
|
+ if (!ReadFile(hSrc, buf, sizeof(buf), &read, nullptr))
|
|
|
+ throw LastError();
|
|
|
+
|
|
|
+ if (read == 0)
|
|
|
+ break;
|
|
|
+
|
|
|
+ if (!WriteFile(hDest, buf, read, &wrote, nullptr))
|
|
|
+ throw LastError();
|
|
|
+
|
|
|
+ if (wrote != read)
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+} catch (LastError error) {
|
|
|
+ SetLastError(error.code);
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+static bool IsSafeFilename(const wchar_t *path)
|
|
|
+{
|
|
|
+ const wchar_t *p = path;
|
|
|
+
|
|
|
+ if (!*p)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (wcsstr(path, L".."))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (*p == '/')
|
|
|
+ return false;
|
|
|
+
|
|
|
+ while (*p) {
|
|
|
+ if (!isalnum(*p) &&
|
|
|
+ *p != '.' &&
|
|
|
+ *p != '/' &&
|
|
|
+ *p != '_' &&
|
|
|
+ *p != '-')
|
|
|
+ return false;
|
|
|
+ p++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static string QuickReadFile(const wchar_t *path)
|
|
|
+{
|
|
|
+ string data;
|
|
|
+
|
|
|
+ WinHandle handle = CreateFileW(path, GENERIC_READ, 0, nullptr,
|
|
|
+ OPEN_EXISTING, 0, nullptr);
|
|
|
+ if (!handle.Valid()) {
|
|
|
+ return string();
|
|
|
+ }
|
|
|
+
|
|
|
+ LARGE_INTEGER size;
|
|
|
+
|
|
|
+ if (!GetFileSizeEx(handle, &size)) {
|
|
|
+ return string();
|
|
|
+ }
|
|
|
+
|
|
|
+ data.resize((size_t)size.QuadPart);
|
|
|
+
|
|
|
+ DWORD read;
|
|
|
+ if (!ReadFile(handle,
|
|
|
+ &data[0],
|
|
|
+ (DWORD)data.size(),
|
|
|
+ &read,
|
|
|
+ nullptr)) {
|
|
|
+ return string();
|
|
|
+ }
|
|
|
+ if (read != size.QuadPart) {
|
|
|
+ return string();
|
|
|
+ }
|
|
|
+
|
|
|
+ return data;
|
|
|
+}
|
|
|
+
|
|
|
+/* ----------------------------------------------------------------------- */
|
|
|
+
|
|
|
+enum state_t {
|
|
|
+ STATE_INVALID,
|
|
|
+ STATE_PENDING_DOWNLOAD,
|
|
|
+ STATE_DOWNLOADING,
|
|
|
+ STATE_DOWNLOADED,
|
|
|
+ STATE_INSTALLED,
|
|
|
+};
|
|
|
+
|
|
|
+struct update_t {
|
|
|
+ wstring sourceURL;
|
|
|
+ wstring outputPath;
|
|
|
+ wstring tempPath;
|
|
|
+ wstring previousFile;
|
|
|
+ wstring basename;
|
|
|
+ string packageName;
|
|
|
+
|
|
|
+ DWORD fileSize = 0;
|
|
|
+ BYTE hash[BLAKE2_HASH_LENGTH];
|
|
|
+ BYTE downloadhash[BLAKE2_HASH_LENGTH];
|
|
|
+ BYTE my_hash[BLAKE2_HASH_LENGTH];
|
|
|
+ state_t state = STATE_INVALID;
|
|
|
+ bool has_hash = false;
|
|
|
+ bool patchable = false;
|
|
|
+
|
|
|
+ inline update_t() {}
|
|
|
+ inline update_t(const update_t &from)
|
|
|
+ : sourceURL(from.sourceURL),
|
|
|
+ outputPath(from.outputPath),
|
|
|
+ tempPath(from.tempPath),
|
|
|
+ previousFile(from.previousFile),
|
|
|
+ basename(from.basename),
|
|
|
+ packageName(from.packageName),
|
|
|
+ fileSize(from.fileSize),
|
|
|
+ state(from.state),
|
|
|
+ has_hash(from.has_hash),
|
|
|
+ patchable(from.patchable)
|
|
|
+ {
|
|
|
+ memcpy(hash, from.hash, sizeof(hash));
|
|
|
+ memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
|
|
|
+ memcpy(my_hash, from.my_hash, sizeof(my_hash));
|
|
|
+ }
|
|
|
+
|
|
|
+ inline update_t(update_t &&from)
|
|
|
+ : sourceURL(std::move(from.sourceURL)),
|
|
|
+ outputPath(std::move(from.outputPath)),
|
|
|
+ tempPath(std::move(from.tempPath)),
|
|
|
+ previousFile(std::move(from.previousFile)),
|
|
|
+ basename(std::move(from.basename)),
|
|
|
+ packageName(std::move(from.packageName)),
|
|
|
+ fileSize(from.fileSize),
|
|
|
+ state(from.state),
|
|
|
+ has_hash(from.has_hash),
|
|
|
+ patchable(from.patchable)
|
|
|
+ {
|
|
|
+ from.state = STATE_INVALID;
|
|
|
+
|
|
|
+ memcpy(hash, from.hash, sizeof(hash));
|
|
|
+ memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
|
|
|
+ memcpy(my_hash, from.my_hash, sizeof(my_hash));
|
|
|
+ }
|
|
|
+
|
|
|
+ void CleanPartialUpdate()
|
|
|
+ {
|
|
|
+ if (state == STATE_INSTALLED) {
|
|
|
+ if (!previousFile.empty()) {
|
|
|
+ DeleteFile(outputPath.c_str());
|
|
|
+ MyCopyFile(previousFile.c_str(),
|
|
|
+ outputPath.c_str());
|
|
|
+ DeleteFile(previousFile.c_str());
|
|
|
+ } else {
|
|
|
+ DeleteFile(outputPath.c_str());
|
|
|
+ }
|
|
|
+ } else if (state == STATE_DOWNLOADED) {
|
|
|
+ DeleteFile(tempPath.c_str());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ inline update_t &operator=(const update_t &from)
|
|
|
+ {
|
|
|
+ sourceURL = from.sourceURL;
|
|
|
+ outputPath = from.outputPath;
|
|
|
+ tempPath = from.tempPath;
|
|
|
+ previousFile = from.previousFile;
|
|
|
+ basename = from.basename;
|
|
|
+ packageName = from.packageName;
|
|
|
+ fileSize = from.fileSize;
|
|
|
+ state = from.state;
|
|
|
+ has_hash = from.has_hash;
|
|
|
+ patchable = from.patchable;
|
|
|
+
|
|
|
+ memcpy(hash, from.hash, sizeof(hash));
|
|
|
+ memcpy(downloadhash, from.downloadhash, sizeof(downloadhash));
|
|
|
+ memcpy(my_hash, from.my_hash, sizeof(my_hash));
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+static vector<update_t> updates;
|
|
|
+static mutex updateMutex;
|
|
|
+
|
|
|
+static inline void CleanupPartialUpdates()
|
|
|
+{
|
|
|
+ for (update_t &update : updates)
|
|
|
+ update.CleanPartialUpdate();
|
|
|
+}
|
|
|
+
|
|
|
+/* ----------------------------------------------------------------------- */
|
|
|
+
|
|
|
+bool DownloadWorkerThread()
|
|
|
+{
|
|
|
+ HttpHandle hSession = WinHttpOpen(L"OBS Studio Updater/2.1",
|
|
|
+ WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
|
|
+ WINHTTP_NO_PROXY_NAME,
|
|
|
+ WINHTTP_NO_PROXY_BYPASS,
|
|
|
+ 0);
|
|
|
+ if (!hSession) {
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ Status(L"Update failed: Couldn't open obsproject.com");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ HttpHandle hConnect = WinHttpConnect(hSession, L"obsproject.com",
|
|
|
+ INTERNET_DEFAULT_HTTPS_PORT, 0);
|
|
|
+ if (!hConnect) {
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ Status(L"Update failed: Couldn't connect to obsproject.com");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (;;) {
|
|
|
+ bool foundWork = false;
|
|
|
+
|
|
|
+ unique_lock<mutex> ulock(updateMutex);
|
|
|
+
|
|
|
+ for (update_t &update : updates) {
|
|
|
+ int responseCode;
|
|
|
+
|
|
|
+ DWORD waitResult =
|
|
|
+ WaitForSingleObject(cancelRequested, 0);
|
|
|
+ if (waitResult == WAIT_OBJECT_0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (update.state != STATE_PENDING_DOWNLOAD)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ update.state = STATE_DOWNLOADING;
|
|
|
+
|
|
|
+ ulock.unlock();
|
|
|
+
|
|
|
+ foundWork = true;
|
|
|
+
|
|
|
+ if (downloadThreadFailure) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status(L"Downloading %s", update.outputPath.c_str());
|
|
|
+
|
|
|
+ if (!HTTPGetFile(hConnect,
|
|
|
+ update.sourceURL.c_str(),
|
|
|
+ update.tempPath.c_str(),
|
|
|
+ L"Accept-Encoding: gzip",
|
|
|
+ &responseCode)) {
|
|
|
+
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ DeleteFile(update.tempPath.c_str());
|
|
|
+ Status(L"Update failed: Could not download "
|
|
|
+ L"%s (error code %d)",
|
|
|
+ update.outputPath.c_str(),
|
|
|
+ responseCode);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (responseCode != 200) {
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ DeleteFile(update.tempPath.c_str());
|
|
|
+ Status(L"Update failed: Could not download "
|
|
|
+ L"%s (error code %d)",
|
|
|
+ update.outputPath.c_str(),
|
|
|
+ responseCode);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ BYTE downloadHash[BLAKE2_HASH_LENGTH];
|
|
|
+ if (!CalculateFileHash(update.tempPath.c_str(),
|
|
|
+ downloadHash)) {
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ DeleteFile(update.tempPath.c_str());
|
|
|
+ Status(L"Update failed: Couldn't verify "
|
|
|
+ L"integrity of %s",
|
|
|
+ update.outputPath.c_str());
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (memcmp(update.downloadhash, downloadHash, 20)) {
|
|
|
+ downloadThreadFailure = true;
|
|
|
+ DeleteFile(update.tempPath.c_str());
|
|
|
+ Status(L"Update failed: Integrity check "
|
|
|
+ L"failed on %s",
|
|
|
+ update.outputPath.c_str());
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ ulock.lock();
|
|
|
+
|
|
|
+ update.state = STATE_DOWNLOADED;
|
|
|
+ completedUpdates++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!foundWork) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (downloadThreadFailure) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static bool RunDownloadWorkers(int num)
|
|
|
+try {
|
|
|
+ vector<future<bool>> thread_success_results;
|
|
|
+ thread_success_results.resize(num);
|
|
|
+
|
|
|
+ for (future<bool> &result : thread_success_results) {
|
|
|
+ result = async(DownloadWorkerThread);
|
|
|
+ }
|
|
|
+ for (future<bool> &result : thread_success_results) {
|
|
|
+ if (!result.get()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+} catch (...) {
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+/* ----------------------------------------------------------------------- */
|
|
|
+
|
|
|
+static inline bool UTF8ToWide(wchar_t *wide, int wideSize, const char *utf8)
|
|
|
+{
|
|
|
+ return !!MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, wideSize);
|
|
|
+}
|
|
|
+
|
|
|
+static inline bool WideToUTF8(char *utf8, int utf8Size, const wchar_t *wide)
|
|
|
+{
|
|
|
+ return !!WideCharToMultiByte(CP_UTF8, 0, wide, -1, utf8, utf8Size,
|
|
|
+ nullptr, nullptr);
|
|
|
+}
|
|
|
+
|
|
|
+static inline bool FileExists(const wchar_t *path)
|
|
|
+{
|
|
|
+ WIN32_FIND_DATAW wfd;
|
|
|
+ HANDLE hFind;
|
|
|
+
|
|
|
+ hFind = FindFirstFileW(path, &wfd);
|
|
|
+ if (hFind != INVALID_HANDLE_VALUE)
|
|
|
+ FindClose(hFind);
|
|
|
+
|
|
|
+ return hFind != INVALID_HANDLE_VALUE;
|
|
|
+}
|
|
|
+
|
|
|
+static bool NonCorePackageInstalled(const char *name)
|
|
|
+{
|
|
|
+ if (strcmp(name, "obs-browser") == 0) {
|
|
|
+ return FileExists(L"obs-plugins\\32bit\\obs-browser.dll");
|
|
|
+ } else if (strcmp(name, "realsense") == 0) {
|
|
|
+ return FileExists(L"obs-plugins\\32bit\\win-ivcam.dll");
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+#define UTF8ToWideBuf(wide, utf8) UTF8ToWide(wide, _countof(wide), utf8)
|
|
|
+#define WideToUTF8Buf(utf8, wide) WideToUTF8(utf8, _countof(utf8), wide)
|
|
|
+
|
|
|
+#define UPDATE_URL L"https://obsproject.com/update_studio"
|
|
|
+
|
|
|
+static bool AddPackageUpdateFiles(json_t *root, size_t idx,
|
|
|
+ const wchar_t *tempPath)
|
|
|
+{
|
|
|
+ json_t *package = json_array_get(root, idx);
|
|
|
+ json_t *name = json_object_get(package, "name");
|
|
|
+ json_t *files = json_object_get(package, "files");
|
|
|
+
|
|
|
+ if (!json_is_array(files))
|
|
|
+ return true;
|
|
|
+ if (!json_is_string(name))
|
|
|
+ return true;
|
|
|
+
|
|
|
+ wchar_t wPackageName[512];
|
|
|
+ const char *packageName = json_string_value(name);
|
|
|
+ size_t fileCount = json_array_size(files);
|
|
|
+
|
|
|
+ if (!UTF8ToWideBuf(wPackageName, packageName))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (strcmp(packageName, "core") != 0 &&
|
|
|
+ !NonCorePackageInstalled(packageName))
|
|
|
+ return true;
|
|
|
+
|
|
|
+ for (size_t j = 0; j < fileCount; j++) {
|
|
|
+ json_t *file = json_array_get(files, j);
|
|
|
+ json_t *fileName = json_object_get(file, "name");
|
|
|
+ json_t *hash = json_object_get(file, "hash");
|
|
|
+ json_t *size = json_object_get(file, "size");
|
|
|
+
|
|
|
+ if (!json_is_string(fileName))
|
|
|
+ continue;
|
|
|
+ if (!json_is_string(hash))
|
|
|
+ continue;
|
|
|
+ if (!json_is_integer(size))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ const char *fileUTF8 = json_string_value(fileName);
|
|
|
+ const char *hashUTF8 = json_string_value(hash);
|
|
|
+ int fileSize = (int)json_integer_value(size);
|
|
|
+
|
|
|
+ if (strlen(hashUTF8) != BLAKE2_HASH_LENGTH * 2)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ /* convert strings to wide */
|
|
|
+
|
|
|
+ wchar_t sourceURL[1024];
|
|
|
+ wchar_t updateFileName[MAX_PATH];
|
|
|
+ wchar_t updateHashStr[BLAKE2_HASH_STR_LENGTH];
|
|
|
+ wchar_t tempFilePath[MAX_PATH];
|
|
|
+
|
|
|
+ if (!UTF8ToWideBuf(updateFileName, fileUTF8))
|
|
|
+ continue;
|
|
|
+ if (!UTF8ToWideBuf(updateHashStr, hashUTF8))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ /* make sure paths are safe */
|
|
|
+
|
|
|
+ if (!IsSafeFilename(updateFileName)) {
|
|
|
+ Status(L"Update failed: Unsafe path '%s' found in "
|
|
|
+ L"manifest", updateFileName);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ StringCbPrintf(sourceURL, sizeof(sourceURL), L"%s/%s/%s",
|
|
|
+ UPDATE_URL, wPackageName, updateFileName);
|
|
|
+ StringCbPrintf(tempFilePath, sizeof(tempFilePath),
|
|
|
+ L"%s\\%s", tempPath, updateHashStr);
|
|
|
+
|
|
|
+ /* Check file hash */
|
|
|
+
|
|
|
+ BYTE existingHash[BLAKE2_HASH_LENGTH];
|
|
|
+ wchar_t fileHashStr[BLAKE2_HASH_STR_LENGTH];
|
|
|
+ bool has_hash;
|
|
|
+
|
|
|
+ /* We don't really care if this fails, it's just to avoid
|
|
|
+ * wasting bandwidth by downloading unmodified files */
|
|
|
+ if (CalculateFileHash(updateFileName, existingHash)) {
|
|
|
+
|
|
|
+ HashToString(existingHash, fileHashStr);
|
|
|
+ if (wcscmp(fileHashStr, updateHashStr) == 0)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ has_hash = true;
|
|
|
+ } else {
|
|
|
+ has_hash = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Add update file */
|
|
|
+
|
|
|
+ update_t update;
|
|
|
+ update.fileSize = fileSize;
|
|
|
+ update.basename = updateFileName;
|
|
|
+ update.outputPath = updateFileName;
|
|
|
+ update.tempPath = tempFilePath;
|
|
|
+ update.sourceURL = sourceURL;
|
|
|
+ update.packageName = packageName;
|
|
|
+ update.state = STATE_PENDING_DOWNLOAD;
|
|
|
+ update.patchable = false;
|
|
|
+
|
|
|
+ StringToHash(updateHashStr, update.downloadhash);
|
|
|
+ memcpy(update.hash, update.downloadhash, sizeof(update.hash));
|
|
|
+
|
|
|
+ update.has_hash = has_hash;
|
|
|
+ if (has_hash)
|
|
|
+ StringToHash(fileHashStr, update.my_hash);
|
|
|
+
|
|
|
+ updates.push_back(move(update));
|
|
|
+
|
|
|
+ totalFileSize += fileSize;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static void UpdateWithPatchIfAvailable(const char *name, const char *hash,
|
|
|
+ const char *source,
|
|
|
+ int size)
|
|
|
+{
|
|
|
+ wchar_t widePatchableFilename[MAX_PATH];
|
|
|
+ wchar_t widePatchHash[MAX_PATH];
|
|
|
+ wchar_t sourceURL[1024];
|
|
|
+ wchar_t patchHashStr[BLAKE2_HASH_STR_LENGTH];
|
|
|
+
|
|
|
+ if (strncmp(source, "https://obsproject.com/", 23) != 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ string patchPackageName = name;
|
|
|
+
|
|
|
+ const char *slash = strchr(name, '/');
|
|
|
+ if (!slash)
|
|
|
+ return;
|
|
|
+
|
|
|
+ patchPackageName.resize(slash - name);
|
|
|
+ name = slash + 1;
|
|
|
+
|
|
|
+ if (!UTF8ToWideBuf(widePatchableFilename, name))
|
|
|
+ return;
|
|
|
+ if (!UTF8ToWideBuf(widePatchHash, hash))
|
|
|
+ return;
|
|
|
+ if (!UTF8ToWideBuf(sourceURL, source))
|
|
|
+ return;
|
|
|
+ if (!UTF8ToWideBuf(patchHashStr, hash))
|
|
|
+ return;
|
|
|
+
|
|
|
+ for (update_t &update : updates) {
|
|
|
+ if (update.packageName != patchPackageName)
|
|
|
+ continue;
|
|
|
+ if (update.basename != widePatchableFilename)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ StringToHash(patchHashStr, update.downloadhash);
|
|
|
+
|
|
|
+ /* Replace the source URL with the patch file, mark it as
|
|
|
+ * patchable, and re-calculate download size */
|
|
|
+ totalFileSize -= (update.fileSize - size);
|
|
|
+ update.sourceURL = sourceURL;
|
|
|
+ update.fileSize = size;
|
|
|
+ update.patchable = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static bool UpdateFile(update_t &file)
|
|
|
+{
|
|
|
+ wchar_t oldFileRenamedPath[MAX_PATH];
|
|
|
+
|
|
|
+ if (file.patchable)
|
|
|
+ Status(L"Updating %s...", file.outputPath.c_str());
|
|
|
+ else
|
|
|
+ Status(L"Installing %s...", file.outputPath.c_str());
|
|
|
+
|
|
|
+ /* Check if we're replacing an existing file or just installing a new
|
|
|
+ * one */
|
|
|
+ DWORD attribs = GetFileAttributes(file.outputPath.c_str());
|
|
|
+
|
|
|
+ if (attribs != INVALID_FILE_ATTRIBUTES) {
|
|
|
+ wchar_t *curFileName = nullptr;
|
|
|
+ wchar_t baseName[MAX_PATH];
|
|
|
+
|
|
|
+ StringCbCopy(baseName, sizeof(baseName),
|
|
|
+ file.outputPath.c_str());
|
|
|
+
|
|
|
+ curFileName = wcsrchr(baseName, '/');
|
|
|
+ if (curFileName) {
|
|
|
+ curFileName[0] = '\0';
|
|
|
+ curFileName++;
|
|
|
+ } else
|
|
|
+ curFileName = baseName;
|
|
|
+
|
|
|
+ /* Backup the existing file in case a rollback is needed */
|
|
|
+ StringCbCopy(oldFileRenamedPath,
|
|
|
+ sizeof(oldFileRenamedPath),
|
|
|
+ file.outputPath.c_str());
|
|
|
+ StringCbCat(oldFileRenamedPath,
|
|
|
+ sizeof(oldFileRenamedPath),
|
|
|
+ L".old");
|
|
|
+
|
|
|
+ if (!MyCopyFile(file.outputPath.c_str(), oldFileRenamedPath)) {
|
|
|
+ int is_sharing_violation =
|
|
|
+ (GetLastError() == ERROR_SHARING_VIOLATION);
|
|
|
+
|
|
|
+ if (is_sharing_violation)
|
|
|
+ Status(L"Update failed: %s is still in use. "
|
|
|
+ L"Close all programs and try again.",
|
|
|
+ curFileName);
|
|
|
+ else
|
|
|
+ Status(L"Update failed: Couldn't backup %s "
|
|
|
+ L"(error %d)",
|
|
|
+ curFileName, GetLastError());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ int error_code;
|
|
|
+ bool installed_ok;
|
|
|
+
|
|
|
+ if (file.patchable) {
|
|
|
+ error_code = ApplyPatch(
|
|
|
+ file.tempPath.c_str(),
|
|
|
+ file.outputPath.c_str());
|
|
|
+ installed_ok = (error_code == 0);
|
|
|
+
|
|
|
+ if (installed_ok) {
|
|
|
+ BYTE patchedFileHash[BLAKE2_HASH_LENGTH];
|
|
|
+ if (!CalculateFileHash(file.outputPath.c_str(),
|
|
|
+ patchedFileHash)) {
|
|
|
+ Status(L"Update failed: Couldn't "
|
|
|
+ L"verify integrity of patched %s",
|
|
|
+ curFileName);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (memcmp(file.hash, patchedFileHash,
|
|
|
+ BLAKE2_HASH_LENGTH) != 0) {
|
|
|
+ Status(L"Update failed: Integrity "
|
|
|
+ L"check of patched "
|
|
|
+ L"%s failed",
|
|
|
+ curFileName);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ installed_ok = MyCopyFile(
|
|
|
+ file.tempPath.c_str(),
|
|
|
+ file.outputPath.c_str());
|
|
|
+ error_code = GetLastError();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!installed_ok) {
|
|
|
+ int is_sharing_violation =
|
|
|
+ (error_code == ERROR_SHARING_VIOLATION);
|
|
|
+
|
|
|
+ if (is_sharing_violation)
|
|
|
+ Status(L"Update failed: %s is still in use. "
|
|
|
+ L"Close all "
|
|
|
+ L"programs and try again.",
|
|
|
+ curFileName);
|
|
|
+ else
|
|
|
+ Status(L"Update failed: Couldn't update %s "
|
|
|
+ L"(error %d)",
|
|
|
+ curFileName,
|
|
|
+ GetLastError());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ file.previousFile = oldFileRenamedPath;
|
|
|
+ file.state = STATE_INSTALLED;
|
|
|
+ } else {
|
|
|
+ if (file.patchable) {
|
|
|
+ /* Uh oh, we thought we could patch something but it's
|
|
|
+ * no longer there! */
|
|
|
+ Status(L"Update failed: Source file %s not found",
|
|
|
+ file.outputPath.c_str());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* We may be installing into new folders,
|
|
|
+ * make sure they exist */
|
|
|
+ CreateFoldersForPath(file.outputPath.c_str());
|
|
|
+
|
|
|
+ bool success = !!MyCopyFile(
|
|
|
+ file.tempPath.c_str(),
|
|
|
+ file.outputPath.c_str());
|
|
|
+ if (!success) {
|
|
|
+ Status(L"Update failed: Couldn't install %s (error %d)",
|
|
|
+ file.outputPath.c_str(),
|
|
|
+ GetLastError());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ file.previousFile = L"";
|
|
|
+ file.state = STATE_INSTALLED;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static wchar_t tempPath[MAX_PATH] = {};
|
|
|
+
|
|
|
+#define PATCH_MANIFEST_URL \
|
|
|
+ L"https://obsproject.com/update_studio/getpatchmanifest"
|
|
|
+#define HASH_NULL \
|
|
|
+ L"0000000000000000000000000000000000000000"
|
|
|
+
|
|
|
+static bool Update(wchar_t *cmdLine)
|
|
|
+{
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Check to make sure OBS isn't running */
|
|
|
+
|
|
|
+ HANDLE hObsUpdateMutex = OpenMutexW(SYNCHRONIZE, false,
|
|
|
+ L"OBSStudioUpdateMutex");
|
|
|
+ if (hObsUpdateMutex) {
|
|
|
+ HANDLE hWait[2];
|
|
|
+ hWait[0] = hObsUpdateMutex;
|
|
|
+ hWait[1] = cancelRequested;
|
|
|
+
|
|
|
+ int i = WaitForMultipleObjects(2, hWait, false, INFINITE);
|
|
|
+
|
|
|
+ if (i == WAIT_OBJECT_0)
|
|
|
+ ReleaseMutex(hObsUpdateMutex);
|
|
|
+
|
|
|
+ CloseHandle(hObsUpdateMutex);
|
|
|
+
|
|
|
+ if (i == WAIT_OBJECT_0 + 1)
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Init crypt stuff */
|
|
|
+
|
|
|
+ CryptProvider hProvider;
|
|
|
+ if (!CryptAcquireContext(&hProvider, nullptr, MS_ENH_RSA_AES_PROV,
|
|
|
+ PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
|
|
|
+ SetDlgItemTextW(hwndMain, IDC_STATUS,
|
|
|
+ L"Update failed: CryptAcquireContext failure");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::hProvider = hProvider;
|
|
|
+
|
|
|
+ /* ------------------------------------- */
|
|
|
+
|
|
|
+ SetDlgItemTextW(hwndMain, IDC_STATUS,
|
|
|
+ L"Searching for available updates...");
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Check if updating portable build */
|
|
|
+
|
|
|
+ bool bIsPortable = false;
|
|
|
+
|
|
|
+ if (cmdLine[0]) {
|
|
|
+ int argc;
|
|
|
+ LPWSTR *argv = CommandLineToArgvW(cmdLine, &argc);
|
|
|
+
|
|
|
+ if (argv) {
|
|
|
+ for (int i = 0; i < argc; i++) {
|
|
|
+ if (wcscmp(argv[i], L"Portable") == 0) {
|
|
|
+ bIsPortable = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalFree((HLOCAL)argv);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Get config path */
|
|
|
+
|
|
|
+ wchar_t lpAppDataPath[MAX_PATH];
|
|
|
+ lpAppDataPath[0] = 0;
|
|
|
+
|
|
|
+ if (bIsPortable) {
|
|
|
+ GetCurrentDirectory(_countof(lpAppDataPath), lpAppDataPath);
|
|
|
+ StringCbCat(lpAppDataPath, sizeof(lpAppDataPath), L"\\config");
|
|
|
+ } else {
|
|
|
+ CoTaskMemPtr<wchar_t> pOut;
|
|
|
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData,
|
|
|
+ KF_FLAG_DEFAULT, nullptr, &pOut);
|
|
|
+ if (hr != S_OK) {
|
|
|
+ Status(L"Update failed: Could not determine AppData "
|
|
|
+ L"location");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ StringCbCopy(lpAppDataPath, sizeof(lpAppDataPath), pOut);
|
|
|
+ StringCbCat(lpAppDataPath, sizeof(lpAppDataPath),
|
|
|
+ L"\\obs-studio");
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Get download path */
|
|
|
+
|
|
|
+ wchar_t manifestPath[MAX_PATH];
|
|
|
+ wchar_t tempDirName[MAX_PATH];
|
|
|
+
|
|
|
+ manifestPath[0] = 0;
|
|
|
+ tempDirName[0] = 0;
|
|
|
+
|
|
|
+ StringCbPrintf(manifestPath, sizeof(manifestPath),
|
|
|
+ L"%s\\updates\\manifest.json", lpAppDataPath);
|
|
|
+ if (!GetTempPathW(_countof(tempPath), tempPath)) {
|
|
|
+ Status(L"Update failed: Failed to get temp path: %ld",
|
|
|
+ GetLastError());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!GetTempFileNameW(tempDirName, L"obs-studio", 0, tempDirName)) {
|
|
|
+ Status(L"Update failed: Failed to create temp dir name: %ld",
|
|
|
+ GetLastError());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ StringCbCat(tempPath, sizeof(tempPath), tempDirName);
|
|
|
+ CreateDirectory(tempPath, nullptr);
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Load manifest file */
|
|
|
+
|
|
|
+ Json root;
|
|
|
+ {
|
|
|
+ string manifestFile = QuickReadFile(manifestPath);
|
|
|
+ if (manifestFile.empty()) {
|
|
|
+ Status(L"Update failed: Couldn't load manifest file");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ json_error_t error;
|
|
|
+ root = json_loads(manifestFile.c_str(), 0, &error);
|
|
|
+
|
|
|
+ if (!root) {
|
|
|
+ Status(L"Update failed: Couldn't parse update "
|
|
|
+ L"manifest: %S", error.text);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!json_is_object(root.get())) {
|
|
|
+ Status(L"Update failed: Invalid update manifest");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Parse current manifest update files */
|
|
|
+
|
|
|
+ json_t *packages = json_object_get(root, "packages");
|
|
|
+ size_t packageCount = json_array_size(packages);
|
|
|
+
|
|
|
+ for (size_t i = 0; i < packageCount; i++) {
|
|
|
+ if (!AddPackageUpdateFiles(packages, i, tempPath)) {
|
|
|
+ Status(L"Failed to process update packages");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Exit if updates already installed */
|
|
|
+
|
|
|
+ if (!updates.size()) {
|
|
|
+ Status(L"All available updates are already installed.");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Generate file hash json */
|
|
|
+
|
|
|
+ Json files(json_array());
|
|
|
+
|
|
|
+ for (update_t &update : updates) {
|
|
|
+ wchar_t whash_string[BLAKE2_HASH_STR_LENGTH];
|
|
|
+ char hash_string[BLAKE2_HASH_STR_LENGTH];
|
|
|
+ char outputPath[MAX_PATH];
|
|
|
+
|
|
|
+ if (!update.has_hash)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ /* check hash */
|
|
|
+ HashToString(update.my_hash, whash_string);
|
|
|
+ if (wcscmp(whash_string, HASH_NULL) == 0)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ if (!WideToUTF8Buf(hash_string, whash_string))
|
|
|
+ continue;
|
|
|
+ if (!WideToUTF8Buf(outputPath, update.basename.c_str()))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ string package_path;
|
|
|
+ package_path = update.packageName;
|
|
|
+ package_path += "/";
|
|
|
+ package_path += outputPath;
|
|
|
+
|
|
|
+ json_t *obj = json_object();
|
|
|
+ json_object_set(obj, "name", json_string(package_path.c_str()));
|
|
|
+ json_object_set(obj, "hash", json_string(hash_string));
|
|
|
+ json_array_append_new(files, obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Send file hashes */
|
|
|
+
|
|
|
+ string newManifest;
|
|
|
+ {
|
|
|
+ char *post_body = json_dumps(files, JSON_COMPACT);
|
|
|
+
|
|
|
+ int responseCode;
|
|
|
+
|
|
|
+ int len = (int)strlen(post_body);
|
|
|
+ uLong compressSize = compressBound(len);
|
|
|
+ string compressedJson;
|
|
|
+
|
|
|
+ compressedJson.resize(compressSize);
|
|
|
+ compress2((Bytef*)&compressedJson[0], &compressSize,
|
|
|
+ (const Bytef*)post_body, len,
|
|
|
+ Z_BEST_COMPRESSION);
|
|
|
+ compressedJson.resize(compressSize);
|
|
|
+
|
|
|
+ bool success = !!HTTPPostData(PATCH_MANIFEST_URL,
|
|
|
+ (BYTE *)&compressedJson[0],
|
|
|
+ (int)compressedJson.size(),
|
|
|
+ L"Accept-Encoding: gzip", &responseCode,
|
|
|
+ newManifest);
|
|
|
+ free(post_body);
|
|
|
+
|
|
|
+ if (!success)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (responseCode != 200) {
|
|
|
+ Status(L"Update failed: HTTP/%d while trying to "
|
|
|
+ L"download patch manifest",
|
|
|
+ responseCode);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Parse new manifest */
|
|
|
+
|
|
|
+ json_error_t error;
|
|
|
+ root = json_loads(newManifest.c_str(), 0, &error);
|
|
|
+ if (!root) {
|
|
|
+ Status(L"Update failed: Couldn't parse patch manifest: %S",
|
|
|
+ error.text);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!json_is_array(root.get())) {
|
|
|
+ Status(L"Update failed: Invalid patch manifest");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ packageCount = json_array_size(root);
|
|
|
+
|
|
|
+ for (size_t i = 0; i < packageCount; i++) {
|
|
|
+ json_t *patch = json_array_get(root, i);
|
|
|
+
|
|
|
+ if (!json_is_object(patch)) {
|
|
|
+ Status(L"Update failed: Invalid patch manifest");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ json_t *name_json = json_object_get(patch, "name");
|
|
|
+ json_t *hash_json = json_object_get(patch, "hash");
|
|
|
+ json_t *source_json = json_object_get(patch, "source");
|
|
|
+ json_t *size_json = json_object_get(patch, "size");
|
|
|
+
|
|
|
+ if (!json_is_string(name_json))
|
|
|
+ continue;
|
|
|
+ if (!json_is_string(hash_json))
|
|
|
+ continue;
|
|
|
+ if (!json_is_string(source_json))
|
|
|
+ continue;
|
|
|
+ if (!json_is_integer(size_json))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ const char *name = json_string_value(name_json);
|
|
|
+ const char *hash = json_string_value(hash_json);
|
|
|
+ const char *source = json_string_value(source_json);
|
|
|
+ int size = (int)json_integer_value(size_json);
|
|
|
+
|
|
|
+ UpdateWithPatchIfAvailable(name, hash, source, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Download Updates */
|
|
|
+
|
|
|
+ if (!RunDownloadWorkers(2))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (completedUpdates != updates.size()) {
|
|
|
+ Status(L"Update failed to download all files.");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------- *
|
|
|
+ * Install updates */
|
|
|
+
|
|
|
+ for (update_t &update : updates) {
|
|
|
+ if (!UpdateFile(update))
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* If we get here, all updates installed successfully so we can purge
|
|
|
+ * the old versions */
|
|
|
+ for (update_t &update : updates) {
|
|
|
+ if (!update.previousFile.empty())
|
|
|
+ DeleteFile(update.previousFile.c_str());
|
|
|
+
|
|
|
+ /* We delete here not above in case of duplicate hashes */
|
|
|
+ if (!update.tempPath.empty())
|
|
|
+ DeleteFile(update.tempPath.c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status(L"Update complete.");
|
|
|
+ SetDlgItemText(hwndMain, IDC_BUTTON, L"Launch OBS");
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+static DWORD WINAPI UpdateThread(void *arg)
|
|
|
+{
|
|
|
+ wchar_t *cmdLine = (wchar_t *)arg;
|
|
|
+
|
|
|
+ bool success = Update(cmdLine);
|
|
|
+
|
|
|
+ if (!success) {
|
|
|
+ /* This handles deleting temp files and rolling back and
|
|
|
+ * partially installed updates */
|
|
|
+ CleanupPartialUpdates();
|
|
|
+
|
|
|
+ if (tempPath[0])
|
|
|
+ RemoveDirectory(tempPath);
|
|
|
+
|
|
|
+ if (WaitForSingleObject(cancelRequested, 0) == WAIT_OBJECT_0)
|
|
|
+ Status(L"Update aborted.");
|
|
|
+
|
|
|
+ SendDlgItemMessage(hwndMain, IDC_PROGRESS, PBM_SETSTATE,
|
|
|
+ PBST_ERROR, 0);
|
|
|
+
|
|
|
+ SetDlgItemText(hwndMain, IDC_BUTTON, L"Exit");
|
|
|
+ EnableWindow(GetDlgItem(hwndMain, IDC_BUTTON), true);
|
|
|
+
|
|
|
+ updateFailed = true;
|
|
|
+ } else {
|
|
|
+ if (tempPath[0])
|
|
|
+ RemoveDirectory(tempPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bExiting)
|
|
|
+ ExitProcess(success);
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static void CancelUpdate(bool quit)
|
|
|
+{
|
|
|
+ if (WaitForSingleObject(updateThread, 0) != WAIT_OBJECT_0) {
|
|
|
+ bExiting = quit;
|
|
|
+ SetEvent(cancelRequested);
|
|
|
+ } else {
|
|
|
+ PostQuitMessage(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static void LaunchOBS()
|
|
|
+{
|
|
|
+ wchar_t cwd[MAX_PATH];
|
|
|
+ wchar_t newCwd[MAX_PATH];
|
|
|
+ wchar_t obsPath[MAX_PATH];
|
|
|
+
|
|
|
+ GetCurrentDirectory(_countof(cwd) - 1, cwd);
|
|
|
+
|
|
|
+ StringCbCopy(obsPath, sizeof(obsPath), cwd);
|
|
|
+ StringCbCat(obsPath, sizeof(obsPath), is32bit
|
|
|
+ ? L"\\bin\\32bit"
|
|
|
+ : L"\\bin\\64bit");
|
|
|
+ SetCurrentDirectory(obsPath);
|
|
|
+ StringCbCopy(newCwd, sizeof(newCwd), obsPath);
|
|
|
+
|
|
|
+ StringCbCat(obsPath, sizeof(obsPath), is32bit
|
|
|
+ ? L"\\obs32.exe"
|
|
|
+ : L"\\obs64.exe");
|
|
|
+
|
|
|
+ if (!FileExists(obsPath)) {
|
|
|
+ StringCbCopy(obsPath, sizeof(obsPath), cwd);
|
|
|
+ StringCbCat(obsPath, sizeof(obsPath), L"\\bin\\32bit");
|
|
|
+ SetCurrentDirectory(obsPath);
|
|
|
+ StringCbCopy(newCwd, sizeof(newCwd), obsPath);
|
|
|
+
|
|
|
+ StringCbCat(obsPath, sizeof(obsPath), L"\\obs32.exe");
|
|
|
+
|
|
|
+ if (!FileExists(obsPath)) {
|
|
|
+ /* TODO: give user a message maybe? */
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ SHELLEXECUTEINFO execInfo;
|
|
|
+
|
|
|
+ ZeroMemory(&execInfo, sizeof(execInfo));
|
|
|
+
|
|
|
+ execInfo.cbSize = sizeof(execInfo);
|
|
|
+ execInfo.lpFile = obsPath;
|
|
|
+ execInfo.lpDirectory = newCwd;
|
|
|
+ execInfo.nShow = SW_SHOWNORMAL;
|
|
|
+
|
|
|
+ ShellExecuteEx(&execInfo);
|
|
|
+}
|
|
|
+
|
|
|
+static INT_PTR CALLBACK UpdateDialogProc(HWND hwnd, UINT message,
|
|
|
+ WPARAM wParam, LPARAM lParam)
|
|
|
+{
|
|
|
+ switch (message) {
|
|
|
+ case WM_INITDIALOG: {
|
|
|
+ static HICON hMainIcon = LoadIcon(hinstMain,
|
|
|
+ MAKEINTRESOURCE(IDI_ICON1));
|
|
|
+ SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hMainIcon);
|
|
|
+ SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hMainIcon);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ case WM_COMMAND:
|
|
|
+ if (LOWORD(wParam) == IDC_BUTTON) {
|
|
|
+ if (HIWORD(wParam) == BN_CLICKED) {
|
|
|
+ DWORD result = WaitForSingleObject(
|
|
|
+ updateThread, 0);
|
|
|
+ if (result == WAIT_OBJECT_0) {
|
|
|
+ if (updateFailed)
|
|
|
+ PostQuitMessage(0);
|
|
|
+ else
|
|
|
+ PostQuitMessage(1);
|
|
|
+ } else {
|
|
|
+ EnableWindow((HWND)lParam, false);
|
|
|
+ CancelUpdate(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+
|
|
|
+ case WM_CLOSE:
|
|
|
+ CancelUpdate(true);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+static void RestartAsAdmin(LPWSTR lpCmdLine)
|
|
|
+{
|
|
|
+ wchar_t myPath[MAX_PATH];
|
|
|
+ if (!GetModuleFileNameW(nullptr, myPath, _countof(myPath) - 1)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ wchar_t cwd[MAX_PATH];
|
|
|
+ GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
|
|
|
+
|
|
|
+ SHELLEXECUTEINFO shExInfo = {0};
|
|
|
+ shExInfo.cbSize = sizeof(shExInfo);
|
|
|
+ shExInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
|
|
|
+ shExInfo.hwnd = 0;
|
|
|
+ shExInfo.lpVerb = L"runas"; /* Operation to perform */
|
|
|
+ shExInfo.lpFile = myPath; /* Application to start */
|
|
|
+ shExInfo.lpParameters = lpCmdLine; /* Additional parameters */
|
|
|
+ shExInfo.lpDirectory = cwd;
|
|
|
+ shExInfo.nShow = SW_NORMAL;
|
|
|
+ shExInfo.hInstApp = 0;
|
|
|
+
|
|
|
+ /* annoyingly the actual elevated updater will disappear behind other
|
|
|
+ * windows :( */
|
|
|
+ AllowSetForegroundWindow(ASFW_ANY);
|
|
|
+
|
|
|
+ if (ShellExecuteEx(&shExInfo)) {
|
|
|
+ DWORD exitCode;
|
|
|
+
|
|
|
+ if (GetExitCodeProcess(shExInfo.hProcess, &exitCode)) {
|
|
|
+ if (exitCode == 1) {
|
|
|
+ LaunchOBS();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ CloseHandle(shExInfo.hProcess);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
|
|
|
+{
|
|
|
+ INITCOMMONCONTROLSEX icce;
|
|
|
+
|
|
|
+ if (!IsAppRunningAsAdminMode()) {
|
|
|
+ HANDLE hLowMutex = CreateMutexW(nullptr, true,
|
|
|
+ L"OBSUpdaterRunningAsNonAdminUser");
|
|
|
+
|
|
|
+ RestartAsAdmin(lpCmdLine);
|
|
|
+
|
|
|
+ if (hLowMutex) {
|
|
|
+ ReleaseMutex(hLowMutex);
|
|
|
+ CloseHandle(hLowMutex);
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ } else {
|
|
|
+ {
|
|
|
+ wchar_t cwd[MAX_PATH];
|
|
|
+ wchar_t newPath[MAX_PATH];
|
|
|
+ GetCurrentDirectoryW(_countof(cwd) - 1, cwd);
|
|
|
+
|
|
|
+ is32bit = wcsstr(cwd, L"bin\\32bit") != nullptr;
|
|
|
+ StringCbCat(cwd, sizeof(cwd), L"\\..\\..");
|
|
|
+
|
|
|
+ GetFullPathName(cwd, _countof(newPath), newPath,
|
|
|
+ nullptr);
|
|
|
+ SetCurrentDirectory(newPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ hinstMain = hInstance;
|
|
|
+
|
|
|
+ icce.dwSize = sizeof(icce);
|
|
|
+ icce.dwICC = ICC_PROGRESS_CLASS;
|
|
|
+
|
|
|
+ InitCommonControlsEx(&icce);
|
|
|
+
|
|
|
+ hwndMain = CreateDialog(hInstance,
|
|
|
+ MAKEINTRESOURCE(IDD_UPDATEDIALOG), nullptr,
|
|
|
+ UpdateDialogProc);
|
|
|
+ if (!hwndMain) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ ShowWindow(hwndMain, SW_SHOWNORMAL);
|
|
|
+ SetForegroundWindow(hwndMain);
|
|
|
+
|
|
|
+ cancelRequested = CreateEvent(nullptr, true, false, nullptr);
|
|
|
+ updateThread = CreateThread(nullptr, 0, UpdateThread,
|
|
|
+ lpCmdLine, 0, nullptr);
|
|
|
+
|
|
|
+ MSG msg;
|
|
|
+ while (GetMessage(&msg, nullptr, 0, 0)) {
|
|
|
+ if (!IsDialogMessage(hwndMain, &msg)) {
|
|
|
+ TranslateMessage(&msg);
|
|
|
+ DispatchMessage(&msg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* there is no non-elevated process waiting for us if UAC is
|
|
|
+ * disabled */
|
|
|
+ WinHandle hMutex = OpenMutex(SYNCHRONIZE, false,
|
|
|
+ L"OBSUpdaterRunningAsNonAdminUser");
|
|
|
+ if (msg.wParam == 1 && !hMutex) {
|
|
|
+ LaunchOBS();
|
|
|
+ }
|
|
|
+
|
|
|
+ return (int)msg.wParam;
|
|
|
+ }
|
|
|
+}
|