mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-26 07:14:33 +08:00
[feat] add in-app updates for desktop installations
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#include "updater/update_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk::updater {
|
||||
namespace {
|
||||
std::string LockPath(const fs::path& application) {
|
||||
return "/tmp/CrossDesk-update-" + Sha256(fs::weakly_canonical(application).string()) + ".lock";
|
||||
}
|
||||
std::vector<char*> Argv(std::vector<std::string>& args) {
|
||||
std::vector<char*> values;
|
||||
for (auto& argument : args) values.push_back(argument.data());
|
||||
values.push_back(nullptr);
|
||||
return values;
|
||||
}
|
||||
int OpenLock(const std::string& path, bool create) {
|
||||
constexpr int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW;
|
||||
int fd = open(path.c_str(), flags | (create ? O_CREAT | O_EXCL : 0), 0644);
|
||||
if (fd < 0 && create && errno == EEXIST) fd = open(path.c_str(), flags);
|
||||
if (fd >= 0) {
|
||||
struct stat info{};
|
||||
if (fstat(fd, &info) || !S_ISREG(info.st_mode)) {
|
||||
close(fd); errno = EINVAL; return -1;
|
||||
}
|
||||
// A restrictive umask must not leave a stale lock file that permanently
|
||||
// prevents other users from probing whether an update is active.
|
||||
if (create && info.st_uid == geteuid() && fchmod(fd, 0644)) {
|
||||
const int error = errno; close(fd); errno = error; return -1;
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool IsPrivileged() { return geteuid() == 0; }
|
||||
bool OwnedJobDirectory(const fs::path& job) {
|
||||
struct stat info{};
|
||||
return lstat(job.c_str(), &info) == 0 && S_ISDIR(info.st_mode) &&
|
||||
info.st_uid == geteuid() && (info.st_mode & 0077) == 0;
|
||||
}
|
||||
fs::path PrivateDirectory() {
|
||||
// Ignore TMPDIR in privileged processes. mkdtemp creates a private 0700
|
||||
// directory; installation only consumes a verified copy inside this directory.
|
||||
std::string pattern = "/tmp/crossdesk-update-XXXXXX";
|
||||
if (!mkdtemp(pattern.data())) throw std::runtime_error("Cannot create private update directory");
|
||||
return fs::path(pattern);
|
||||
}
|
||||
fs::path HelperPath(const fs::path& application) {
|
||||
return application.parent_path() / "crossdesk_updater";
|
||||
}
|
||||
std::shared_ptr<void> PinHelper(const fs::path&) { return {}; }
|
||||
void RestoreServices() {}
|
||||
|
||||
int Run(const fs::path& executable, const std::vector<std::string>& arguments, bool elevate) {
|
||||
// Platform-specific elevation builds a fixed argv and calls this with false.
|
||||
if (elevate) throw std::runtime_error("Use platform elevation entry point");
|
||||
std::vector<std::string> args{executable.string()};
|
||||
args.insert(args.end(), arguments.begin(), arguments.end());
|
||||
auto argv = Argv(args);
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) throw std::runtime_error("Cannot start installer");
|
||||
if (pid == 0) {
|
||||
execv(executable.c_str(), argv.data());
|
||||
_exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
pid_t result;
|
||||
do { result = waitpid(pid, &status, 0); } while (result < 0 && errno == EINTR);
|
||||
return result == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
||||
}
|
||||
|
||||
void Launch(const fs::path& executable, const std::vector<std::string>& arguments) {
|
||||
std::vector<std::string> args{executable.string()};
|
||||
args.insert(args.end(), arguments.begin(), arguments.end());
|
||||
auto argv = Argv(args);
|
||||
int errors[2];
|
||||
if (pipe(errors)) throw std::runtime_error("Cannot create launch pipe");
|
||||
fcntl(errors[0], F_SETFD, FD_CLOEXEC);
|
||||
fcntl(errors[1], F_SETFD, FD_CLOEXEC);
|
||||
const pid_t child = fork();
|
||||
if (child == 0) {
|
||||
close(errors[0]);
|
||||
if (setsid() < 0) _exit(127);
|
||||
const pid_t detached = fork();
|
||||
if (detached < 0) _exit(127);
|
||||
if (detached > 0) _exit(0);
|
||||
const int null = open("/dev/null", O_RDWR);
|
||||
if (null >= 0) {
|
||||
dup2(null, STDIN_FILENO); dup2(null, STDOUT_FILENO); dup2(null, STDERR_FILENO);
|
||||
if (null > STDERR_FILENO) close(null);
|
||||
}
|
||||
execv(executable.c_str(), argv.data());
|
||||
const int error = errno;
|
||||
(void)write(errors[1], &error, sizeof(error));
|
||||
_exit(127);
|
||||
}
|
||||
close(errors[1]);
|
||||
if (child < 0) { close(errors[0]); throw std::runtime_error("Cannot launch update helper"); }
|
||||
int status = 0;
|
||||
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
||||
int error = 0;
|
||||
ssize_t bytes;
|
||||
do { bytes = read(errors[0], &error, sizeof(error)); } while (bytes < 0 && errno == EINTR);
|
||||
close(errors[0]);
|
||||
if (bytes != 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
||||
throw std::runtime_error("Cannot execute update helper");
|
||||
}
|
||||
|
||||
MaintenanceLock::MaintenanceLock(const fs::path& application) {
|
||||
const int fd = OpenLock(LockPath(application), true);
|
||||
if (fd < 0) throw std::runtime_error("Cannot open update lock");
|
||||
if (flock(fd, LOCK_EX | LOCK_NB)) { close(fd); throw std::runtime_error("An update is already running"); }
|
||||
handle_ = fd;
|
||||
}
|
||||
MaintenanceLock::~MaintenanceLock() { if (handle_ != -1) close(static_cast<int>(handle_)); }
|
||||
bool MaintenanceActive(const fs::path& application) {
|
||||
const int fd = OpenLock(LockPath(application), false);
|
||||
if (fd < 0) return errno != ENOENT;
|
||||
const bool locked = flock(fd, LOCK_SH | LOCK_NB) != 0;
|
||||
close(fd);
|
||||
return locked;
|
||||
}
|
||||
} // namespace crossdesk::updater
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "updater/update_platform.h"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk::updater {
|
||||
fs::path ExecutablePath() { return fs::read_symlink("/proc/self/exe"); }
|
||||
bool SupportedInstallation(const fs::path& application) {
|
||||
return application == fs::path("/usr/lib/crossdesk/crossdesk-bin") &&
|
||||
!PlatformKey(application).empty() && fs::is_regular_file(HelperPath(application)) &&
|
||||
access("/usr/bin/pkexec", X_OK) == 0 && access("/usr/bin/apt-get", X_OK) == 0;
|
||||
}
|
||||
int RunElevated(const fs::path& executable, const fs::path& job) {
|
||||
return Run("/usr/bin/pkexec", {executable.string(), "--install", job.string()});
|
||||
}
|
||||
void WaitForApplicationExit(const fs::path& application) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(45);
|
||||
do {
|
||||
bool running = false;
|
||||
for (const auto& entry : fs::directory_iterator("/proc")) {
|
||||
const auto name = entry.path().filename().string();
|
||||
if (name.empty() || name.find_first_not_of("0123456789") != std::string::npos) continue;
|
||||
std::error_code error;
|
||||
const auto executable = fs::read_symlink(entry.path() / "exe", error);
|
||||
if (!error && executable == application) running = true;
|
||||
// Fail closed when a matching process cannot be inspected. A disappeared
|
||||
// process has no comm file and does not block installation.
|
||||
if (error) {
|
||||
std::ifstream comm(entry.path() / "comm");
|
||||
std::string process_name;
|
||||
std::getline(comm, process_name);
|
||||
if (process_name == "crossdesk-bin") running = true;
|
||||
}
|
||||
}
|
||||
if (!running) return;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
throw std::runtime_error("CrossDesk is still running; stop other sessions or its supervisor and retry");
|
||||
}
|
||||
void InstallPackage(const fs::path& package, const fs::path&) {
|
||||
// No shell and no automatic dependency removal. apt owns dpkg locking and
|
||||
// dependency resolution; a busy/broken package manager is reported as failure.
|
||||
setenv("DEBIAN_FRONTEND", "noninteractive", 1);
|
||||
if (Run("/usr/bin/apt-get", {"install", "--yes", "--no-remove",
|
||||
"-o", "Dpkg::Options::=--force-confold", package.string()}) != 0)
|
||||
throw std::runtime_error("The package manager could not install the update");
|
||||
}
|
||||
void Restart(const fs::path&) { Launch("/usr/bin/crossdesk", {}); }
|
||||
} // namespace crossdesk::updater
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "updater/update_platform.h"
|
||||
|
||||
#include <libproc.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <unistd.h>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk::updater {
|
||||
namespace {
|
||||
std::string ShellQuote(const std::string& value) {
|
||||
std::string result = "'";
|
||||
for (char c : value) result += c == '\'' ? "'\\''" : std::string(1, c);
|
||||
return result + "'";
|
||||
}
|
||||
std::string AppleScriptString(const std::string& value) {
|
||||
std::string result = "\"";
|
||||
for (char c : value) {
|
||||
if (c == '\\' || c == '"') result += '\\';
|
||||
result += c;
|
||||
}
|
||||
return result + "\"";
|
||||
}
|
||||
} // namespace
|
||||
fs::path ExecutablePath() {
|
||||
uint32_t size = 0;
|
||||
_NSGetExecutablePath(nullptr, &size);
|
||||
std::vector<char> buffer(size);
|
||||
if (_NSGetExecutablePath(buffer.data(), &size)) throw std::runtime_error("Cannot locate executable");
|
||||
return fs::canonical(buffer.data());
|
||||
}
|
||||
bool SupportedInstallation(const fs::path& application) {
|
||||
return application == fs::path("/Applications/CrossDesk.app/Contents/MacOS/CrossDesk") &&
|
||||
fs::is_regular_file(HelperPath(application));
|
||||
}
|
||||
int RunElevated(const fs::path& executable, const fs::path& job) {
|
||||
const auto command = ShellQuote(executable.string()) + " --install " + ShellQuote(job.string());
|
||||
return Run("/usr/bin/osascript", {"-e", "do shell script " +
|
||||
AppleScriptString(command) + " with administrator privileges"});
|
||||
}
|
||||
void WaitForApplicationExit(const fs::path& application) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(45);
|
||||
do {
|
||||
const int count = proc_listallpids(nullptr, 0);
|
||||
if (count <= 0) throw std::runtime_error("Cannot enumerate CrossDesk processes");
|
||||
std::vector<pid_t> pids(static_cast<std::size_t>(count) + 256);
|
||||
const int length = proc_listallpids(pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
|
||||
if (length <= 0 || length >= static_cast<int>(pids.size()))
|
||||
throw std::runtime_error("Cannot inspect all CrossDesk processes");
|
||||
bool running = false;
|
||||
for (int i = 0; i < length; ++i) {
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE]{};
|
||||
if (proc_pidpath(pids[i], path, sizeof(path)) > 0) {
|
||||
if (fs::path(path) == application) running = true;
|
||||
} else {
|
||||
char name[1024]{};
|
||||
if (proc_name(pids[i], name, sizeof(name)) > 0 && std::string(name) == "CrossDesk")
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
if (!running) return;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
throw std::runtime_error("CrossDesk is still running in another session");
|
||||
}
|
||||
void InstallPackage(const fs::path& package, const fs::path&) {
|
||||
if (Run("/usr/sbin/installer", {"-pkg", package.string(), "-target", "/"}) != 0)
|
||||
throw std::runtime_error("The macOS installer could not install the update");
|
||||
}
|
||||
void Restart(const fs::path&) { Launch("/usr/bin/open", {"/Applications/CrossDesk.app"}); }
|
||||
} // namespace crossdesk::updater
|
||||
@@ -0,0 +1,303 @@
|
||||
#include "updater/update_platform.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <sddl.h>
|
||||
#include <aclapi.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cwctype>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk::updater {
|
||||
namespace {
|
||||
bool service_was_running = false;
|
||||
|
||||
std::wstring Wide(const std::string& text) { return fs::u8path(text).wstring(); }
|
||||
|
||||
std::wstring Quote(const std::wstring& value) {
|
||||
std::wstring result = L"\"";
|
||||
unsigned slashes = 0;
|
||||
for (wchar_t c : value) {
|
||||
if (c == L'\\') { ++slashes; continue; }
|
||||
result.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\');
|
||||
slashes = 0;
|
||||
result += c;
|
||||
}
|
||||
result.append(slashes * 2, L'\\');
|
||||
return result + L"\"";
|
||||
}
|
||||
|
||||
std::wstring Arguments(const std::vector<std::string>& args) {
|
||||
std::wstring result;
|
||||
for (const auto& argument : args) {
|
||||
if (!result.empty()) result += L' ';
|
||||
result += Quote(Wide(argument));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring Normalized(const fs::path& path) {
|
||||
auto value = fs::weakly_canonical(path).wstring();
|
||||
std::transform(value.begin(), value.end(), value.begin(), towlower);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring LockName(const fs::path& app) {
|
||||
return L"Global\\CrossDeskUpdate-" + Wide(Sha256(fs::path(Normalized(app)).u8string()));
|
||||
}
|
||||
|
||||
void StopService() {
|
||||
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
|
||||
if (!manager) throw std::runtime_error("Cannot query CrossDesk service");
|
||||
SC_HANDLE service = OpenServiceW(manager, L"CrossDeskService", SERVICE_STOP | SERVICE_QUERY_STATUS);
|
||||
const DWORD error = GetLastError();
|
||||
CloseServiceHandle(manager);
|
||||
if (!service) {
|
||||
if (error == ERROR_SERVICE_DOES_NOT_EXIST) return;
|
||||
throw std::runtime_error("Cannot open CrossDesk service");
|
||||
}
|
||||
SERVICE_STATUS_PROCESS status{};
|
||||
DWORD bytes = 0;
|
||||
if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
|
||||
reinterpret_cast<BYTE*>(&status), sizeof(status), &bytes)) {
|
||||
CloseServiceHandle(service);
|
||||
throw std::runtime_error("Cannot query CrossDesk service state");
|
||||
}
|
||||
service_was_running = status.dwCurrentState == SERVICE_RUNNING ||
|
||||
status.dwCurrentState == SERVICE_START_PENDING;
|
||||
SERVICE_STATUS ignored{};
|
||||
if (status.dwCurrentState != SERVICE_STOPPED && status.dwCurrentState != SERVICE_STOP_PENDING &&
|
||||
!ControlService(service, SERVICE_CONTROL_STOP, &ignored)) {
|
||||
CloseServiceHandle(service);
|
||||
throw std::runtime_error("Cannot stop CrossDesk service");
|
||||
}
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
|
||||
reinterpret_cast<BYTE*>(&status), sizeof(status), &bytes)) break;
|
||||
if (status.dwCurrentState == SERVICE_STOPPED) {
|
||||
CloseServiceHandle(service);
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
CloseServiceHandle(service);
|
||||
throw std::runtime_error("CrossDesk service did not stop");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
fs::path ExecutablePath() {
|
||||
wchar_t buffer[32768];
|
||||
const DWORD size = GetModuleFileNameW(nullptr, buffer, 32768);
|
||||
if (!size || size >= 32768) throw std::runtime_error("Cannot locate executable");
|
||||
return fs::path(std::wstring(buffer, size));
|
||||
}
|
||||
|
||||
fs::path HelperPath(const fs::path& application) {
|
||||
return application.parent_path() / L"crossdesk_updater.exe";
|
||||
}
|
||||
|
||||
bool SupportedInstallation(const fs::path& application) {
|
||||
// Only the installer owns this value. Portable/development directories cannot
|
||||
// become destinations for an elevated update.
|
||||
wchar_t directory[32768];
|
||||
DWORD bytes = sizeof(directory);
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\CrossDesk",
|
||||
L"InstallLocation", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6432KEY, nullptr,
|
||||
directory, &bytes) != ERROR_SUCCESS) return false;
|
||||
return Normalized(fs::path(directory) / L"CrossDesk.exe") == Normalized(application) &&
|
||||
fs::is_regular_file(HelperPath(application));
|
||||
}
|
||||
|
||||
bool IsPrivileged() {
|
||||
SID_IDENTIFIER_AUTHORITY authority = SECURITY_NT_AUTHORITY;
|
||||
PSID admins = nullptr;
|
||||
BOOL member = FALSE;
|
||||
if (AllocateAndInitializeSid(&authority, 2, SECURITY_BUILTIN_DOMAIN_RID,
|
||||
DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &admins)) {
|
||||
CheckTokenMembership(nullptr, admins, &member);
|
||||
FreeSid(admins);
|
||||
}
|
||||
return member != FALSE;
|
||||
}
|
||||
|
||||
fs::path PrivateDirectory() {
|
||||
PWSTR base = nullptr;
|
||||
if (FAILED(SHGetKnownFolderPath(IsPrivileged() ? FOLDERID_ProgramData : FOLDERID_LocalAppData,
|
||||
0, nullptr, &base))) throw std::runtime_error("Cannot locate update storage");
|
||||
const fs::path directory = fs::path(base) / (L"CrossDesk-update-" + Wide(RandomId()));
|
||||
CoTaskMemFree(base);
|
||||
std::wstring acl = IsPrivileged() ? L"O:BAG:BAD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)"
|
||||
: L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
|
||||
if (!IsPrivileged()) {
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
throw std::runtime_error("Cannot read update owner");
|
||||
DWORD bytes = 0;
|
||||
GetTokenInformation(token, TokenUser, nullptr, 0, &bytes);
|
||||
std::vector<unsigned char> data(bytes);
|
||||
const BOOL ok = GetTokenInformation(token, TokenUser, data.data(), bytes, &bytes);
|
||||
CloseHandle(token);
|
||||
if (!ok) throw std::runtime_error("Cannot read update owner");
|
||||
PWSTR sid = nullptr;
|
||||
if (!ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(data.data())->User.Sid, &sid))
|
||||
throw std::runtime_error("Cannot format update owner");
|
||||
acl += L"(A;OICI;FA;;;" + std::wstring(sid) + L")";
|
||||
LocalFree(sid);
|
||||
}
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(acl.c_str(), SDDL_REVISION_1,
|
||||
&descriptor, nullptr)) throw std::runtime_error("Cannot protect update storage");
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(attributes), descriptor, FALSE};
|
||||
const BOOL created = CreateDirectoryW(directory.c_str(), &attributes);
|
||||
LocalFree(descriptor);
|
||||
if (!created) throw std::runtime_error("Cannot create private update directory");
|
||||
return directory;
|
||||
}
|
||||
|
||||
bool OwnedJobDirectory(const fs::path& job) {
|
||||
if (!IsPrivileged()) return true;
|
||||
// Elevated coordinators only write to a directory owned by Administrators,
|
||||
// never a caller-controlled directory with reparse points or writable ACLs.
|
||||
PSID owner = nullptr;
|
||||
PACL acl = nullptr;
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (GetNamedSecurityInfoW(job.c_str(), SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&owner, nullptr, &acl, nullptr, &descriptor) != ERROR_SUCCESS) return false;
|
||||
bool safe = owner && IsWellKnownSid(owner, WinBuiltinAdministratorsSid) && acl;
|
||||
for (DWORD i = 0; safe && i < acl->AceCount; ++i) {
|
||||
void* raw = nullptr;
|
||||
if (!GetAce(acl, i, &raw)) { safe = false; break; }
|
||||
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
|
||||
if (ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE ||
|
||||
(!IsWellKnownSid(&ace->SidStart, WinBuiltinAdministratorsSid) &&
|
||||
!IsWellKnownSid(&ace->SidStart, WinLocalSystemSid))) safe = false;
|
||||
}
|
||||
LocalFree(descriptor);
|
||||
return safe;
|
||||
}
|
||||
|
||||
void Launch(const fs::path& executable, const std::vector<std::string>& arguments) {
|
||||
auto command = Quote(executable.wstring()) + L" " + Arguments(arguments);
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
const auto cwd = executable.parent_path();
|
||||
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, FALSE,
|
||||
CREATE_NO_WINDOW, nullptr, cwd.c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start update process");
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
}
|
||||
|
||||
int Run(const fs::path& executable, const std::vector<std::string>& arguments, bool elevate) {
|
||||
const auto parameters = Arguments(arguments);
|
||||
const auto cwd = executable.parent_path();
|
||||
SHELLEXECUTEINFOW execution{sizeof(execution)};
|
||||
execution.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI;
|
||||
execution.lpVerb = elevate ? L"runas" : L"open";
|
||||
execution.lpFile = executable.c_str();
|
||||
execution.lpParameters = parameters.c_str();
|
||||
execution.lpDirectory = cwd.c_str();
|
||||
execution.nShow = SW_HIDE;
|
||||
if (!ShellExecuteExW(&execution) || !execution.hProcess)
|
||||
throw std::runtime_error("Installation authorization was cancelled or could not start");
|
||||
WaitForSingleObject(execution.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(execution.hProcess, &code);
|
||||
CloseHandle(execution.hProcess);
|
||||
return static_cast<int>(code);
|
||||
}
|
||||
|
||||
int RunElevated(const fs::path& executable, const fs::path& job) {
|
||||
return Run(executable, {"--install", job.u8string()}, true);
|
||||
}
|
||||
|
||||
void WaitForApplicationExit(const fs::path& application) {
|
||||
StopService();
|
||||
const auto expected = Normalized(application);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(45);
|
||||
do {
|
||||
bool running = false;
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) throw std::runtime_error("Cannot enumerate application processes");
|
||||
PROCESSENTRY32W entry{sizeof(entry)};
|
||||
if (Process32FirstW(snapshot, &entry)) do {
|
||||
if (_wcsicmp(entry.szExeFile, application.filename().c_str()) != 0) continue;
|
||||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE,
|
||||
FALSE, entry.th32ProcessID);
|
||||
if (!process) { running = true; continue; }
|
||||
wchar_t image[32768];
|
||||
DWORD size = 32768;
|
||||
if (!QueryFullProcessImageNameW(process, 0, image, &size)) running = true;
|
||||
else if (Normalized(fs::path(std::wstring(image, size))) == expected &&
|
||||
WaitForSingleObject(process, 0) == WAIT_TIMEOUT) running = true;
|
||||
CloseHandle(process);
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
CloseHandle(snapshot);
|
||||
if (!running) return;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
throw std::runtime_error("CrossDesk is still running in this or another session");
|
||||
}
|
||||
|
||||
void InstallPackage(const fs::path& package, const fs::path& application) {
|
||||
// NSIS requires /D to be the final argument, without quotes around its value.
|
||||
std::wstring command = Quote(package.wstring()) + L" /S /CROSSDESK_UPDATE /D=" +
|
||||
application.parent_path().wstring();
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
const auto cwd = package.parent_path();
|
||||
if (!CreateProcessW(package.c_str(), command.data(), nullptr, nullptr, FALSE,
|
||||
CREATE_NO_WINDOW, nullptr, cwd.c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start CrossDesk installer");
|
||||
WaitForSingleObject(process.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(process.hProcess, &code);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
if (code != 0) throw std::runtime_error("CrossDesk installer failed: " + std::to_string(code));
|
||||
}
|
||||
|
||||
void RestoreServices() {
|
||||
if (!service_was_running) return;
|
||||
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
|
||||
if (!manager) return;
|
||||
SC_HANDLE service = OpenServiceW(manager, L"CrossDeskService", SERVICE_START);
|
||||
if (service) { StartServiceW(service, 0, nullptr); CloseServiceHandle(service); }
|
||||
CloseServiceHandle(manager);
|
||||
}
|
||||
|
||||
void Restart(const fs::path& application) { Launch(application, {}); }
|
||||
|
||||
std::shared_ptr<void> PinHelper(const fs::path& helper) {
|
||||
HANDLE handle = CreateFileW(helper.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE) throw std::runtime_error("Cannot lock update helper");
|
||||
return {handle, [](void* value) { CloseHandle(value); }};
|
||||
}
|
||||
|
||||
MaintenanceLock::MaintenanceLock(const fs::path& application) {
|
||||
HANDLE mutex = CreateMutexExW(nullptr, LockName(application).c_str(), 0, SYNCHRONIZE);
|
||||
const DWORD error = GetLastError();
|
||||
if (!mutex || error == ERROR_ALREADY_EXISTS) {
|
||||
if (mutex) CloseHandle(mutex);
|
||||
throw std::runtime_error("An update is already running");
|
||||
}
|
||||
handle_ = reinterpret_cast<std::intptr_t>(mutex);
|
||||
}
|
||||
MaintenanceLock::~MaintenanceLock() {
|
||||
if (handle_ != -1) CloseHandle(reinterpret_cast<HANDLE>(handle_));
|
||||
}
|
||||
bool MaintenanceActive(const fs::path& application) {
|
||||
HANDLE mutex = OpenMutexW(SYNCHRONIZE, FALSE, LockName(application).c_str());
|
||||
if (mutex) { CloseHandle(mutex); return true; }
|
||||
return GetLastError() != ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
} // namespace crossdesk::updater
|
||||
Reference in New Issue
Block a user