#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(value.size()), nullptr, 0, nullptr, nullptr); std::string output(size, '\0'); WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(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 buffer(32768); DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(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 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::min(remaining, bufferLimit)); input.read(destination, chunk); if (input.gcount() != chunk) fail(L"安装包数据提前结束。"); destination += chunk; remaining -= static_cast(chunk); } } Footer readFooter(std::ifstream& input, std::uint64_t fileLength) { if (fileLength <= kFooterLength) fail(L"安装包缺少应用数据,请重新下载。"); input.seekg(static_cast(fileLength - kFooterLength), std::ios::beg); Footer footer; input.read(reinterpret_cast(&footer.cliLength), sizeof(footer.cliLength)); input.read(reinterpret_cast(&footer.guiLength), sizeof(footer.guiLength)); input.read(reinterpret_cast(&footer.dllLength), sizeof(footer.dllLength)); input.read(reinterpret_cast(&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 buffer(1024 * 1024); std::uint64_t remaining = length; while (remaining > 0) { std::streamsize chunk = static_cast(std::min(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(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(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(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 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 buffer(32768); DWORD length = static_cast(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(&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(&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; } }