PRA-only-demo/scripts/installer/Program.cpp

379 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 缺失或不完整。");
}
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) {
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;
}
}