446 lines
18 KiB
C++
446 lines
18 KiB
C++
#include <windows.h>
|
||
#include <shellapi.h>
|
||
#include <shlobj.h>
|
||
#include <shobjidl.h>
|
||
#include <tlhelp32.h>
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <cwctype>
|
||
#include <cstdint>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <string>
|
||
#include <thread>
|
||
#include <vector>
|
||
|
||
namespace fs = std::filesystem;
|
||
|
||
namespace {
|
||
constexpr char kMagic[] = "RPASTUDIOPAYLOAD";
|
||
constexpr std::size_t kMagicLength = 16;
|
||
constexpr std::size_t kFooterLength = sizeof(std::uint64_t) * 4 + kMagicLength;
|
||
|
||
struct Options {
|
||
bool silent = false;
|
||
bool noLaunch = false;
|
||
bool noShortcut = false;
|
||
fs::path target;
|
||
};
|
||
|
||
struct Footer {
|
||
std::uint64_t cliLength = 0;
|
||
std::uint64_t guiLength = 0;
|
||
std::uint64_t dllLength = 0;
|
||
std::uint64_t payloadLength = 0;
|
||
};
|
||
|
||
fs::path g_logPath;
|
||
|
||
std::wstring lower(std::wstring value) {
|
||
std::transform(value.begin(), value.end(), value.begin(), towlower);
|
||
return value;
|
||
}
|
||
|
||
std::string utf8(const std::wstring& value) {
|
||
if (value.empty()) return {};
|
||
int size = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||
std::string output(size, '\0');
|
||
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), output.data(), size, nullptr, nullptr);
|
||
return output;
|
||
}
|
||
|
||
void logLine(const std::wstring& message) {
|
||
try {
|
||
SYSTEMTIME now{};
|
||
GetLocalTime(&now);
|
||
char timestamp[32]{};
|
||
std::snprintf(timestamp, sizeof(timestamp), "%04u-%02u-%02uT%02u:%02u:%02u ", now.wYear, now.wMonth,
|
||
now.wDay, now.wHour, now.wMinute, now.wSecond);
|
||
std::ofstream output(g_logPath, std::ios::binary | std::ios::app);
|
||
output << timestamp << utf8(message) << "\r\n";
|
||
} catch (...) {
|
||
}
|
||
}
|
||
|
||
[[noreturn]] void fail(const std::wstring& message) {
|
||
logLine(L"ERROR: " + message);
|
||
throw message;
|
||
}
|
||
|
||
fs::path modulePath() {
|
||
std::vector<wchar_t> buffer(32768);
|
||
DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||
if (length == 0 || length >= buffer.size()) fail(L"无法读取安装包路径。");
|
||
return fs::path(std::wstring(buffer.data(), length));
|
||
}
|
||
|
||
fs::path environmentPath(const wchar_t* name) {
|
||
DWORD required = GetEnvironmentVariableW(name, nullptr, 0);
|
||
if (required == 0) return {};
|
||
std::vector<wchar_t> buffer(required);
|
||
GetEnvironmentVariableW(name, buffer.data(), required);
|
||
return fs::path(buffer.data());
|
||
}
|
||
|
||
Options parseOptions() {
|
||
Options options;
|
||
int count = 0;
|
||
LPWSTR* arguments = CommandLineToArgvW(GetCommandLineW(), &count);
|
||
if (!arguments) fail(L"无法解析安装参数。");
|
||
|
||
for (int index = 1; index < count; ++index) {
|
||
std::wstring argument = arguments[index];
|
||
std::wstring normalized = lower(argument);
|
||
if (normalized == L"/s" || normalized == L"/q" || normalized == L"--silent" || normalized == L"--quiet") {
|
||
options.silent = true;
|
||
} else if (normalized == L"/nolaunch" || normalized == L"--no-launch") {
|
||
options.noLaunch = true;
|
||
} else if (normalized == L"/noshortcut" || normalized == L"--no-shortcut") {
|
||
options.noShortcut = true;
|
||
} else if (normalized.rfind(L"/target=", 0) == 0 || normalized.rfind(L"--target=", 0) == 0) {
|
||
std::size_t equals = argument.find(L'=');
|
||
options.target = fs::path(argument.substr(equals + 1));
|
||
}
|
||
}
|
||
LocalFree(arguments);
|
||
|
||
if (options.target.empty()) {
|
||
fs::path localAppData = environmentPath(L"LOCALAPPDATA");
|
||
if (localAppData.empty()) fail(L"无法读取 LOCALAPPDATA。");
|
||
options.target = localAppData / L"Programs" / L"RPA Studio";
|
||
}
|
||
options.target = fs::absolute(options.target).lexically_normal();
|
||
return options;
|
||
}
|
||
|
||
void readExact(std::ifstream& input, char* destination, std::uint64_t length) {
|
||
constexpr std::size_t bufferLimit = 1024 * 1024;
|
||
std::uint64_t remaining = length;
|
||
while (remaining > 0) {
|
||
std::streamsize chunk = static_cast<std::streamsize>(std::min<std::uint64_t>(remaining, bufferLimit));
|
||
input.read(destination, chunk);
|
||
if (input.gcount() != chunk) fail(L"安装包数据提前结束。");
|
||
destination += chunk;
|
||
remaining -= static_cast<std::uint64_t>(chunk);
|
||
}
|
||
}
|
||
|
||
Footer readFooter(std::ifstream& input, std::uint64_t fileLength) {
|
||
if (fileLength <= kFooterLength) fail(L"安装包缺少应用数据,请重新下载。");
|
||
input.seekg(static_cast<std::streamoff>(fileLength - kFooterLength), std::ios::beg);
|
||
Footer footer;
|
||
input.read(reinterpret_cast<char*>(&footer.cliLength), sizeof(footer.cliLength));
|
||
input.read(reinterpret_cast<char*>(&footer.guiLength), sizeof(footer.guiLength));
|
||
input.read(reinterpret_cast<char*>(&footer.dllLength), sizeof(footer.dllLength));
|
||
input.read(reinterpret_cast<char*>(&footer.payloadLength), sizeof(footer.payloadLength));
|
||
char magic[kMagicLength]{};
|
||
input.read(magic, kMagicLength);
|
||
if (!input || std::memcmp(magic, kMagic, kMagicLength) != 0) fail(L"安装包数据校验失败,请重新下载。");
|
||
if (footer.cliLength == 0 || footer.guiLength == 0 || footer.dllLength == 0 || footer.payloadLength == 0)
|
||
fail(L"安装包数据长度无效。");
|
||
std::uint64_t total = footer.cliLength + footer.guiLength + footer.dllLength + footer.payloadLength;
|
||
if (total + kFooterLength >= fileLength) fail(L"安装包载荷无效。");
|
||
return footer;
|
||
}
|
||
|
||
void writeSegment(std::ifstream& input, const fs::path& outputPath, std::uint64_t length) {
|
||
std::ofstream output(outputPath, std::ios::binary | std::ios::trunc);
|
||
if (!output) fail(L"无法创建临时文件:" + outputPath.wstring());
|
||
std::vector<char> buffer(1024 * 1024);
|
||
std::uint64_t remaining = length;
|
||
while (remaining > 0) {
|
||
std::streamsize chunk = static_cast<std::streamsize>(std::min<std::uint64_t>(remaining, buffer.size()));
|
||
input.read(buffer.data(), chunk);
|
||
if (input.gcount() != chunk) fail(L"安装包数据提前结束。");
|
||
output.write(buffer.data(), chunk);
|
||
if (!output) fail(L"无法写入临时文件:" + outputPath.wstring());
|
||
remaining -= static_cast<std::uint64_t>(chunk);
|
||
}
|
||
}
|
||
|
||
void extractPayload(const fs::path& temporaryRoot, fs::path& cli, fs::path& gui, fs::path& dll, fs::path& payload) {
|
||
fs::path self = modulePath();
|
||
std::ifstream input(self, std::ios::binary | std::ios::ate);
|
||
if (!input) fail(L"无法读取安装包。");
|
||
std::uint64_t fileLength = static_cast<std::uint64_t>(input.tellg());
|
||
Footer footer = readFooter(input, fileLength);
|
||
std::uint64_t payloadStart = fileLength - kFooterLength - footer.cliLength - footer.guiLength - footer.dllLength - footer.payloadLength;
|
||
input.seekg(static_cast<std::streamoff>(payloadStart), std::ios::beg);
|
||
|
||
cli = temporaryRoot / L"7z.exe";
|
||
gui = temporaryRoot / L"7zG.exe";
|
||
dll = temporaryRoot / L"7z.dll";
|
||
payload = temporaryRoot / L"payload.7z";
|
||
writeSegment(input, cli, footer.cliLength);
|
||
writeSegment(input, gui, footer.guiLength);
|
||
writeSegment(input, dll, footer.dllLength);
|
||
writeSegment(input, payload, footer.payloadLength);
|
||
}
|
||
|
||
DWORD runProcess(const fs::path& executable, const std::wstring& arguments, bool hidden) {
|
||
std::wstring command = L"\"" + executable.wstring() + L"\" " + arguments;
|
||
std::vector<wchar_t> mutableCommand(command.begin(), command.end());
|
||
mutableCommand.push_back(L'\0');
|
||
STARTUPINFOW startup{};
|
||
startup.cb = sizeof(startup);
|
||
if (hidden) {
|
||
startup.dwFlags = STARTF_USESHOWWINDOW;
|
||
startup.wShowWindow = SW_HIDE;
|
||
}
|
||
PROCESS_INFORMATION process{};
|
||
DWORD flags = hidden ? CREATE_NO_WINDOW : 0;
|
||
if (!CreateProcessW(executable.c_str(), mutableCommand.data(), nullptr, nullptr, FALSE, flags, nullptr,
|
||
executable.parent_path().c_str(), &startup, &process)) {
|
||
fail(L"无法启动解压程序,Windows 错误码:" + std::to_wstring(GetLastError()));
|
||
}
|
||
WaitForSingleObject(process.hProcess, INFINITE);
|
||
DWORD exitCode = 1;
|
||
GetExitCodeProcess(process.hProcess, &exitCode);
|
||
CloseHandle(process.hThread);
|
||
CloseHandle(process.hProcess);
|
||
return exitCode;
|
||
}
|
||
|
||
bool samePath(const fs::path& left, const fs::path& right) {
|
||
return lower(fs::absolute(left).lexically_normal().wstring()) == lower(fs::absolute(right).lexically_normal().wstring());
|
||
}
|
||
|
||
void stopInstalledApplication(const fs::path& targetExecutable) {
|
||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||
if (snapshot == INVALID_HANDLE_VALUE) return;
|
||
PROCESSENTRY32W entry{};
|
||
entry.dwSize = sizeof(entry);
|
||
if (Process32FirstW(snapshot, &entry)) {
|
||
do {
|
||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE, FALSE, entry.th32ProcessID);
|
||
if (!process) continue;
|
||
std::vector<wchar_t> buffer(32768);
|
||
DWORD length = static_cast<DWORD>(buffer.size());
|
||
if (QueryFullProcessImageNameW(process, 0, buffer.data(), &length) &&
|
||
samePath(fs::path(std::wstring(buffer.data(), length)), targetExecutable)) {
|
||
TerminateProcess(process, 0);
|
||
WaitForSingleObject(process, 5000);
|
||
}
|
||
CloseHandle(process);
|
||
} while (Process32NextW(snapshot, &entry));
|
||
}
|
||
CloseHandle(snapshot);
|
||
}
|
||
|
||
void removeDirectoryWithRetry(const fs::path& directory) {
|
||
if (!fs::exists(directory)) return;
|
||
std::error_code error;
|
||
for (int attempt = 0; attempt < 6; ++attempt) {
|
||
fs::remove_all(directory, error);
|
||
if (!error && !fs::exists(directory)) return;
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(500 * (attempt + 1)));
|
||
}
|
||
fail(L"无法更新安装目录,请关闭正在运行的 RPA Studio 后重试。\r\n" + directory.wstring());
|
||
}
|
||
|
||
void validateApplication(const fs::path& directory) {
|
||
fs::path executable = directory / L"RPA Studio.exe";
|
||
fs::path asar = directory / L"resources" / L"app.asar";
|
||
if (!fs::exists(executable) || fs::file_size(executable) < 1024 * 1024) fail(L"RPA Studio.exe 缺失或不完整。");
|
||
if (!fs::exists(asar) || fs::file_size(asar) < 1024 * 1024) fail(L"resources\\app.asar 缺失或不完整。");
|
||
}
|
||
|
||
bool chooseInstallDirectory(const fs::path& currentTarget, fs::path& selectedTarget) {
|
||
HRESULT initialized = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||
IFileOpenDialog* dialog = nullptr;
|
||
HRESULT result = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
|
||
reinterpret_cast<void**>(&dialog));
|
||
if (FAILED(result) || !dialog) {
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"无法打开安装目录选择窗口。");
|
||
}
|
||
|
||
FILEOPENDIALOGOPTIONS options = 0;
|
||
dialog->GetOptions(&options);
|
||
dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_DONTADDTORECENT);
|
||
dialog->SetTitle(L"选择 RPA Studio 的安装位置");
|
||
dialog->SetOkButtonLabel(L"选择此文件夹");
|
||
dialog->SetFileName(L"RPA Studio");
|
||
|
||
fs::path initial = currentTarget.parent_path();
|
||
while (!initial.empty() && !fs::exists(initial)) initial = initial.parent_path();
|
||
if (!initial.empty()) {
|
||
IShellItem* initialItem = nullptr;
|
||
if (SUCCEEDED(SHCreateItemFromParsingName(initial.c_str(), nullptr, IID_IShellItem,
|
||
reinterpret_cast<void**>(&initialItem))) && initialItem) {
|
||
dialog->SetFolder(initialItem);
|
||
initialItem->Release();
|
||
}
|
||
}
|
||
|
||
result = dialog->Show(nullptr);
|
||
if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
|
||
dialog->Release();
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
return false;
|
||
}
|
||
if (FAILED(result)) {
|
||
dialog->Release();
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"安装目录选择失败。");
|
||
}
|
||
|
||
IShellItem* selectedItem = nullptr;
|
||
result = dialog->GetResult(&selectedItem);
|
||
dialog->Release();
|
||
if (FAILED(result) || !selectedItem) {
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"无法读取所选安装目录。");
|
||
}
|
||
|
||
PWSTR selectedPath = nullptr;
|
||
result = selectedItem->GetDisplayName(SIGDN_FILESYSPATH, &selectedPath);
|
||
selectedItem->Release();
|
||
if (FAILED(result) || !selectedPath) {
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"所选位置不是有效的本地文件夹。");
|
||
}
|
||
|
||
fs::path parent(selectedPath);
|
||
CoTaskMemFree(selectedPath);
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
selectedTarget = lower(parent.filename().wstring()) == L"rpa studio" ? parent : parent / L"RPA Studio";
|
||
selectedTarget = fs::absolute(selectedTarget).lexically_normal();
|
||
return true;
|
||
}
|
||
|
||
void createShortcut(const fs::path& targetExecutable, const fs::path& workingDirectory) {
|
||
HRESULT initialized = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||
IShellLinkW* shellLink = nullptr;
|
||
HRESULT result = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
|
||
reinterpret_cast<void**>(&shellLink));
|
||
if (FAILED(result) || !shellLink) {
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"无法创建桌面快捷方式。");
|
||
}
|
||
shellLink->SetPath(targetExecutable.c_str());
|
||
shellLink->SetWorkingDirectory(workingDirectory.c_str());
|
||
shellLink->SetDescription(L"RPA Studio");
|
||
|
||
wchar_t desktop[MAX_PATH]{};
|
||
if (FAILED(SHGetFolderPathW(nullptr, CSIDL_DESKTOPDIRECTORY, nullptr, SHGFP_TYPE_CURRENT, desktop))) {
|
||
shellLink->Release();
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
fail(L"无法定位桌面目录。");
|
||
}
|
||
IPersistFile* persist = nullptr;
|
||
result = shellLink->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&persist));
|
||
if (SUCCEEDED(result) && persist) {
|
||
result = persist->Save((fs::path(desktop) / L"RPA Studio.lnk").c_str(), TRUE);
|
||
persist->Release();
|
||
}
|
||
shellLink->Release();
|
||
if (SUCCEEDED(initialized)) CoUninitialize();
|
||
if (FAILED(result)) fail(L"桌面快捷方式保存失败。");
|
||
}
|
||
|
||
void copyDirectory(const fs::path& source, const fs::path& destination) {
|
||
std::error_code error;
|
||
fs::rename(source, destination, error);
|
||
if (!error) return;
|
||
error.clear();
|
||
fs::copy(source, destination, fs::copy_options::recursive | fs::copy_options::overwrite_existing, error);
|
||
if (error) fail(L"无法写入安装目录:" + destination.wstring());
|
||
fs::remove_all(source, error);
|
||
}
|
||
|
||
void install(const Options& options) {
|
||
fs::path temp = environmentPath(L"TEMP");
|
||
if (temp.empty()) temp = environmentPath(L"TMP");
|
||
if (temp.empty()) fail(L"无法定位临时目录。");
|
||
fs::path temporaryRoot = temp / (L"RPA-Studio-Install-" + std::to_wstring(GetCurrentProcessId()));
|
||
fs::path stage = temporaryRoot / L"app";
|
||
fs::path targetExecutable = options.target / L"RPA Studio.exe";
|
||
removeDirectoryWithRetry(temporaryRoot);
|
||
fs::create_directories(temporaryRoot);
|
||
|
||
try {
|
||
logLine(L"正在准备安装文件...");
|
||
fs::path cli, gui, dll, payload;
|
||
extractPayload(temporaryRoot, cli, gui, dll, payload);
|
||
fs::create_directories(stage);
|
||
|
||
logLine(L"正在解压应用...");
|
||
fs::path extractor = options.silent ? cli : gui;
|
||
std::wstring arguments = L"x \"" + payload.wstring() + L"\" -o\"" + stage.wstring() + L"\" -y";
|
||
if (runProcess(extractor, arguments, options.silent) != 0) fail(L"应用文件解压失败。");
|
||
validateApplication(stage);
|
||
|
||
logLine(L"正在写入安装目录...");
|
||
stopInstalledApplication(targetExecutable);
|
||
removeDirectoryWithRetry(options.target);
|
||
fs::create_directories(options.target.parent_path());
|
||
copyDirectory(stage, options.target);
|
||
validateApplication(options.target);
|
||
|
||
if (!options.noShortcut) {
|
||
logLine(L"正在创建桌面快捷方式...");
|
||
createShortcut(targetExecutable, options.target);
|
||
}
|
||
try {
|
||
fs::copy_file(g_logPath, options.target / L"install.log", fs::copy_options::overwrite_existing);
|
||
} catch (...) {
|
||
}
|
||
removeDirectoryWithRetry(temporaryRoot);
|
||
|
||
if (!options.noLaunch) {
|
||
logLine(L"正在启动 RPA Studio...");
|
||
ShellExecuteW(nullptr, L"open", targetExecutable.c_str(), nullptr, options.target.c_str(), SW_SHOWNORMAL);
|
||
}
|
||
} catch (...) {
|
||
std::error_code ignored;
|
||
fs::remove_all(temporaryRoot, ignored);
|
||
throw;
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||
fs::path temp = environmentPath(L"TEMP");
|
||
if (temp.empty()) temp = fs::temp_directory_path();
|
||
g_logPath = temp / (L"RPA-Studio-Install-" + std::to_wstring(GetCurrentProcessId()) + L".log");
|
||
|
||
bool silent = false;
|
||
try {
|
||
Options options = parseOptions();
|
||
silent = options.silent;
|
||
logLine(L"installer started");
|
||
if (!options.silent) {
|
||
fs::path selectedTarget;
|
||
if (!chooseInstallDirectory(options.target, selectedTarget)) return 0;
|
||
options.target = selectedTarget;
|
||
std::wstring prompt = L"将 RPA Studio __PRODUCT_VERSION__ 安装到当前 Windows 用户?\r\n\r\n安装位置:" +
|
||
options.target.wstring();
|
||
if (MessageBoxW(nullptr, prompt.c_str(), L"RPA Studio 安装", MB_YESNO | MB_ICONQUESTION | MB_SETFOREGROUND) != IDYES)
|
||
return 0;
|
||
}
|
||
|
||
install(options);
|
||
logLine(L"installation completed");
|
||
if (!options.silent)
|
||
MessageBoxW(nullptr, L"RPA Studio 已安装完成。", L"RPA Studio 安装", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
|
||
return 0;
|
||
} catch (const std::wstring& error) {
|
||
if (g_logPath.empty()) return 1;
|
||
if (GetFileAttributesW(g_logPath.c_str()) == INVALID_FILE_ATTRIBUTES) logLine(L"ERROR: " + error);
|
||
if (!silent)
|
||
MessageBoxW(nullptr,
|
||
(L"安装失败。\r\n\r\n" + error + L"\r\n\r\n诊断日志:" + g_logPath.wstring()).c_str(),
|
||
L"RPA Studio 安装失败", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
|
||
return 1;
|
||
} catch (...) {
|
||
logLine(L"ERROR: unknown installer failure");
|
||
if (!silent)
|
||
MessageBoxW(nullptr, L"安装失败,发生未知错误。", L"RPA Studio 安装失败", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
|
||
return 1;
|
||
}
|
||
}
|