Release v0.2.1: support remote input, safe updates and high-DPI windows
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
#include "update.h"
|
||||
#include "json.hpp"
|
||||
|
||||
namespace ks
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Handle
|
||||
{
|
||||
HANDLE value{};
|
||||
~Handle() { if (value) CloseHandle(value); }
|
||||
};
|
||||
std::wstring quoteArgument(const std::wstring &value)
|
||||
{
|
||||
// Windows argv escaping, including a path ending with a backslash.
|
||||
std::wstring out = L"\"";
|
||||
size_t slashes = 0;
|
||||
for (auto ch : value)
|
||||
{
|
||||
if (ch == L'\\') { ++slashes; continue; }
|
||||
out.append(ch == L'"' ? slashes * 2 + 1 : slashes, L'\\');
|
||||
slashes = 0;
|
||||
out += ch;
|
||||
}
|
||||
out.append(slashes * 2, L'\\');
|
||||
return out + L'"';
|
||||
}
|
||||
}
|
||||
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion)
|
||||
{
|
||||
UpdateResult result;
|
||||
GUID guid{};
|
||||
if (FAILED(CoCreateGuid(&guid))) throw std::runtime_error("Cannot create update identifier");
|
||||
wchar_t id[40]{};
|
||||
StringFromGUID2(guid, id, 40);
|
||||
auto work = std::filesystem::temp_directory_path() / (L"KSwitcher-update-" + std::wstring(id));
|
||||
std::filesystem::create_directory(work);
|
||||
bool detached = false;
|
||||
Handle child;
|
||||
try
|
||||
{
|
||||
HRSRC resource = FindResourceW(nullptr, MAKEINTRESOURCEW(201), RT_RCDATA);
|
||||
HGLOBAL loaded = resource ? LoadResource(nullptr, resource) : nullptr;
|
||||
auto bytes = loaded ? static_cast<const unsigned char *>(LockResource(loaded)) : nullptr;
|
||||
if (!bytes) throw std::runtime_error("The updater resource is missing");
|
||||
atomicWrite(work / L"update.ps1", Bytes(bytes, bytes + SizeofResource(nullptr, resource)));
|
||||
std::wstring readyName = L"Local\\KSwitcher.Update.Ready." + std::wstring(id);
|
||||
Handle ready{CreateEventW(nullptr, TRUE, FALSE, readyName.c_str())};
|
||||
if (!ready.value) throw std::runtime_error("Cannot create updater handshake");
|
||||
wchar_t system[MAX_PATH]{};
|
||||
if (!GetSystemDirectoryW(system, MAX_PATH)) throw std::runtime_error("Cannot locate Windows PowerShell");
|
||||
auto powershell = std::filesystem::path(system) / L"WindowsPowerShell/v1.0/powershell.exe";
|
||||
std::wstring command = quoteArgument(powershell.wstring()) + L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
|
||||
quoteArgument((work / L"update.ps1").wstring()) + L" -Mode " + (install ? L"Install" : L"Check") +
|
||||
L" -CurrentVersion " + quoteArgument(AppVersion) + L" -WorkDirectory " + quoteArgument(work.wstring()) +
|
||||
L" -InstallDirectory " + quoteArgument(std::filesystem::path(executable()).parent_path().wstring()) +
|
||||
L" -ParentProcessId " + std::to_wstring(GetCurrentProcessId()) + L" -ReadyEvent " + quoteArgument(readyName);
|
||||
if (install) command += L" -ExpectedVersion " + quoteArgument(expectedVersion);
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(powershell.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, work.c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start Windows PowerShell updater");
|
||||
child.value = process.hProcess;
|
||||
Handle thread{process.hThread};
|
||||
HANDLE waits[]{ready.value, child.value};
|
||||
DWORD waited = WaitForMultipleObjects(2, waits, FALSE, 240000);
|
||||
if (waited != WAIT_OBJECT_0 && waited != WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
// The helper cannot replace files while this process is alive.
|
||||
TerminateProcess(child.value, 1);
|
||||
WaitForSingleObject(child.value, 5000);
|
||||
throw std::runtime_error("Update preparation timed out; the installed application was not changed");
|
||||
}
|
||||
auto data = readFile(work / L"result.json", 64 * 1024);
|
||||
auto json = nlohmann::json::parse(data.begin(), data.end());
|
||||
result.state = wide(json.at("state").get<std::string>());
|
||||
result.version = wide(json.value("version", std::string{}));
|
||||
result.error = wide(json.value("error", std::string{}));
|
||||
detached = install && waited == WAIT_OBJECT_0 && result.state == L"prepared";
|
||||
if (result.state == L"prepared" && !detached)
|
||||
throw std::runtime_error("Update helper exited before installation");
|
||||
if (!detached) std::filesystem::remove_all(work);
|
||||
return result;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (!detached && child.value && WaitForSingleObject(child.value, 0) == WAIT_TIMEOUT)
|
||||
{
|
||||
TerminateProcess(child.value, 1);
|
||||
WaitForSingleObject(child.value, 5000);
|
||||
}
|
||||
if (!detached) { std::error_code ignored; std::filesystem::remove_all(work, ignored); }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void confirmUpdatedStartup(const std::wstring &eventName)
|
||||
{
|
||||
if (!eventName.starts_with(L"Local\\KSwitcher.Update.Started.")) return;
|
||||
Handle event{OpenEventW(EVENT_MODIFY_STATE, FALSE, eventName.c_str())};
|
||||
if (event.value) SetEvent(event.value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user