Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 10 additions & 23 deletions installer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,20 @@ if(NOT WIN32)
endif()

# ============================================================================
# minizip-ng library (minimal ZIP support)
# miniz library (ZIP + inflate) - vendored amalgamation
# ============================================================================

set(MINIZIP_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_inflate.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_zip.cpp
add_library(miniz STATIC
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/miniz/miniz.c
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/miniz/miniz.h
)

set(MINIZIP_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz.h
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_inflate.h
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng/mz_zip.h
target_include_directories(miniz PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/miniz
)

add_library(minizip-ng STATIC ${MINIZIP_SOURCES} ${MINIZIP_HEADERS})

target_include_directories(minizip-ng PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng
)

# Prevent Windows min/max macros from conflicting with std::min/std::max
target_compile_definitions(minizip-ng PRIVATE NOMINMAX)

set_target_properties(minizip-ng PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
# miniz uses fopen etc.; silence MSVC CRT deprecation warnings
target_compile_definitions(miniz PRIVATE _CRT_SECURE_NO_WARNINGS)

# ============================================================================
# WinDbg installer library (used by standalone installer CLI)
Expand Down Expand Up @@ -63,11 +50,11 @@ add_library(windbg-installer-lib STATIC ${INSTALLER_LIB_SOURCES} ${INSTALLER_LIB
target_include_directories(windbg-installer-lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/pugixml
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/minizip-ng
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/miniz
)

target_link_libraries(windbg-installer-lib
PRIVATE minizip-ng
PRIVATE miniz
PRIVATE winhttp.lib
PRIVATE ole32.lib
PRIVATE shell32.lib
Expand Down
51 changes: 39 additions & 12 deletions installer/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,16 @@ void PrintUsage(const char* programName) {
<< " check-update Check for updates (compares local vs latest online)\n"
<< "\n"
<< "Options:\n"
<< " --path <dir> Specify installation directory\n"
<< " (default: %APPDATA%\\Binary Ninja\\windbg)\n"
<< " --update Update mode: wait for Binary Ninja to exit first\n"
<< " (use this when WinDbg DLLs may be loaded)\n"
<< " --quiet Suppress progress output (exit code only)\n"
<< " --json Output in JSON format (for API integration)\n"
<< " --help Show this help message\n"
<< " --path <dir> Specify installation directory\n"
<< " (default: %APPDATA%\\Binary Ninja\\windbg)\n"
<< " --bundle <file> Install from an already-downloaded .msixbundle instead\n"
<< " of downloading it (for testing; the file is kept)\n"
<< " --update Update mode: wait for Binary Ninja to exit first\n"
<< " (use this when WinDbg DLLs may be loaded)\n"
<< " --quiet Suppress progress output (exit code only)\n"
<< " --json Output in JSON format (for API integration)\n"
<< " --verbose, -v Show info-level logs, including timing measurements\n"
<< " --help Show this help message\n"
<< "\n"
<< "Examples:\n"
<< " " << programName << " version\n"
Expand All @@ -274,7 +277,8 @@ void PrintBanner() {
}

/* Command: install */
int CmdInstall(const std::string& installPath, OutputMode mode, bool isUpdate) {
int CmdInstall(const std::string& installPath, OutputMode mode, bool isUpdate,
const std::string& bundlePath, bool verbose) {
/* Determine and print install path */
std::string targetPath = installPath.empty() ? GetDefaultInstallPath() : installPath;

Expand All @@ -301,6 +305,15 @@ int CmdInstall(const std::string& installPath, OutputMode mode, bool isUpdate) {
InstallConfig config;
config.installPath = targetPath;
config.updateSettings = true;
config.localBundlePath = bundlePath;

if (!bundlePath.empty()) {
if (mode == OutputMode::Human) {
std::cout << " Using local bundle (skipping download): " << bundlePath << "\n\n";
} else if (mode == OutputMode::Json) {
std::cout << "{\"type\":\"info\",\"localBundle\":\"" << bundlePath << "\"}" << std::endl;
}
}

std::string lastStep;

Expand All @@ -326,9 +339,12 @@ int CmdInstall(const std::string& installPath, OutputMode mode, bool isUpdate) {
}
};

config.onLog = [](int level, const std::string& message) {
if (level >= LOG_WARN) {
ConsoleColor color = (level == LOG_ERROR) ? COLOR_RED : COLOR_YELLOW;
int logThreshold = verbose ? LOG_INFO : LOG_WARN;
config.onLog = [logThreshold](int level, const std::string& message) {
if (level >= logThreshold) {
ConsoleColor color = COLOR_DEFAULT;
if (level == LOG_ERROR) color = COLOR_RED;
else if (level == LOG_WARN) color = COLOR_YELLOW;
SetConsoleColor(color);
std::cout << "\n " << message;
ResetConsoleColor();
Expand Down Expand Up @@ -498,8 +514,10 @@ int main(int argc, char* argv[]) {
/* Parse command line arguments */
std::string command;
std::string installPath;
std::string bundlePath;
OutputMode mode = OutputMode::Human;
bool isUpdate = false;
bool verbose = false;

for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
Expand All @@ -512,12 +530,21 @@ int main(int argc, char* argv[]) {
std::cerr << "Error: --path requires a directory argument\n";
return 1;
}
} else if (strcmp(argv[i], "--bundle") == 0) {
if (i + 1 < argc) {
bundlePath = argv[++i];
} else {
std::cerr << "Error: --bundle requires a file path argument\n";
return 1;
}
} else if (strcmp(argv[i], "--quiet") == 0 || strcmp(argv[i], "-q") == 0) {
mode = OutputMode::Quiet;
} else if (strcmp(argv[i], "--json") == 0) {
mode = OutputMode::Json;
} else if (strcmp(argv[i], "--update") == 0) {
isUpdate = true;
} else if (strcmp(argv[i], "--verbose") == 0 || strcmp(argv[i], "-v") == 0) {
verbose = true;
} else if (argv[i][0] != '-') {
if (command.empty()) {
command = argv[i];
Expand Down Expand Up @@ -545,7 +572,7 @@ int main(int argc, char* argv[]) {

/* Execute command */
if (command == "install") {
return CmdInstall(installPath, mode, isUpdate);
return CmdInstall(installPath, mode, isUpdate, bundlePath, verbose);
} else if (command == "version") {
return CmdVersion(installPath, mode);
} else if (command == "check-update") {
Expand Down
123 changes: 66 additions & 57 deletions installer/windbg_installer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,74 +185,81 @@ InstallResult Install(const InstallConfig& config) {
}
Log(logCallback, LOG_INFO, "Installation target: " + installTarget);

/* Step 1: Download appinstaller file (small, no progress needed) */
ReportProgress(progressCallback, "Downloading WinDbg package information from:", 0);
ReportProgress(progressCallback, std::string(kWinDbgDownloadUrl), 0);

std::string appInstallerPath = GetTempFilePath(".appinstaller");
tempFiles.push_back(appInstallerPath);

if (!DownloadFileWithProgress(kWinDbgDownloadUrl, appInstallerPath, nullptr, logCallback)) {
std::string error = "Failed to download appinstaller file";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}

/* Step 2: Parse XML to get MSIX bundle URL */
ReportProgress(progressCallback, "Parsing package information...", 0);
/* appInstallerPath stays empty when a local bundle is supplied (no download). */
std::string appInstallerPath;
std::string msixPath;

if (!config.localBundlePath.empty()) {
/* Testing shortcut: use an already-downloaded bundle and skip all network steps.
* The file is used in place and is NOT added to tempFiles, so cleanup never
* deletes the caller's bundle (they can reuse it across test runs). */
if (!fs::exists(config.localBundlePath)) {
std::string error = "Local bundle not found: " + config.localBundlePath;
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}
msixPath = config.localBundlePath;
ReportProgress(progressCallback, "Using local MSIX bundle (skipping download)...", 0);
Log(logCallback, LOG_INFO, "Using local MSIX bundle, skipping download: " + msixPath);
} else {
/* Step 1: Download appinstaller file (small, no progress needed) */
ReportProgress(progressCallback, "Downloading WinDbg package information from:", 0);
ReportProgress(progressCallback, std::string(kWinDbgDownloadUrl), 0);

std::string msixUrl = ParseAppInstallerXml(appInstallerPath, logCallback);
if (msixUrl.empty()) {
std::string error = "Failed to parse appinstaller XML";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}
appInstallerPath = GetTempFilePath(".appinstaller");
tempFiles.push_back(appInstallerPath);

/* Step 3: Download MSIX bundle (this is the main download that shows progress) */
ReportProgress(progressCallback, "Downloading WinDbg/TTD package from:", 0);
ReportProgress(progressCallback, msixUrl, 0);
if (!DownloadFileWithProgress(kWinDbgDownloadUrl, appInstallerPath, nullptr, logCallback)) {
std::string error = "Failed to download appinstaller file";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}

std::string msixPath = GetTempFilePath(".msixbundle.zip");
tempFiles.push_back(msixPath);
/* Step 2: Parse XML to get MSIX bundle URL */
ReportProgress(progressCallback, "Parsing package information...", 0);

auto msixDownloadProgressCb = [&](const DownloadProgress& dp) {
/* Report download percentage (0-100%) directly - this is the only step that needs progress display */
int percent = 0;
if (dp.totalBytes > 0) {
percent = (int)(100 * dp.bytesDownloaded / dp.totalBytes);
std::string msixUrl = ParseAppInstallerXml(appInstallerPath, logCallback);
if (msixUrl.empty()) {
std::string error = "Failed to parse appinstaller XML";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}
ReportProgress(progressCallback, "Downloading...", percent,
dp.bytesDownloaded, dp.totalBytes, dp.bytesPerSecond);
};

if (!DownloadFileWithProgress(msixUrl, msixPath, msixDownloadProgressCb, logCallback)) {
std::string error = "Failed to download MSIX bundle";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}
/* Step 3: Download MSIX bundle (this is the main download that shows progress) */
ReportProgress(progressCallback, "Downloading WinDbg/TTD package from:", 0);
ReportProgress(progressCallback, msixUrl, 0);

/* Step 4: Extract inner MSIX file from bundle */
ReportProgress(progressCallback, "Extracting package contents...", 0);
msixPath = GetTempFilePath(".msixbundle.zip");
tempFiles.push_back(msixPath);

std::string tempExtractDir = GetTempFilePath("_extract");
tempFiles.push_back(tempExtractDir);
auto msixDownloadProgressCb = [&](const DownloadProgress& dp) {
/* Report download percentage (0-100%) directly - this is the only step that needs progress display */
int percent = 0;
if (dp.totalBytes > 0) {
percent = (int)(100 * dp.bytesDownloaded / dp.totalBytes);
}
ReportProgress(progressCallback, "Downloading...", percent,
dp.bytesDownloaded, dp.totalBytes, dp.bytesPerSecond);
};

std::string innerMsixPath = ExtractFileFromZipArchive(msixPath, kInnerMsixName, tempExtractDir, logCallback);
if (innerMsixPath.empty()) {
std::string error = "Failed to extract inner MSIX file";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
if (!DownloadFileWithProgress(msixUrl, msixPath, msixDownloadProgressCb, logCallback)) {
std::string error = "Failed to download MSIX bundle";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
}
}

/* Step 5: Extract WinDbg contents to installation directory */
/* Step 4: Extract the inner package's contents straight to the install dir.
* The inner MSIX is read from memory (miniz), so no multi-hundred-MB temp file
* is written or later deleted - that delete was the slow, antivirus-scanned step. */
ReportProgress(progressCallback, "Installing WinDbg/TTD files...", 0);

if (!ExtractZipArchive(innerMsixPath, installTarget, nullptr, logCallback)) {
std::string error = "Failed to extract WinDbg contents";
if (!ExtractInnerPackageToDir(msixPath, kInnerMsixName, installTarget, nullptr, logCallback)) {
std::string error = "Failed to extract WinDbg contents from package";
Log(logCallback, LOG_ERROR, error);
CleanupTempFiles(tempFiles, logCallback);
return InstallResult(false, error);
Expand All @@ -273,7 +280,7 @@ InstallResult Install(const InstallConfig& config) {
/* Step 6b: Write version marker file */
/* Re-parse appinstaller to get version (file is still on disk) */
std::string installedVersion;
{
if (!appInstallerPath.empty()) {
pugi::xml_document doc;
if (doc.load_file(appInstallerPath.c_str())) {
pugi::xml_node appInstaller = doc.child("AppInstaller");
Expand Down Expand Up @@ -304,7 +311,9 @@ InstallResult Install(const InstallConfig& config) {
PrintSettingsInfo(x64dbgEngPath, logCallback);
}

/* Cleanup */
/* Cleanup. Give it its own progress message instead of leaving
* "Verifying installation..." on screen while the temp files are removed. */
ReportProgress(progressCallback, "Cleaning up temporary files...", 0);
CleanupTempFiles(tempFiles, logCallback);

ReportProgress(progressCallback, "Installation completed successfully!", 0);
Expand Down
2 changes: 2 additions & 0 deletions installer/windbg_installer.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ enum LogLevel {
struct InstallConfig {
std::string installPath; /* Override default install path (empty = use default) */
bool updateSettings; /* Whether to update Binary Ninja settings (default: true) */
std::string localBundlePath; /* Use this already-downloaded .msixbundle instead of downloading
(for testing). The file is used in place and never deleted. */
ProgressCallback onProgress; /* Progress callback */
LogCallback onLog; /* Logging callback */

Expand Down
Loading