From 8c020a87a02bdd9932d19b13c85b8a4c96cdbc35 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Wed, 3 Apr 2019 18:06:20 -0500 Subject: [PATCH 01/44] Added more elaborate installer using Windows GDI Making an installer using the windows GDI some changes have been made to the rest of the api to facilitate logging to an intermediate location (tasklogger), which other classes can register callbacks to to read information (such as nsuite adding a callback to funnel log data to std::cout, while the installer funnels it to the GUI). At this point the installer works, but will be updated in the future --- CMakeLists.txt | 24 +- src/DirectoryTools.cpp | 56 +++-- src/TaskLogger.h | 78 +++++++ src/Threader.h | 8 +- src/nStaller/CMakeLists.txt | 63 +++-- src/nStaller/Frames/DirectoryFrame.cpp | 210 +++++++++++++++++ src/nStaller/Frames/DirectoryFrame.h | 28 +++ src/nStaller/Frames/FinishFrame.cpp | 66 ++++++ src/nStaller/Frames/FinishFrame.h | 16 ++ src/nStaller/Frames/Frame.h | 27 +++ src/nStaller/Frames/InstallFrame.cpp | 82 +++++++ src/nStaller/Frames/InstallFrame.h | 23 ++ src/nStaller/Frames/WelcomeFrame.cpp | 79 +++++++ src/nStaller/Frames/WelcomeFrame.h | 16 ++ src/nStaller/nStaller.cpp | 285 ++++++++++++++++++++--- src/nStaller/nStaller.rc | Bin 1314 -> 1322 bytes src/nSuite/CMakeLists.txt | 60 ++--- src/nSuite/Commands/DiffCommand.cpp | 36 +-- src/nSuite/Commands/InstallerCommand.cpp | 38 +-- src/nSuite/Commands/PackCommand.cpp | 34 +-- src/nSuite/Commands/PatchCommand.cpp | 34 +-- src/nSuite/Commands/UnpackCommand.cpp | 34 +-- src/nSuite/nSuite.cpp | 31 +-- src/nUpdater/CMakeLists.txt | 55 +++-- src/nUpdater/nUpdater.cpp | 36 +-- 25 files changed, 1165 insertions(+), 254 deletions(-) create mode 100644 src/TaskLogger.h create mode 100644 src/nStaller/Frames/DirectoryFrame.cpp create mode 100644 src/nStaller/Frames/DirectoryFrame.h create mode 100644 src/nStaller/Frames/FinishFrame.cpp create mode 100644 src/nStaller/Frames/FinishFrame.h create mode 100644 src/nStaller/Frames/Frame.h create mode 100644 src/nStaller/Frames/InstallFrame.cpp create mode 100644 src/nStaller/Frames/InstallFrame.h create mode 100644 src/nStaller/Frames/WelcomeFrame.cpp create mode 100644 src/nStaller/Frames/WelcomeFrame.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 50e38fb..8254cb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,6 @@ +#################### +### nSuite Tools ### +#################### cmake_minimum_required(VERSION 3.0) project(nStallerTools) @@ -8,19 +11,26 @@ set (PROJECT_BIN ${CMAKE_SOURCE_DIR}) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") # Include directories of dependencies for entire project -include_directories ( ${LZ4_DIR}/lib/ - ${CORE_DIR} ) +include_directories( + ${LZ4_DIR}/lib/ + ${CORE_DIR} +) + +# Add libraries common throughout entire project +link_libraries( + debug ${LZ4_DIR}/x64_Debug/liblz4_static.lib + optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib +) -link_libraries ( debug ${LZ4_DIR}/x64_Debug/liblz4_static.lib - optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib ) - # add all sub-projects and plugins here add_subdirectory( "src/nStaller" ) add_subdirectory( "src/nUpdater" ) add_subdirectory( "src/nSuite" ) + +# Visual studio specific setting: make nSuite the startup project set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT nSuite) # Enable folder structure -set_property (GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) # ALL_BUILD, ZERO_CHECK, and other build functions' folder -set_property (GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER Build_Functions) \ No newline at end of file +set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER Build_Functions) \ No newline at end of file diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 373e5ea..5fe9da1 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -3,6 +3,7 @@ #include "Common.h" #include "Instructions.h" #include "Threader.h" +#include "TaskLogger.h" #include #include #include @@ -13,6 +14,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer { // Variables Threader threader; + auto & logger = TaskLogger::GetInstance(); const auto absolute_path_length = srcDirectory.size(); const auto directoryArray = get_file_paths(srcDirectory); struct FileData { @@ -25,7 +27,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Get path name const auto srcPath = std::filesystem::path(srcDirectory); if (!std::filesystem::is_directory(srcPath) || !srcPath.has_stem()) { - std::cout << "Critical failure: the source path specified \"" << srcDirectory << "\" is not a (useable) directory.\n"; + logger << "Critical failure: the source path specified \"" << srcDirectory << "\" is not a (useable) directory.\r\n"; return false; } const auto folderName = srcPath.stem().string(); @@ -96,7 +98,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Compress the archive if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize)) { - std::cout << "Critical failure: cannot perform compression operation on the set of joined files.\n"; + logger << "Critical failure: cannot perform compression operation on the set of joined files.\r\n"; return false; } @@ -108,10 +110,11 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount) { Threader threader; + auto & log = TaskLogger::GetInstance(); char * decompressedBuffer(nullptr); size_t decompressedSize(0ull); if (!BFT::DecompressBuffer(packBuffer, packSize, &decompressedBuffer, decompressedSize)) { - std::cout << "Critical failure: cannot decompress package file.\n"; + log << "Critical failure: cannot decompress package file.\r\n"; return false; } @@ -128,6 +131,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Read the archive size_t bytesRead = sizeof(size_t) + folderSize; std::atomic_size_t filesWritten(0ull), bytesWritten(0ull); + log.setRange(decompressedSize + 1); while (bytesRead < decompressedSize) { // Read the total number of characters from the path string, from the archive const auto pathSize = *reinterpret_cast(readingPtr); @@ -157,13 +161,16 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe readingPtr = PTR_ADD(readingPtr, fileSize); bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; + log << "Writing file: " << std::string(path_array, pathSize) << "\r\n"; + log.setProgress(bytesRead); } // Wait for threaded operations to complete threader.prepareForShutdown(); while (!threader.isFinished()) - continue; + continue; threader.shutdown(); + log.setProgress(bytesRead + 1); // Success fileCount = filesWritten; @@ -217,10 +224,11 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne else { // Treat as a snapshot file if it isn't a directory // Open diff file + auto & logger = TaskLogger::GetInstance(); std::ifstream packFile(directory, std::ios::binary | std::ios::beg); const size_t packSize = std::filesystem::file_size(directory); if (!packFile.is_open()) { - std::cout << "Critical failure: cannot read package file.\n"; + logger << "Critical failure: cannot read package file.\r\n"; return false; } char * compBuffer = new char[packSize]; @@ -230,7 +238,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Decompress size_t snapSize(0ull); if (!BFT::DecompressBuffer(compBuffer, packSize, snapshot, snapSize)) { - std::cout << "Critical failure: cannot decompress package file.\n"; + logger << "Critical failure: cannot decompress package file.\r\n"; return false; } delete[] compBuffer; @@ -330,6 +338,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne }; // Retrieve all common, added, and removed files + auto & logger = TaskLogger::GetInstance(); PathPairList commonFiles; PathList addedFiles, removedFiles; char * oldSnap(nullptr), * newSnap(nullptr); @@ -351,7 +360,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * buffer(nullptr); size_t size(0ull); if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionCount)) { - std::cout << "diffing file \"" << cFiles.first->relative << "\"\n"; + logger << "diffing file \"" << cFiles.first->relative << "\"\r\n"; writeInstructions(cFiles.first->relative, oldHash, newHash, buffer, size, 'U', vecBuffer); } delete[] buffer; @@ -372,7 +381,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * buffer(nullptr); size_t size(0ull); if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionCount)) { - std::cout << "adding file \"" << nFile->relative << "\"\n"; + logger << "adding file \"" << nFile->relative << "\"\r\n"; writeInstructions(nFile->relative, 0ull, newHash, buffer, size, 'N', vecBuffer); } delete[] buffer; @@ -389,7 +398,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne size_t oldHash(0ull); if (oFile->open(&oldBuffer, oldHash)) { instructionCount++; - std::cout << "removing file \"" << oFile->relative << "\"\n"; + logger << "removing file \"" << oFile->relative << "\"\r\n"; writeInstructions(oFile->relative, oldHash, 0ull, nullptr, 0ull, 'D', vecBuffer); } delete[] oldBuffer; @@ -400,7 +409,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Compress final buffer if (!BFT::CompressBuffer(vecBuffer.data(), vecBuffer.size(), diffBuffer, diffSize)) { - std::cout << "Critical failure: cannot compress diff file.\n"; + logger << "Critical failure: cannot compress diff file.\r\n"; return false; } return true; @@ -408,10 +417,11 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t & bytesWritten, size_t & instructionsUsed) { + auto & logger = TaskLogger::GetInstance(); char * diffBuffer(nullptr); size_t diffSize(0ull); if (!BFT::DecompressBuffer(diffBufferCompressed, diffSizeCompressed, &diffBuffer, diffSize)) { - std::cout << "Critical failure: cannot decompress diff file.\n"; + logger << "Critical failure: cannot decompress diff file.\r\n"; return false; } @@ -493,34 +503,34 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) { - std::cout << "Critical failure: Cannot read source file from disk.\n"; + logger << "Critical failure: Cannot read source file from disk.\r\n"; return false; } // Patch if this source file hasn't been patched yet if (oldHash == file.diff_newHash) - std::cout << "The file \"" << file.path << "\" is already up to date, skipping...\n"; + logger << "The file \"" << file.path << "\" is already up to date, skipping...\r\n"; else if (oldHash != file.diff_oldHash) { - std::cout << "Critical failure: the file \"" << file.path << "\" is of an unexpected version. \n"; + logger << "Critical failure: the file \"" << file.path << "\" is of an unexpected version. \r\n"; return false; } else { // Patch buffer - std::cout << "patching file \"" << file.path << "\"\n"; + logger << "patching file \"" << file.path << "\"\r\n"; size_t newSize(0ull); BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match if (newHash != file.diff_newHash) { - std::cout << "Critical failure: patched file is corrupted (hash mismatch).\n"; + logger << "Critical failure: patched file is corrupted (hash mismatch).\r\n"; return false; } // Write patched buffer to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - std::cout << "Critical failure: cannot write patched file to disk.\n"; + logger << "Critical failure: cannot write patched file to disk.\r\n"; return false; } newFile.write(newBuffer, std::streamsize(newSize)); @@ -537,7 +547,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // By this point all files matched, safe to add new ones for each (const auto & file in addedFiles) { std::filesystem::create_directories(std::filesystem::path(file.fullPath).parent_path()); - std::cout << "adding file \"" << file.path << "\"\n"; + logger << "adding file \"" << file.path << "\"\r\n"; // Write the 'insert' instructions // Remember that we use the diff/patch function to add new files too @@ -548,14 +558,14 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Confirm new hashes match if (newHash != file.diff_newHash) { - std::cout << "Critical failure: new file is corrupted (hash mismatch).\n"; + logger << "Critical failure: new file is corrupted (hash mismatch).\r\n"; return false; } // Write new file to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - std::cout << "Critical failure: cannot write new file to disk.\n"; + logger << "Critical failure: cannot write new file to disk.\r\n"; return false; } newFile.write(newBuffer, std::streamsize(newSize)); @@ -575,14 +585,14 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) - std::cout << "The file \"" << file.path << "\" has already been removed, skipping...\n"; + logger << "The file \"" << file.path << "\" has already been removed, skipping...\r\n"; else { // Only remove source files if they match entirely if (oldHash == file.diff_oldHash) if (!std::filesystem::remove(file.fullPath)) - std::cout << "Error: cannot delete file \"" << file.path << "\" from disk, delete this file manually if you can. \n"; + logger << "Error: cannot delete file \"" << file.path << "\" from disk, delete this file manually if you can. \r\n"; else - std::cout << "removing file \"" << file.path << "\"\n"; + logger << "removing file \"" << file.path << "\"\r\n"; } // Cleanup and finish diff --git a/src/TaskLogger.h b/src/TaskLogger.h new file mode 100644 index 0000000..69c324c --- /dev/null +++ b/src/TaskLogger.h @@ -0,0 +1,78 @@ +#pragma once +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include + +/*********** SINGLETON ***********/ +/* */ +/* Performs logging operations */ +/* */ +/*********** SINGLETON ***********/ +class TaskLogger { +public: + // Public Methods + inline static TaskLogger & GetInstance() { + static TaskLogger instance; + return instance; + } + inline size_t addCallback_Text(std::function && func) { + auto index = m_textCallbacks.size(); + m_textCallbacks.emplace_back(std::move(func)); + return index; + }inline size_t addCallback_Progress(std::function && func) { + auto index = m_progressCallbacks.size(); + m_progressCallbacks.emplace_back(std::move(func)); + return index; + } + inline void removeCallback_Text(const size_t & index) { + m_textCallbacks.erase(m_textCallbacks.begin() + index); + } + inline void removeCallback_Progress(const size_t & index) { + m_progressCallbacks.erase(m_progressCallbacks.begin() + index); + } + inline TaskLogger& operator <<(const std::string & message) { + // Add the message to the log + m_log += message; + + // Notify all observers that the log has been updated + for each (const auto & callback in m_textCallbacks) + callback(message); + + // Returning a reference so we can chain values like + // log << value << value << value + return *this; + } + inline void setRange(const size_t & value) { + m_range = value; + } + inline size_t getRange() const { + return m_range; + } + inline void setProgress(const size_t & amount) { + m_pos = amount; + + // Notify all observers that the task has updated + for each (const auto & callback in m_progressCallbacks) + callback(m_pos, m_range); + } + + +private: + // Private (de)constructors + ~TaskLogger() = default; + TaskLogger() = default; + TaskLogger(TaskLogger const&) = delete; + void operator=(TaskLogger const&) = delete; + + + // Private Attributes + std::string m_log; + size_t m_range = 0ull, m_pos = 0ull; + std::vector> m_textCallbacks; + std::vector> m_progressCallbacks; +}; + +#endif // LOGGER_H \ No newline at end of file diff --git a/src/Threader.h b/src/Threader.h index 850191d..ae3ed9e 100644 --- a/src/Threader.h +++ b/src/Threader.h @@ -19,8 +19,9 @@ class Threader { shutdown(); } /** Creates a threader object and generates as many worker threads as the system allows. */ - inline Threader() { - for (size_t x = 0; x < std::thread::hardware_concurrency(); ++x) { + inline Threader(const size_t & maxThreads = std::thread::hardware_concurrency()) { + m_maxThreads = maxThreads; + for (size_t x = 0; x < m_maxThreads; ++x) { std::thread thread([&]() { while (m_alive) { // Check if there is a job to do @@ -64,7 +65,7 @@ class Threader { } inline void shutdown() { m_alive = false; - for (size_t x = 0; x < std::thread::hardware_concurrency() && x < m_threads.size(); ++x) { + for (size_t x = 0; x < m_maxThreads && x < m_threads.size(); ++x) { if (m_threads[x].joinable()) m_threads[x].join(); } @@ -81,6 +82,7 @@ class Threader { std::vector m_threads; std::deque> m_jobs; std::atomic_size_t m_threadsActive = 0ull, m_jobsStarted = 0ull, m_jobsFinished = 0ull; + size_t m_maxThreads = 0ull; }; #endif // THREADER_H \ No newline at end of file diff --git a/src/nStaller/CMakeLists.txt b/src/nStaller/CMakeLists.txt index 2689c36..e7a60b4 100644 --- a/src/nStaller/CMakeLists.txt +++ b/src/nStaller/CMakeLists.txt @@ -1,36 +1,51 @@ -# Get source files for this project +################ +### nStaller ### +################ +set (Module nStaller) + +# Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure +# Mimic the file-folder structure foreach(source IN LISTS ROOT) get_filename_component(source_path "${source}" PATH) string(REPLACE "/" "\\" source_path_msvc "${source_path}") source_group("${source_path_msvc}" FILES "${source}") endforeach() +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) -############ -# EXEC # -############ -set (Module nStaller) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp ) -# Set working directory to the project directory -set_target_properties (${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") +# Add libraries +target_link_libraries(${Module} + "Comctl32.lib" + "propsys.lib" + "Shlwapi.lib" +) + +# Set windows/visual studio settings +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) +# Force highest c++ version supported if (MSVC_VERSION GREATER_EQUAL "1900") include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp new file mode 100644 index 0000000..a2ba66a --- /dev/null +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -0,0 +1,210 @@ +#include "DirectoryFrame.h" +#include +#include +#include +#include +#include + + +constexpr static auto CLASS_NAME = "DIRECTORY_FRAME"; +constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +DirectoryFrame::~DirectoryFrame() +{ + UnregisterClass(CLASS_NAME, m_hinstance); + DeleteObject(m_hwnd); +} + +DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +{ + // Try to create window class + m_directory = directory; + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = CLASS_NAME; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + + // Create extra fields + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); + m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); +} + +void DirectoryFrame::setDirectory(const std::string & dir) +{ + *m_directory = dir; + SetWindowText(m_directoryField, dir.c_str()); +} + +const HWND DirectoryFrame::getBrowseButton() const +{ + return m_browseButton; +} + +static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) +{ + /** File Dialog Event Handler */ + class DialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents { + private: + ~DialogEventHandler() { }; + long _cRef; + + + public: + // Constructor + DialogEventHandler() : _cRef(1) { }; + + + // IUnknown methods + IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) { + static const QITAB qit[] = { + QITABENT(DialogEventHandler, IFileDialogEvents), + QITABENT(DialogEventHandler, IFileDialogControlEvents), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); + } + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement(&_cRef); + } + IFACEMETHODIMP_(ULONG) Release() { + long cRef = InterlockedDecrement(&_cRef); + if (!cRef) + delete this; + return cRef; + } + + + // IFileDialogEvents methods + IFACEMETHODIMP OnFileOk(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChanging(IFileDialog *, IShellItem *) { return S_OK; }; + IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnSelectionChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; + IFACEMETHODIMP OnTypeChange(IFileDialog *pfd) { return S_OK; }; + IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; + + + // IFileDialogControlEvents methods + IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *pfdc, DWORD dwIDCtl, DWORD dwIDItem) { return S_OK; }; + IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; + IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize *, DWORD, BOOL) { return S_OK; }; + IFACEMETHODIMP OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; + }; + + *ppv = NULL; + DialogEventHandler *pDialogEventHandler = new (std::nothrow) DialogEventHandler(); + HRESULT hr = pDialogEventHandler ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) { + hr = pDialogEventHandler->QueryInterface(riid, ppv); + pDialogEventHandler->Release(); + } + return hr; +} + +static HRESULT OpenFileDialog(std::string & directory) +{ + // CoCreate the File Open Dialog object. + IFileDialog *pfd = NULL; + IFileDialogEvents *pfde = NULL; + DWORD dwCookie, dwFlags; + HRESULT hr = S_FALSE; + if ( + SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))) && + SUCCEEDED(CreateDialogEventHandler(IID_PPV_ARGS(&pfde))) && + SUCCEEDED(pfd->Advise(pfde, &dwCookie)) && + SUCCEEDED(pfd->GetOptions(&dwFlags)) && + SUCCEEDED(pfd->SetOptions(dwFlags | FOS_PICKFOLDERS | FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT)) && + SUCCEEDED(pfd->Show(NULL)) + ) + { + // The result is an IShellItem object. + IShellItem *psiResult; + PWSTR pszFilePath = NULL; + if (SUCCEEDED(pfd->GetResult(&psiResult)) && SUCCEEDED(psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath))) { + std::wstringstream ss; + ss << pszFilePath; + auto ws = ss.str(); + typedef std::codecvt converter_type; + const std::locale locale(""); + const converter_type& converter = std::use_facet(locale); + std::vector to(ws.length() * converter.max_length()); + std::mbstate_t state; + const wchar_t* from_next; + char* to_next; + const converter_type::result result = converter.out(state, ws.data(), ws.data() + ws.length(), from_next, &to[0], &to[0] + to.size(), to_next); + if (result == converter_type::ok || result == converter_type::noconv) { + directory = std::string(&to[0], to_next); + hr = S_OK; + } + CoTaskMemFree(pszFilePath); + psiResult->Release(); + } + + // Unhook the event handler. + pfd->Unadvise(dwCookie); + pfde->Release(); + pfd->Release(); + } + return hr; +} + +static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + + // Draw Text + constexpr static char* text[] = { + "Where would you like to install to?", + "Choose a folder by pressing the 'Browse' button.", + "Alternatively, type a specific directory into the box below.", + "Press the 'Next' button to begin installing . . ." + }; + SelectObject(hdc, big_font); + SetTextColor(hdc, RGB(25, 125, 225)); + TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + + SelectObject(hdc, reg_font); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, 10, 100, text[1], _tcslen(text[1])); + TextOut(hdc, 10, 115, text[2], _tcslen(text[2])); + TextOut(hdc, 10, 420, text[3], _tcslen(text[3])); + + // Cleanup + DeleteObject(big_font); + DeleteObject(reg_font); + + EndPaint(hWnd, &ps); + return 0; + } + else if (message == WM_COMMAND) { + if (HIWORD(wParam) == BN_CLICKED) { + auto hndl = LOWORD(lParam); + if (hndl == LOWORD(ptr->getBrowseButton())) { + std::string directory(""); + if (SUCCEEDED(OpenFileDialog(directory))) + ptr->setDirectory(directory); + } + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h new file mode 100644 index 0000000..40bc277 --- /dev/null +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -0,0 +1,28 @@ +#pragma once +#ifndef DIRECTORYFRAME_H +#define DIRECTORYFRAME_H + +#include "Frame.h" +#include + + +/** Custom frame class, representing the installer 'directory choosing' screen. */ +class DirectoryFrame : public Frame { +public: + // Public (de)Constructors + ~DirectoryFrame(); + DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + + + // Public Methods + void setDirectory(const std::string & dir); + const HWND getBrowseButton() const; + + +private: + // Private Attributes + std::string * m_directory = nullptr; + HWND m_directoryField = nullptr, m_browseButton = nullptr; +}; + +#endif // DIRECTORYFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp new file mode 100644 index 0000000..da60dc4 --- /dev/null +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -0,0 +1,66 @@ +#include "FinishFrame.h" +#include + + +constexpr static auto CLASS_NAME = "FINISH_FRAME"; +constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FinishFrame::~FinishFrame() +{ + UnregisterClass(CLASS_NAME, m_hinstance); + DeleteObject(m_hwnd); +} + +FinishFrame::FinishFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +{ + // Try to create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = CLASS_NAME; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + setVisible(false); +} + +static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + + // Draw Text + constexpr static char* text[] = { + "Installation Complete", + "Press the 'Close' button to finish . . ." + }; + SelectObject(hdc, big_font); + SetTextColor(hdc, RGB(25, 125, 225)); + TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + + SelectObject(hdc, reg_font); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, 10, 420, text[1], _tcslen(text[1])); + + // Cleanup + DeleteObject(big_font); + DeleteObject(reg_font); + + EndPaint(hWnd, &ps); + return 0; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h new file mode 100644 index 0000000..646cc1d --- /dev/null +++ b/src/nStaller/Frames/FinishFrame.h @@ -0,0 +1,16 @@ +#pragma once +#ifndef FINISHFRAME_H +#define FINISHFRAME_H + +#include "Frame.h" + + +/** Custom frame class, representing the installer 'finish' screen. */ +class FinishFrame : public Frame { +public: + // Public (de)Constructors + ~FinishFrame(); + FinishFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); +}; + +#endif // FINISHFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/Frame.h b/src/nStaller/Frames/Frame.h new file mode 100644 index 0000000..9b00af1 --- /dev/null +++ b/src/nStaller/Frames/Frame.h @@ -0,0 +1,27 @@ +#pragma once +#ifndef FRAME_H +#define FRAME_H + +#include + + +/** Encapsulation of a windows 'window' object.*/ +class Frame { +public: + // Public Methods + /** Sets the visibility & enable state of this window. + @param state whether or not to show and activate this window. */ + void setVisible(const bool & state) { + ShowWindow(m_hwnd, state); + EnableWindow(m_hwnd, state); + } + + +protected: + // Private Attributes + WNDCLASSEX m_wcex; + HWND m_hwnd = nullptr; + HINSTANCE m_hinstance; +}; + +#endif // FRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp new file mode 100644 index 0000000..d8d9916 --- /dev/null +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -0,0 +1,82 @@ +#include "InstallFrame.h" +#include "TaskLogger.h" +#include +#include + + +constexpr static auto CLASS_NAME = "INSTALL_FRAME"; +constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); +constexpr static auto FOREGROUND_COLOR2 = RGB(255, 255, 255); +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +InstallFrame::~InstallFrame() +{ + UnregisterClass(CLASS_NAME, m_hinstance); + DeleteObject(m_hwnd); + TaskLogger::GetInstance().removeCallback_Text(m_logIndex); + TaskLogger::GetInstance().removeCallback_Progress(m_taskIndex); +} + +InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +{ + // Try to create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = CLASS_NAME; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + + // Create log box + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, w-20, h-100, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, h - 40, w - 20, 25, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsText = CreateWindowEx(WS_EX_TRANSPARENT, "static", "0%", WS_CHILD | WS_VISIBLE | SS_CENTER, (w / 2)-20, 0, 40, 25, m_hwndPrgsBar, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); + auto & logger = TaskLogger::GetInstance(); + m_logIndex = logger.addCallback_Text([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = logger.addCallback_Progress([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + std::string s = std::to_string(int((float(position) / float(range)) * 100.0f))+ "%"; + SetWindowText(m_hwndPrgsText, s.c_str()); + }); + + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); +} + +static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + + // Draw Title + constexpr static auto text = "Installing"; + SelectObject(hdc, big_font); + SetTextColor(hdc, RGB(25, 125, 225)); + TextOut(hdc, 10, 10, text, _tcslen(text)); + + // Cleanup + DeleteObject(big_font); + DeleteObject(reg_font); + + EndPaint(hWnd, &ps); + return 0; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h new file mode 100644 index 0000000..8f7d1b3 --- /dev/null +++ b/src/nStaller/Frames/InstallFrame.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef INSTALLFRAME_H +#define INSTALLFRAME_H + +#include "Frame.h" +#include + + +/** Custom frame class, representing the installer 'install' screen. */ +class InstallFrame : public Frame { +public: + // Public (de)Constructors + ~InstallFrame(); + InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + + +private: + // Private Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_hwndPrgsText = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; +}; + +#endif // INSTALLFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp new file mode 100644 index 0000000..90447cf --- /dev/null +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -0,0 +1,79 @@ +#include "WelcomeFrame.h" +#include + + +constexpr static auto CLASS_NAME = "WELCOME_FRAME"; +constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +WelcomeFrame::~WelcomeFrame() +{ + UnregisterClass(CLASS_NAME, m_hinstance); + DeleteObject(m_hwnd); +} + +WelcomeFrame::WelcomeFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +{ + // Try to create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = CLASS_NAME; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + setVisible(false); +} + +static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font_under = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, TRUE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + + // Draw Text + constexpr static char* text[] = { + "Welcome", + "This custom installer was generated using nSuite", + "Source-code can be found at:", + "https://github.com/Yattabyte/nStallr/", + "Press the 'Next' button to continue . . ." + }; + SelectObject(hdc, big_font); + SetTextColor(hdc, RGB(25, 125, 225)); + TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + + SelectObject(hdc, reg_font); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, 10, 100, text[1], _tcslen(text[1])); + TextOut(hdc, 10, 115, text[2], _tcslen(text[2])); + + SelectObject(hdc, reg_font_under); + SetTextColor(hdc, RGB(0, 0, 238)); + TextOut(hdc, 35, 130, text[3], _tcslen(text[3])); + + SelectObject(hdc, reg_font); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, 10, 420, text[4], _tcslen(text[4])); + + // Cleanup + DeleteObject(big_font); + DeleteObject(reg_font); + DeleteObject(reg_font_under); + + EndPaint(hWnd, &ps); + return 0; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.h b/src/nStaller/Frames/WelcomeFrame.h new file mode 100644 index 0000000..ba0c184 --- /dev/null +++ b/src/nStaller/Frames/WelcomeFrame.h @@ -0,0 +1,16 @@ +#pragma once +#ifndef WELCOMEFRAME_H +#define WELCOMEFRAME_H + +#include "Frame.h" + + +/** Custom frame class, representing the installer 'welcome' screen. */ +class WelcomeFrame : public Frame { +public: + // Public (de)Constructors + ~WelcomeFrame(); + WelcomeFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); +}; + +#endif // WELCOMEFRAME_H \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 2f3862e..697b2ef 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -2,44 +2,257 @@ #include "DirectoryTools.h" #include "Resource.h" #include +#include +// +// +///** Entry point. */ +//int main() +//{ +// // Check command line arguments +// std::string dstDirectory(get_current_directory()); +// +// // Report an overview of supplied procedure +// std::cout << +// " ~\r\n" +// " Installer /\r\n" +// " ~-----------------~\r\n" +// " /\r\n" +// "~\r\n\r\n" +// "Installing to the following directory:\r\n" +// "\t> " + dstDirectory + "\\\r\n" +// "\r\n"; +// pause_program("Ready to install?"); +// +// // Acquire archive resource +// const auto start = std::chrono::system_clock::now(); +// size_t fileCount(0ull), byteCount(0ull); +// Resource archive(IDR_ARCHIVE, "ARCHIVE"); +// if (!archive.exists()) +// exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); +// +// // Unpackage using the resource file +// if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) +// exit_program("Cannot decompress embedded package resource, aborting...\r\n"); +// +// // Success, report results +// const auto end = std::chrono::system_clock::now(); +// const std::chrono::duration elapsed_seconds = end - start; +// std::cout +// << "Files written: " << fileCount << "\r\n" +// << "Bytes written: " << byteCount << "\r\n" +// << "Total duration: " << elapsed_seconds.count() << " seconds\r\n\r\n"; +// system("pause"); +// exit(EXIT_SUCCESS); +//}*/ +#include "Frames/WelcomeFrame.h" +#include "Frames/DirectoryFrame.h" +#include "Frames/InstallFrame.h" +#include "Frames/FinishFrame.h" +#include "Threader.h" +#include +#include +#include +#include +#include -/** Entry point. */ -int main() + +constexpr static auto WINDOW_CLASS = "nStaller"; +constexpr static auto WINDOW_TITLE = "Installer"; +constexpr static auto HEADER_COLOR = RGB(240, 240, 240); +constexpr static auto FOOTER_COLOR = RGB(220, 220, 220); +constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); +constexpr static auto BACKGROUND_COLOR = RGB(200, 200, 200); +static Threader threader = Threader(1ull); +static std::string directory = "E:\\Test"; +static std::vector frames; +static std::vector> frameOperations; +static int FRAME_INDEX = 0; +static Resource ARCHIVE(IDR_ARCHIVE, "ARCHIVE"); +static HWND hwnd_window, hwnd_exitButton, hwnd_prevButton, hwnd_nextButton, hwnd_dialogue = NULL; +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +static bool CreateMainWindow(HINSTANCE hInstance) { - // Check command line arguments - std::string dstDirectory(get_current_directory()); - - // Report an overview of supplied procedure - std::cout << - " ~\n" - " Installer /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - "Installing to the following directory:\n" - "\t> " + dstDirectory + "\\\n" - "\n"; - pause_program("Ready to install?"); - - // Acquire archive resource - const auto start = std::chrono::system_clock::now(); - size_t fileCount(0ull), byteCount(0ull); - Resource archive(IDR_ARCHIVE, "ARCHIVE"); - if (!archive.exists()) - exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\n"); + // Try to create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = WINDOW_CLASS; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + if (!RegisterClassEx(&wcex)) + return false; + + // Try to create window object + hwnd_window = CreateWindow( + WINDOW_CLASS, WINDOW_TITLE, + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + hwnd_prevButton = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + hwnd_nextButton = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + hwnd_exitButton = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + + auto dwStyle = GetWindowLongPtr(hwnd_window, GWL_STYLE); + auto dwExStyle = GetWindowLongPtr(hwnd_window, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(hwnd_window, true); + UpdateWindow(hwnd_window); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(hwnd_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + return true; +} + +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) +{ + if (!CreateMainWindow(hInstance)) + exit(EXIT_FAILURE); + + // Welcome Screen + frames.emplace_back(new WelcomeFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + frameOperations.emplace_back([&]() { + ShowWindow(hwnd_exitButton, true); + ShowWindow(hwnd_prevButton, false); + ShowWindow(hwnd_nextButton, true); + frames[FRAME_INDEX]->setVisible(true); + }); - // Unpackage using the resource file - if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) - exit_program("Cannot decompress embedded package resource, aborting...\n"); - - // Success, report results - const auto end = std::chrono::system_clock::now(); - const std::chrono::duration elapsed_seconds = end - start; - std::cout - << "Files written: " << fileCount << "\n" - << "Bytes written: " << byteCount << "\n" - << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; - system("pause"); - exit(EXIT_SUCCESS); + // Directory Screen + frames.emplace_back(new DirectoryFrame(&directory, hInstance, hwnd_window, 170, 0, 630, 450)); + frameOperations.emplace_back([&]() { + ShowWindow(hwnd_exitButton, true); + ShowWindow(hwnd_prevButton, true); + ShowWindow(hwnd_nextButton, true); + frames[FRAME_INDEX]->setVisible(true); + }); + + // Installation Screen + frames.emplace_back(new InstallFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + frameOperations.emplace_back([&]() { + EnableWindow(hwnd_exitButton, false); + EnableWindow(hwnd_prevButton, false); + EnableWindow(hwnd_nextButton, false); + frames[FRAME_INDEX]->setVisible(true); + + // Acquire archive resource + threader.addJob([]() { + const auto start = std::chrono::system_clock::now(); + size_t fileCount(0ull), byteCount(0ull); + Resource archive(IDR_ARCHIVE, "ARCHIVE"); + if (!archive.exists()) { + //exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); + } + + // Unpackage using the resource file + else if (!DRT::DecompressDirectory(directory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) { + // exit_program("Cannot decompress embedded package resource, aborting...\r\n"); + } + + EnableWindow(hwnd_nextButton, true); + }); + }); + + // Finish Screen + frames.emplace_back(new FinishFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + frameOperations.emplace_back([&]() { + EnableWindow(hwnd_exitButton, true); + ShowWindow(hwnd_exitButton, true); + ShowWindow(hwnd_prevButton, false); + ShowWindow(hwnd_nextButton, false); + frames[FRAME_INDEX]->setVisible(true); + SetWindowText(hwnd_exitButton, "Close"); + }); + + // Begin first frame + frameOperations[FRAME_INDEX](); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return (int)msg.wParam; +} + +LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto font = CreateFont(25, 10, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + SelectObject(hdc, font); + + // Background + SelectObject(hdc, GetStockObject(DC_BRUSH)); + SelectObject(hdc, GetStockObject(DC_PEN)); + SetDCBrushColor(hdc, RGB(25,25,25)); + SetDCPenColor(hdc, RGB(25,25,25)); + SetBkColor(hdc, RGB(25,25,25)); + Rectangle(hdc, 0, 0, 800, 500); + + // Footer + SetDCBrushColor(hdc, RGB(75,75,75)); + SetDCPenColor(hdc, RGB(75, 75, 75)); + Rectangle(hdc, 0, 450, 800, 500); + + // Steps + constexpr static char* step_labels[] = { "Welcome", "Directory", "Install", "Finish" }; + SetDCBrushColor(hdc, RGB(100, 100, 100)); + SetDCPenColor(hdc, RGB(100, 100, 100)); + Rectangle(hdc, 26, 0, 29, 450); + int vertical_offset = 15; + for (int x = 0; x < 4; ++x) { + // Draw Circle + auto color = x == FRAME_INDEX ? RGB(25, 225, 125) : x < FRAME_INDEX ? RGB(25, 125, 225) : RGB(255, 255, 255); + SetDCBrushColor(hdc, color); + SetDCPenColor(hdc, color); + SetTextColor(hdc, color); + Ellipse(hdc, 20, vertical_offset + 6, 35, vertical_offset + 21); + + // Draw Text + TextOut(hdc, 50, vertical_offset, step_labels[x], _tcslen(step_labels[x])); + vertical_offset += 50; + + if (x == 2) + vertical_offset = 420; + } + + DeleteObject(font); + EndPaint(hWnd, &ps); + } + else if (message == WM_DESTROY) + PostQuitMessage(0); + else if (message == WM_COMMAND) { + if (HIWORD(wParam) == BN_CLICKED) { + auto hndl = LOWORD(lParam); + // A button has been clicked, so SOMETHING drastic is going to happen + frames[FRAME_INDEX]->setVisible(false); + // If exit + if (hndl == LOWORD(hwnd_exitButton)) + PostQuitMessage(0); + // If previous + else if (hndl == LOWORD(hwnd_prevButton)) + frameOperations[--FRAME_INDEX](); + // If next + else if (hndl == LOWORD(hwnd_nextButton)) + frameOperations[++FRAME_INDEX](); + + RECT rc = { 0, 0, 160, 450 }; + RedrawWindow(hwnd_window, &rc, NULL, RDW_INVALIDATE); + } + } + else + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc index fb7149a8888d69ba8c4697da99b4d9ea53ad55ea..c451470029e76c508c80ca90fc72600b5e0bb206 100644 GIT binary patch delta 19 ZcmZ3)wTf$l2Fqj>mK-3~o4ku90{}fs1_l5C delta 19 ZcmZ3*wTNqj2Fv6bELxMpSnPoGBmhBo27&+p diff --git a/src/nSuite/CMakeLists.txt b/src/nSuite/CMakeLists.txt index 0620ca8..0912284 100644 --- a/src/nSuite/CMakeLists.txt +++ b/src/nSuite/CMakeLists.txt @@ -1,38 +1,46 @@ -# Get source files for this project +############## +### nSuite ### +############## +set (Module nSuite) + +# Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure +# Mimic the file-folder structure foreach(source IN LISTS ROOT) get_filename_component(source_path "${source}" PATH) string(REPLACE "/" "\\" source_path_msvc "${source_path}") source_group("${source_path_msvc}" FILES "${source}") endforeach() +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) -############ -# EXEC # -############ -set (Module nSuite) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp) -# This module requires the installer to be built first -add_dependencies(nSuite nStaller) -# Set working directory to the project directory -set_target_properties(${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} ) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") +# This module requires the installer and updater to be built first +add_dependencies(nSuite nStaller nUpdater) + +# Set visual studio settings +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) +# Force highest c++ version supported if (MSVC_VERSION GREATER_EQUAL "1900") include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 361b3fd..d5bd527 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -1,19 +1,21 @@ #include "DiffCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void DiffCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Patch Maker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + auto & logger = TaskLogger::GetInstance(); + logger << + " ~\r\n" + " Patch Maker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n"; // Check command line arguments std::string oldDirectory(""), newDirectory(""), dstDirectory(""); @@ -28,11 +30,11 @@ void DiffCommand::execute(const int & argc, char * argv[]) const dstDirectory = std::string(&argv[x][5]); else exit_program( - " Arguments Expected:\n" - " -old=[path to the older directory]\n" - " -new=[path to the newer directory]\n" - " -dst=[path to write the diff file] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -old=[path to the older directory]\r\n" + " -new=[path to the newer directory]\r\n" + " -dst=[path to write the diff file] (can omit filename)\r\n" + "\r\n" ); } @@ -55,13 +57,13 @@ void DiffCommand::execute(const int & argc, char * argv[]) const char * diffBuffer(nullptr); size_t diffSize(0ull), instructionCount(0ull); if (!DRT::DiffDirectory(oldDirectory, newDirectory, &diffBuffer, diffSize, instructionCount)) - exit_program("aborting...\n"); + exit_program("aborting...\r\n"); // Create diff file std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write diff file to disk, aborting...\n"); + exit_program("Cannot write diff file to disk, aborting...\r\n"); // Write the diff file to disk file.write(diffBuffer, std::streamsize(diffSize)); @@ -69,7 +71,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const delete[] diffBuffer; // Output results - std::cout - << "Instruction(s): " << instructionCount << "\n" - << "Bytes written: " << diffSize << "\n"; + logger + << "Instruction(s): " << std::to_string(instructionCount) << "\r\n" + << "Bytes written: " << std::to_string(diffSize) << "\r\n"; } \ No newline at end of file diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index b430842..a9e8864 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -1,20 +1,22 @@ #include "InstallerCommand.h" #include "BufferTools.h" +#include "Common.h" #include "DirectoryTools.h" +#include "TaskLogger.h" #include "Resource.h" -#include "Common.h" #include void InstallerCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Installer Maker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + auto & logger = TaskLogger::GetInstance(); + logger << + " ~\r\n" + " Installer Maker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n"; // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -27,10 +29,10 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const dstDirectory = std::string(&argv[x][5]); else exit_program( - " Arguments Expected:\n" - " -src=[path to the directory to compress]\n" - " -dst=[path to write the installer] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the directory to compress]\r\n" + " -dst=[path to write the installer] (can omit filename)\r\n" + "\r\n" ); } @@ -46,30 +48,30 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const char * packBuffer(nullptr); size_t packSize(0ull), fileCount(0ull); if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) - exit_program("Cannot create installer from the directory specified, aborting...\n"); + exit_program("Cannot create installer from the directory specified, aborting...\r\n"); // Acquire installer resource Resource installer(IDR_INSTALLER, "INSTALLER"); if (!installer.exists()) - exit_program("Cannot access installer resource, aborting...\n"); + exit_program("Cannot access installer resource, aborting...\r\n"); // Write installer to disk std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write installer to disk, aborting...\n"); + exit_program("Cannot write installer to disk, aborting...\r\n"); file.write(reinterpret_cast(installer.getPtr()), (std::streamsize)installer.getSize()); file.close(); // Update installer's resource auto handle = BeginUpdateResource(dstDirectory.c_str(), false); if (!(bool)UpdateResource(handle, "ARCHIVE", MAKEINTRESOURCE(IDR_ARCHIVE), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), packBuffer, (DWORD)packSize)) - exit_program("Cannot write archive contents to the installer, aborting...\n"); + exit_program("Cannot write archive contents to the installer, aborting...\r\n"); EndUpdateResource(handle, FALSE); delete[] packBuffer; // Output results - std::cout - << "Files packaged: " << fileCount << "\n" - << "Bytes packaged: " << packSize << "\n"; + logger + << "Files packaged: " << std::to_string(fileCount) << "\r\n" + << "Bytes packaged: " << std::to_string(packSize) << "\r\n"; } \ No newline at end of file diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index e5e1138..9e41ed9 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -1,19 +1,21 @@ #include "PackCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void PackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Packager /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + auto & logger = TaskLogger::GetInstance(); + logger << + " ~\r\n" + " Packager /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n"; // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -26,10 +28,10 @@ void PackCommand::execute(const int & argc, char * argv[]) const dstDirectory = std::string(&argv[x][5]); else exit_program( - " Arguments Expected:\n" - " -src=[path to the directory to compress]\n" - " -dst=[path to write the package] (can omit filename)\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the directory to compress]\r\n" + " -dst=[path to write the package] (can omit filename)\r\n" + "\r\n" ); } @@ -45,19 +47,19 @@ void PackCommand::execute(const int & argc, char * argv[]) const char * packBuffer(nullptr); size_t packSize(0ull), fileCount(0ull); if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) - exit_program("Cannot create package from the directory specified, aborting...\n"); + exit_program("Cannot create package from the directory specified, aborting...\r\n"); // Write package to disk std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); if (!file.is_open()) - exit_program("Cannot write package to disk, aborting...\n"); + exit_program("Cannot write package to disk, aborting...\r\n"); file.write(packBuffer, (std::streamsize)packSize); file.close(); delete[] packBuffer; // Output results - std::cout - << "Files packaged: " << fileCount << "\n" - << "Bytes packaged: " << packSize << "\n"; + logger + << "Files packaged: " << std::to_string(fileCount) << "\r\n" + << "Bytes packaged: " << std::to_string(packSize) << "\r\n"; } \ No newline at end of file diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 128fa23..52c3762 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -1,19 +1,21 @@ #include "PatchCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void PatchCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Patcher /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + auto & logger = TaskLogger::GetInstance(); + logger << + " ~\r\n" + " Patcher /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n"; // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -26,10 +28,10 @@ void PatchCommand::execute(const int & argc, char * argv[]) const dstDirectory = std::string(&argv[x][5]); else exit_program( - " Arguments Expected:\n" - " -src=[path to the .ndiff file]\n" - " -dst=[path to the directory to patch]\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the .ndiff file]\r\n" + " -dst=[path to the directory to patch]\r\n" + "\r\n" ); } @@ -37,7 +39,7 @@ void PatchCommand::execute(const int & argc, char * argv[]) const std::ifstream diffFile(srcDirectory, std::ios::binary | std::ios::beg); const size_t diffSize = std::filesystem::file_size(srcDirectory); if (!diffFile.is_open()) - exit_program("Cannot read diff file, aborting...\n"); + exit_program("Cannot read diff file, aborting...\r\n"); // Patch the directory specified char * diffBuffer = new char[diffSize]; @@ -45,11 +47,11 @@ void PatchCommand::execute(const int & argc, char * argv[]) const diffFile.close(); size_t bytesWritten(0ull), instructionsUsed(0ull); if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) - exit_program("aborting...\n"); + exit_program("aborting...\r\n"); delete[] diffBuffer; // Output results - std::cout - << "Instruction(s): " << instructionsUsed << "\n" - << "Bytes written: " << bytesWritten << "\n"; + logger + << "Instruction(s): " << std::to_string(instructionsUsed) << "\r\n" + << "Bytes written: " << std::to_string(bytesWritten) << "\r\n"; } \ No newline at end of file diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index d5bcc36..f70bd53 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -1,19 +1,21 @@ #include "UnpackCommand.h" #include "BufferTools.h" -#include "DirectoryTools.h" #include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" #include void UnpackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - std::cout << - " ~\n" - " Unpacker /\n" - " ~-----------------~\n" - " /\n" - "~\n\n"; + auto & logger = TaskLogger::GetInstance(); + logger << + " ~\r\n" + " Unpacker /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n"; // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -26,10 +28,10 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const dstDirectory = std::string(&argv[x][5]); else exit_program( - " Arguments Expected:\n" - " -src=[path to the package file]\n" - " -dst=[directory to write package contents]\n" - "\n" + " Arguments Expected:\r\n" + " -src=[path to the package file]\r\n" + " -dst=[directory to write package contents]\r\n" + "\r\n" ); } @@ -37,7 +39,7 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const std::ifstream packFile(srcDirectory, std::ios::binary | std::ios::beg); const size_t packSize = std::filesystem::file_size(srcDirectory); if (!packFile.is_open()) - exit_program("Cannot read diff file, aborting...\n"); + exit_program("Cannot read diff file, aborting...\r\n"); // Copy contents into a buffer char * packBuffer = new char[packSize]; @@ -47,10 +49,10 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const // Unpackage using the resource file size_t fileCount(0ull), byteCount(0ull); if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, byteCount, fileCount)) - exit_program("Cannot decompress package file, aborting...\n"); + exit_program("Cannot decompress package file, aborting...\r\n"); // Output results - std::cout - << "Files written: " << fileCount << "\n" - << "Bytes written: " << byteCount << "\n"; + logger + << "Files written: " << std::to_string(fileCount) << "\r\n" + << "Bytes written: " << std::to_string(byteCount) << "\r\n"; } \ No newline at end of file diff --git a/src/nSuite/nSuite.cpp b/src/nSuite/nSuite.cpp index eb15202..5842027 100644 --- a/src/nSuite/nSuite.cpp +++ b/src/nSuite/nSuite.cpp @@ -1,4 +1,5 @@ #include "Common.h" +#include "TaskLogger.h" #include // Command inclusions @@ -23,22 +24,26 @@ int main(int argc, char *argv[]) { "-diff" , new DiffCommand() }, { "-patch" , new PatchCommand() } }; + auto & logger = TaskLogger::GetInstance(); + logger.addCallback_Text([&](const std::string & message) { + std::cout << message; + }); // Check for valid arguments if (argc <= 1 || commandMap.find(argv[1]) == commandMap.end()) exit_program( - " ~\n" - " nStallr Help: /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - " Operations Supported:\n" - " -installer (To package and compress an entire directory into an executable file)\n" - " -pack (To compress an entire directory into a single .npack file)\n" - " -unpack (To decompress an entire directory from a .npack file)\n" - " -diff (To diff an entire directory into a single .ndiff file)\n" - " -patch (To patch an entire directory from a .ndiff file)\n" - "\n\n" + " ~\r\n" + " nSuite Help: /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + " Operations Supported:\r\n" + " -installer (To package and compress an entire directory into an executable file)\r\n" + " -pack (To compress an entire directory into a single .npack file)\r\n" + " -unpack (To decompress an entire directory from a .npack file)\r\n" + " -diff (To diff an entire directory into a single .ndiff file)\r\n" + " -patch (To patch an entire directory from a .ndiff file)\r\n" + "\r\n\r\n" ); // Command exists in command map, execute it @@ -47,7 +52,7 @@ int main(int argc, char *argv[]) // Output results and finish const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - std::cout << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; + logger << "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n"; system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/nUpdater/CMakeLists.txt b/src/nUpdater/CMakeLists.txt index fc1600e..0571401 100644 --- a/src/nUpdater/CMakeLists.txt +++ b/src/nUpdater/CMakeLists.txt @@ -1,36 +1,43 @@ -# Get source files for this project +################ +### nUpdater ### +################ +set (Module nUpdater) + +# Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") -# Generate source groups mimicking the folder structure +# Mimic the file-folder structure foreach(source IN LISTS ROOT) get_filename_component(source_path "${source}" PATH) string(REPLACE "/" "\\" source_path_msvc "${source_path}") source_group("${source_path_msvc}" FILES "${source}") endforeach() +# Add source files +add_executable(${Module} + ${ROOT} + ${CORE_DIR}/Common.h + ${CORE_DIR}/Resource.h + ${CORE_DIR}/Threader.h + ${CORE_DIR}/Instructions.h + ${CORE_DIR}/Instructions.cpp + ${CORE_DIR}/BufferTools.h + ${CORE_DIR}/BufferTools.cpp + ${CORE_DIR}/DirectoryTools.h + ${CORE_DIR}/DirectoryTools.cpp + ${CORE_DIR}/TaskLogger.h +) -############ -# EXEC # -############ -set (Module nUpdater) -# Create a library using those source files -add_executable(${Module} ${ROOT} - ${CORE_DIR}/Common.h - ${CORE_DIR}/Resource.h - ${CORE_DIR}/Threader.h - ${CORE_DIR}/Instructions.h - ${CORE_DIR}/Instructions.cpp - ${CORE_DIR}/BufferTools.h - ${CORE_DIR}/BufferTools.cpp - ${CORE_DIR}/DirectoryTools.h - ${CORE_DIR}/DirectoryTools.cpp ) -# Set working directory to the project directory -set_target_properties (${Module} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} - PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) -target_compile_Definitions (${Module} PRIVATE $<$:DEBUG>) -set_target_properties(${Module} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app") +# Set visual studio settings +target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) +# Force highest c++ version supported if (MSVC_VERSION GREATER_EQUAL "1900") include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) diff --git a/src/nUpdater/nUpdater.cpp b/src/nUpdater/nUpdater.cpp index 8a60cb7..7e148b4 100644 --- a/src/nUpdater/nUpdater.cpp +++ b/src/nUpdater/nUpdater.cpp @@ -1,5 +1,6 @@ #include "Common.h" #include "DirectoryTools.h" +#include "TaskLogger.h" #include #include @@ -19,19 +20,24 @@ static auto get_patches(const std::string & srcDirectory) /** Entry point. */ int main() { + auto & logger = TaskLogger::GetInstance(); + logger.addCallback_Text([&](const std::string & message) { + std::cout << message; + }); + // Find all patch files? const auto dstDirectory(get_current_directory()); const auto patches = get_patches(dstDirectory); // Report an overview of supplied procedure - std::cout << - " ~\n" - " Updater /\n" - " ~-----------------~\n" - " /\n" - "~\n\n" - "There are " << patches.size() << " patches(s) found.\n" - "\n"; + logger << + " ~\r\n" + " Updater /\r\n" + " ~-----------------~\r\n" + " /\r\n" + "~\r\n\r\n" + "There are " << std::to_string(patches.size()) << " patches(s) found.\r\n" + "\r\n"; if (patches.size()) { pause_program("Ready to update?"); @@ -43,7 +49,7 @@ int main() std::ifstream diffFile(patch, std::ios::binary | std::ios::beg); const size_t diffSize = std::filesystem::file_size(patch); if (!diffFile.is_open()) { - std::cout << "Cannot read diff file, skipping...\n"; + logger << "Cannot read diff file, skipping...\r\n"; continue; } else { @@ -52,14 +58,14 @@ int main() diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) { - std::cout << "skipping patch...\n"; + logger << "skipping patch...\r\n"; delete[] diffBuffer; continue; } // Delete patch file at very end if (!std::filesystem::remove(patch)) - std::cout << "Cannot delete diff file \"" << patch << "\" from disk, make sure to delete it manually.\n"; + logger << "Cannot delete diff file \"" << patch.path().string() << "\" from disk, make sure to delete it manually.\r\n"; patchesApplied++; delete[] diffBuffer; } @@ -68,10 +74,10 @@ int main() // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - std::cout - << "Patches used: " << patchesApplied << " out of " << patches.size() << "\n" - << "Bytes written: " << bytesWritten << "\n" - << "Total duration: " << elapsed_seconds.count() << " seconds\n\n"; + logger + << "Patches used: " << std::to_string(patchesApplied) << " out of " << std::to_string(patches.size()) << "\r\n" + << "Bytes written: " << std::to_string(bytesWritten) << "\r\n" + << "Total duration: " << std::to_string(elapsed_seconds.count()) << " seconds\r\n\r\n"; } system("pause"); From e80fc6509569e61615a4d097272f9151bec88fda Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Wed, 3 Apr 2019 23:10:46 -0500 Subject: [PATCH 02/44] Updated Installer Better support for the chosen directories, bug fixes --- src/BufferTools.cpp | 20 ++- src/BufferTools.h | 2 +- src/DirectoryTools.cpp | 53 +++--- src/nStaller/Frames/DirectoryFrame.cpp | 16 +- src/nStaller/Frames/DirectoryFrame.h | 4 +- src/nStaller/Frames/InstallFrame.cpp | 2 +- src/nStaller/nStaller.cpp | 229 +++++++++++-------------- src/nSuite/Commands/UnpackCommand.cpp | 15 +- 8 files changed, 155 insertions(+), 186 deletions(-) diff --git a/src/BufferTools.cpp b/src/BufferTools.cpp index a62a165..3092e0b 100644 --- a/src/BufferTools.cpp +++ b/src/BufferTools.cpp @@ -1,33 +1,35 @@ #include "BufferTools.h" +#include "Common.h" #include "Instructions.h" #include "Threader.h" #include "lz4.h" #include -bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize) +bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize, const size_t & headerPadding) { // Allocate enough room for the compressed buffer (2 size_t bigger than source buffer) - destinationSize = (sourceSize * 2ull) + size_t(sizeof(size_t)); + destinationSize = (sourceSize * 2ull) + size_t(sizeof(size_t)) + headerPadding; *destinationBuffer = new char[destinationSize]; + // Offset pointer by the amount requested for header-space + char * pointer = reinterpret_cast(PTR_ADD(*destinationBuffer, headerPadding)); + // First chunk of data = the total uncompressed size - *reinterpret_cast(*destinationBuffer) = sourceSize; + *reinterpret_cast(pointer) = sourceSize; // Increment pointer so that the compression works on the remaining part of the buffer - *destinationBuffer = reinterpret_cast(*destinationBuffer) + size_t(sizeof(size_t)); + pointer = reinterpret_cast(pointer) + size_t(sizeof(size_t)); // Compress the buffer auto result = LZ4_compress_default( sourceBuffer, - *destinationBuffer, + pointer, int(sourceSize), - int(destinationSize - size_t(sizeof(size_t))) + int(destinationSize - size_t(sizeof(size_t)) - headerPadding) ); - // Decrement pointer - *destinationBuffer = reinterpret_cast(*destinationBuffer) - size_t(sizeof(size_t)); - destinationSize = size_t(result) + sizeof(size_t); + destinationSize = headerPadding + size_t(sizeof(size_t)) + size_t(result); return (result > 0); } diff --git a/src/BufferTools.h b/src/BufferTools.h index 8e0e63c..94d8e99 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -11,7 +11,7 @@ namespace BFT { @param destinationBuffer pointer to the destination buffer, which will hold compressed contents. @param destinationSize reference updated with the size in bytes of the compressed destinationBuffer. @return true if compression success, false otherwise. */ - bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); + bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize, const size_t & headerPadding = 0ull); /** Decompressess a source buffer into an equal or larger sized destination buffer. @param sourceBuffer the original buffer to read from. @param sourceSize the size in bytes of the source buffer. diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 5fe9da1..93d2fd9 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -34,7 +34,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Calculate final file size using all the files in this directory, // and make a list containing all relevant files and their attributes - size_t archiveSize = sizeof(size_t) + folderName.size(); // include size of the root folder name + size_t archiveSize(0ull); for each (const auto & entry in directoryArray) { auto path = entry.path().string(); path = path.substr(absolute_path_length, path.size() - absolute_path_length); @@ -53,17 +53,8 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Create buffer for final file data char * filebuffer = new char[archiveSize]; - // Begin writing data into buffer - void * pointer = filebuffer; - - // Write root folder name and size - auto pathSize = folderName.size(); - memcpy(pointer, reinterpret_cast(&pathSize), size_t(sizeof(size_t))); - pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); - memcpy(pointer, folderName.data(), pathSize); - pointer = PTR_ADD(pointer, pathSize); - // Write file data into the buffer + void * pointer = filebuffer; for each (const auto & file in files) { threader.addJob([file, pointer]() { // Write the total number of characters in the path string, into the archive @@ -96,14 +87,22 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer continue; threader.shutdown(); - // Compress the archive - if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize)) { + // Compress the archive, pad the beginning with header-space + if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize, sizeof(size_t) + folderName.size())) { logger << "Critical failure: cannot perform compression operation on the set of joined files.\r\n"; return false; } + delete[] filebuffer; + + // Write root folder name and size + pointer = *packBuffer; + auto pathSize = folderName.size(); + memcpy(pointer, reinterpret_cast(&pathSize), size_t(sizeof(size_t))); + pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); + memcpy(pointer, folderName.data(), pathSize); + pointer = PTR_ADD(pointer, pathSize); // Clean up - delete[] filebuffer; return true; } @@ -120,18 +119,9 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Begin reading the archive void * readingPtr = decompressedBuffer; - - // Read the root folder name and size - const auto folderSize = *reinterpret_cast(readingPtr); - readingPtr = PTR_ADD(readingPtr, size_t(sizeof(size_t))); - const char * folderArray = reinterpret_cast(readingPtr); - const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); - readingPtr = PTR_ADD(readingPtr, folderSize); - - // Read the archive - size_t bytesRead = sizeof(size_t) + folderSize; - std::atomic_size_t filesWritten(0ull), bytesWritten(0ull); - log.setRange(decompressedSize + 1); + size_t bytesRead(0ull); + std::atomic_size_t filesWritten(0ull); + log.setRange(decompressedSize + 100); while (bytesRead < decompressedSize) { // Read the total number of characters from the path string, from the archive const auto pathSize = *reinterpret_cast(readingPtr); @@ -139,7 +129,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Read the file path string, from the archive const char * path_array = reinterpret_cast(readingPtr); - const auto path = finalDestionation + std::string(path_array, pathSize); + const auto path = dstDirectory + std::string(path_array, pathSize); readingPtr = PTR_ADD(readingPtr, pathSize); // Read the file size in bytes, from the archive @@ -148,20 +138,19 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Write file out to disk, from the archive void * ptrCopy = readingPtr; // needed for lambda, since readingPtr gets incremented - threader.addJob([ptrCopy, path, fileSize, &filesWritten, &bytesWritten]() { + threader.addJob([ptrCopy, path, fileSize, &filesWritten]() { // Write-out the file if it doesn't exist yet, if the size is different, or if it's older std::filesystem::create_directories(std::filesystem::path(path).parent_path()); std::ofstream file(path, std::ios::binary | std::ios::out); if (file.is_open()) file.write(reinterpret_cast(ptrCopy), (std::streamsize)fileSize); file.close(); - bytesWritten += fileSize; filesWritten++; }); readingPtr = PTR_ADD(readingPtr, fileSize); bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; - log << "Writing file: " << std::string(path_array, pathSize) << "\r\n"; + log << "Writing file: " + std::string(path_array, pathSize) + "\r\n"; log.setProgress(bytesRead); } @@ -170,11 +159,11 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe while (!threader.isFinished()) continue; threader.shutdown(); - log.setProgress(bytesRead + 1); + log.setProgress(bytesRead + 100); // Success fileCount = filesWritten; - byteCount = bytesWritten; + byteCount = bytesRead; delete[] decompressedBuffer; return true; } diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index a2ba66a..614d3c1 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -40,6 +40,7 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInsta m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_directoryField, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); } @@ -49,11 +50,6 @@ void DirectoryFrame::setDirectory(const std::string & dir) SetWindowText(m_directoryField, dir.c_str()); } -const HWND DirectoryFrame::getBrowseButton() const -{ - return m_browseButton; -} - static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) { /** File Dialog Event Handler */ @@ -199,12 +195,20 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) else if (message == WM_COMMAND) { if (HIWORD(wParam) == BN_CLICKED) { auto hndl = LOWORD(lParam); - if (hndl == LOWORD(ptr->getBrowseButton())) { + if (hndl == LOWORD(ptr->m_browseButton)) { std::string directory(""); if (SUCCEEDED(OpenFileDialog(directory))) ptr->setDirectory(directory); } } + else if (HIWORD(wParam) == EN_CHANGE) { + if (ptr != nullptr) { + int textLength = GetWindowTextLength(ptr->m_directoryField); + char * string = new char[textLength]; + GetWindowText(ptr->m_directoryField, string, textLength); + *ptr->m_directory = std::string(string, textLength); + } + } } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index 40bc277..bb0d012 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -16,11 +16,9 @@ class DirectoryFrame : public Frame { // Public Methods void setDirectory(const std::string & dir); - const HWND getBrowseButton() const; -private: - // Private Attributes + // Public Attributes std::string * m_directory = nullptr; HWND m_directoryField = nullptr, m_browseButton = nullptr; }; diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index d8d9916..58f642d 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -48,7 +48,7 @@ InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, con m_taskIndex = logger.addCallback_Progress([&](const size_t & position, const size_t & range) { SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); - std::string s = std::to_string(int((float(position) / float(range)) * 100.0f))+ "%"; + std::string s = std::to_string( position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f)))+ "%"; SetWindowText(m_hwndPrgsText, s.c_str()); }); diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 697b2ef..bcb87ce 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -1,181 +1,106 @@ #include "Common.h" +#include "BufferTools.h" #include "DirectoryTools.h" +#include "Threader.h" #include "Resource.h" -#include -#include -// -// -///** Entry point. */ -//int main() -//{ -// // Check command line arguments -// std::string dstDirectory(get_current_directory()); -// -// // Report an overview of supplied procedure -// std::cout << -// " ~\r\n" -// " Installer /\r\n" -// " ~-----------------~\r\n" -// " /\r\n" -// "~\r\n\r\n" -// "Installing to the following directory:\r\n" -// "\t> " + dstDirectory + "\\\r\n" -// "\r\n"; -// pause_program("Ready to install?"); -// -// // Acquire archive resource -// const auto start = std::chrono::system_clock::now(); -// size_t fileCount(0ull), byteCount(0ull); -// Resource archive(IDR_ARCHIVE, "ARCHIVE"); -// if (!archive.exists()) -// exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); -// -// // Unpackage using the resource file -// if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) -// exit_program("Cannot decompress embedded package resource, aborting...\r\n"); -// -// // Success, report results -// const auto end = std::chrono::system_clock::now(); -// const std::chrono::duration elapsed_seconds = end - start; -// std::cout -// << "Files written: " << fileCount << "\r\n" -// << "Bytes written: " << byteCount << "\r\n" -// << "Total duration: " << elapsed_seconds.count() << " seconds\r\n\r\n"; -// system("pause"); -// exit(EXIT_SUCCESS); -//}*/ - #include "Frames/WelcomeFrame.h" #include "Frames/DirectoryFrame.h" #include "Frames/InstallFrame.h" #include "Frames/FinishFrame.h" -#include "Threader.h" -#include -#include +#include +#include #include +#include #include +#include +#include #include -constexpr static auto WINDOW_CLASS = "nStaller"; -constexpr static auto WINDOW_TITLE = "Installer"; -constexpr static auto HEADER_COLOR = RGB(240, 240, 240); -constexpr static auto FOOTER_COLOR = RGB(220, 220, 220); -constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); -constexpr static auto BACKGROUND_COLOR = RGB(200, 200, 200); -static Threader threader = Threader(1ull); -static std::string directory = "E:\\Test"; -static std::vector frames; -static std::vector> frameOperations; -static int FRAME_INDEX = 0; -static Resource ARCHIVE(IDR_ARCHIVE, "ARCHIVE"); +static Threader threader(1ull); +static std::vector Frames; +static std::vector> FrameOperations; +static int FrameIndex = 0; static HWND hwnd_window, hwnd_exitButton, hwnd_prevButton, hwnd_nextButton, hwnd_dialogue = NULL; -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -static bool CreateMainWindow(HINSTANCE hInstance) -{ - // Try to create window class - WNDCLASSEX wcex; - wcex.cbSize = sizeof(WNDCLASSEX); - wcex.style = CS_HREDRAW | CS_VREDRAW; - wcex.lpfnWndProc = WndProc; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = hInstance; - wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wcex.lpszMenuName = NULL; - wcex.lpszClassName = WINDOW_CLASS; - wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); - if (!RegisterClassEx(&wcex)) - return false; - - // Try to create window object - hwnd_window = CreateWindow( - WINDOW_CLASS, WINDOW_TITLE, - WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, - CW_USEDEFAULT, CW_USEDEFAULT, - 800, 500, - NULL, NULL, hInstance, NULL - ); - constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - hwnd_prevButton = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - hwnd_nextButton = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - hwnd_exitButton = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - auto dwStyle = GetWindowLongPtr(hwnd_window, GWL_STYLE); - auto dwExStyle = GetWindowLongPtr(hwnd_window, GWL_EXSTYLE); - RECT rc = { 0, 0, 800, 500 }; - ShowWindow(hwnd_window, true); - UpdateWindow(hwnd_window); - AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); - SetWindowPos(hwnd_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); - return true; -} +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static bool CreateMainWindow(HINSTANCE, const std::string &); int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { - if (!CreateMainWindow(hInstance)) + // Get user's program files directory + TCHAR pf[MAX_PATH]; + SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); + std::string writeDirectory(pf), directoryName = ""; + + // Get installer's payload + Resource archive(IDR_ARCHIVE, "ARCHIVE"); + size_t archiveOffset(0ull); + if (!archive.exists()) { + // Show error screen + } + else { + // Read the package header + const auto folderSize = *reinterpret_cast(archive.getPtr()); + directoryName = std::string(reinterpret_cast(PTR_ADD(archive.getPtr(), size_t(sizeof(size_t)))), folderSize); + writeDirectory += "\\" + directoryName; + archiveOffset = size_t(sizeof(size_t)) + folderSize; + } + + if (!CreateMainWindow(hInstance, directoryName)) exit(EXIT_FAILURE); // Welcome Screen - frames.emplace_back(new WelcomeFrame(hInstance, hwnd_window, 170, 0, 630, 450)); - frameOperations.emplace_back([&]() { + Frames.emplace_back(new WelcomeFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + FrameOperations.emplace_back([&]() { ShowWindow(hwnd_exitButton, true); ShowWindow(hwnd_prevButton, false); ShowWindow(hwnd_nextButton, true); - frames[FRAME_INDEX]->setVisible(true); + Frames[FrameIndex]->setVisible(true); }); // Directory Screen - frames.emplace_back(new DirectoryFrame(&directory, hInstance, hwnd_window, 170, 0, 630, 450)); - frameOperations.emplace_back([&]() { + Frames.emplace_back(new DirectoryFrame(&writeDirectory, hInstance, hwnd_window, 170, 0, 630, 450)); + FrameOperations.emplace_back([&]() { ShowWindow(hwnd_exitButton, true); ShowWindow(hwnd_prevButton, true); ShowWindow(hwnd_nextButton, true); - frames[FRAME_INDEX]->setVisible(true); + Frames[FrameIndex]->setVisible(true); }); // Installation Screen - frames.emplace_back(new InstallFrame(hInstance, hwnd_window, 170, 0, 630, 450)); - frameOperations.emplace_back([&]() { + Frames.emplace_back(new InstallFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + FrameOperations.emplace_back([&]() { EnableWindow(hwnd_exitButton, false); EnableWindow(hwnd_prevButton, false); EnableWindow(hwnd_nextButton, false); - frames[FRAME_INDEX]->setVisible(true); + Frames[FrameIndex]->setVisible(true); // Acquire archive resource - threader.addJob([]() { - const auto start = std::chrono::system_clock::now(); - size_t fileCount(0ull), byteCount(0ull); - Resource archive(IDR_ARCHIVE, "ARCHIVE"); - if (!archive.exists()) { - //exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); + threader.addJob([&writeDirectory, &archive, &archiveOffset]() { + // Unpackage using the rest of the resource file + size_t byteCount(0ull), fileCount(0ull); + sanitize_path(writeDirectory); + if (!DRT::DecompressDirectory(writeDirectory, reinterpret_cast(PTR_ADD(archive.getPtr(), archiveOffset)), archive.getSize() - archiveOffset, byteCount, fileCount)) { + // exit_program("Cannot decompress embedded package resource, aborting...\r\n"); } - - // Unpackage using the resource file - else if (!DRT::DecompressDirectory(directory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) { - // exit_program("Cannot decompress embedded package resource, aborting...\r\n"); - } - EnableWindow(hwnd_nextButton, true); }); }); // Finish Screen - frames.emplace_back(new FinishFrame(hInstance, hwnd_window, 170, 0, 630, 450)); - frameOperations.emplace_back([&]() { + Frames.emplace_back(new FinishFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + FrameOperations.emplace_back([&]() { EnableWindow(hwnd_exitButton, true); ShowWindow(hwnd_exitButton, true); ShowWindow(hwnd_prevButton, false); ShowWindow(hwnd_nextButton, false); - frames[FRAME_INDEX]->setVisible(true); + Frames[FrameIndex]->setVisible(true); SetWindowText(hwnd_exitButton, "Close"); }); // Begin first frame - frameOperations[FRAME_INDEX](); + FrameOperations[FrameIndex](); // Main message loop: MSG msg; @@ -186,6 +111,48 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In return (int)msg.wParam; } +static bool CreateMainWindow(HINSTANCE hInstance, const std::string & windowName) +{ + // Try to create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = "nStaller"; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + if (!RegisterClassEx(&wcex)) + return false; + + // Try to create window object + hwnd_window = CreateWindow( + "nStaller", std::string(windowName + " - installer").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + hwnd_prevButton = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + hwnd_nextButton = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + hwnd_exitButton = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); + + auto dwStyle = GetWindowLongPtr(hwnd_window, GWL_STYLE); + auto dwExStyle = GetWindowLongPtr(hwnd_window, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(hwnd_window, true); + UpdateWindow(hwnd_window); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(hwnd_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + return true; +} + LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_PAINT) { @@ -215,7 +182,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) int vertical_offset = 15; for (int x = 0; x < 4; ++x) { // Draw Circle - auto color = x == FRAME_INDEX ? RGB(25, 225, 125) : x < FRAME_INDEX ? RGB(25, 125, 225) : RGB(255, 255, 255); + auto color = x == FrameIndex ? RGB(25, 225, 125) : x < FrameIndex ? RGB(25, 125, 225) : RGB(255, 255, 255); SetDCBrushColor(hdc, color); SetDCPenColor(hdc, color); SetTextColor(hdc, color); @@ -238,16 +205,16 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) if (HIWORD(wParam) == BN_CLICKED) { auto hndl = LOWORD(lParam); // A button has been clicked, so SOMETHING drastic is going to happen - frames[FRAME_INDEX]->setVisible(false); + Frames[FrameIndex]->setVisible(false); // If exit if (hndl == LOWORD(hwnd_exitButton)) PostQuitMessage(0); // If previous else if (hndl == LOWORD(hwnd_prevButton)) - frameOperations[--FRAME_INDEX](); + FrameOperations[--FrameIndex](); // If next else if (hndl == LOWORD(hwnd_nextButton)) - frameOperations[++FRAME_INDEX](); + FrameOperations[++FrameIndex](); RECT rc = { 0, 0, 160, 450 }; RedrawWindow(hwnd_window, &rc, NULL, RDW_INVALIDATE); diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index f70bd53..ff8ee03 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -46,13 +46,22 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const packFile.read(packBuffer, std::streamsize(packSize)); packFile.close(); + // Read the package header + char * packBufferOffset = packBuffer; + const auto folderSize = *reinterpret_cast(packBufferOffset); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + const char * folderArray = reinterpret_cast(packBufferOffset); + const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + // Unpackage using the resource file size_t fileCount(0ull), byteCount(0ull); - if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, byteCount, fileCount)) + if (!DRT::DecompressDirectory(finalDestionation, packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), byteCount, fileCount)) exit_program("Cannot decompress package file, aborting...\r\n"); + delete[] packBuffer; // Output results logger - << "Files written: " << std::to_string(fileCount) << "\r\n" - << "Bytes written: " << std::to_string(byteCount) << "\r\n"; + << "Files written: " << std::to_string(fileCount) << "\r\n" + << "Bytes processed: " << std::to_string(byteCount) << "\r\n"; } \ No newline at end of file From e1962b34a85486ce59f924d4f424422f7d569127 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Thu, 4 Apr 2019 14:21:25 -0500 Subject: [PATCH 03/44] Added UAC control and open dir checkbox Added UAC control so that it can be run as an administrator, in case we're installing to a protected folder like \\Program files\\ Added a checkbox, which doesn't do anything yet. Later, I'm going to probably try to implement some sort of state design pattern to manage all these buttons. --- src/nStaller/CMakeLists.txt | 1 + src/nStaller/Frames/FinishFrame.cpp | 16 +++++++++++++++- src/nStaller/Frames/FinishFrame.h | 5 ++++- src/nStaller/nStaller.cpp | 9 +++++---- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/nStaller/CMakeLists.txt b/src/nStaller/CMakeLists.txt index e7a60b4..1a6440e 100644 --- a/src/nStaller/CMakeLists.txt +++ b/src/nStaller/CMakeLists.txt @@ -43,6 +43,7 @@ set_target_properties(${Module} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" + LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\"" ) # Force highest c++ version supported diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index da60dc4..3db85ba 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -1,5 +1,7 @@ #include "FinishFrame.h" #include +#include +#include constexpr static auto CLASS_NAME = "FINISH_FRAME"; @@ -12,9 +14,10 @@ FinishFrame::~FinishFrame() DeleteObject(m_hwnd); } -FinishFrame::FinishFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) { // Try to create window class + m_openDirOnClose = openDirOnClose; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -31,11 +34,16 @@ FinishFrame::FinishFrame(const HINSTANCE & hInstance, const HWND & parent, const RegisterClassEx(&m_wcex); m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + auto checkbox = CreateWindow("Button", "Open installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, w-20, 25, m_hwnd, (HMENU)1, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(checkbox, GWLP_USERDATA, (LONG_PTR)this); + CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); setVisible(false); } static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (FinishFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; auto hdc = BeginPaint(hWnd, &ps); @@ -62,5 +70,11 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) EndPaint(hWnd, &ps); return 0; } + else if (message == WM_COMMAND) { + if (HIWORD(wParam) == BN_CLICKED) { + BOOL checked = IsDlgButtonChecked(hWnd, 1); + *(ptr->m_openDirOnClose) = checked; + } + } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h index 646cc1d..b0ae894 100644 --- a/src/nStaller/Frames/FinishFrame.h +++ b/src/nStaller/Frames/FinishFrame.h @@ -10,7 +10,10 @@ class FinishFrame : public Frame { public: // Public (de)Constructors ~FinishFrame(); - FinishFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + + + bool * m_openDirOnClose = nullptr; }; #endif // FINISHFRAME_H \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index bcb87ce..0f80a23 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -32,6 +32,7 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In TCHAR pf[MAX_PATH]; SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); std::string writeDirectory(pf), directoryName = ""; + bool openDirectoryOnClose = true; // Get installer's payload Resource archive(IDR_ARCHIVE, "ARCHIVE"); @@ -76,20 +77,20 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In EnableWindow(hwnd_nextButton, false); Frames[FrameIndex]->setVisible(true); - // Acquire archive resource + threader.addJob([&writeDirectory, &archive, &archiveOffset]() { - // Unpackage using the rest of the resource file + /*// Unpackage using the rest of the resource file size_t byteCount(0ull), fileCount(0ull); sanitize_path(writeDirectory); if (!DRT::DecompressDirectory(writeDirectory, reinterpret_cast(PTR_ADD(archive.getPtr(), archiveOffset)), archive.getSize() - archiveOffset, byteCount, fileCount)) { // exit_program("Cannot decompress embedded package resource, aborting...\r\n"); - } + }*/ EnableWindow(hwnd_nextButton, true); }); }); // Finish Screen - Frames.emplace_back(new FinishFrame(hInstance, hwnd_window, 170, 0, 630, 450)); + Frames.emplace_back(new FinishFrame(&openDirectoryOnClose, hInstance, hwnd_window, 170, 0, 630, 450)); FrameOperations.emplace_back([&]() { EnableWindow(hwnd_exitButton, true); ShowWindow(hwnd_exitButton, true); From 337915bb6de60cb70d8211dd2e398f7eeef6b6e6 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 5 Apr 2019 00:18:04 -0500 Subject: [PATCH 04/44] Cleaned up code, introduced state design pattern States are now used installer operations condenced into a proper class failure state introduced more safety checks introduced Still needs a bit more work, as well as more comments --- src/BufferTools.cpp | 4 + src/DirectoryTools.cpp | 5 + src/Instructions.cpp | 4 +- src/TaskLogger.h | 3 + src/nStaller/Frames/DirectoryFrame.cpp | 29 ++-- src/nStaller/Frames/DirectoryFrame.h | 2 +- src/nStaller/Frames/FailFrame.cpp | 78 +++++++++ src/nStaller/Frames/FailFrame.h | 22 +++ src/nStaller/Frames/FinishFrame.cpp | 21 ++- src/nStaller/Frames/FinishFrame.h | 3 +- src/nStaller/Frames/InstallFrame.cpp | 28 ++-- src/nStaller/Frames/InstallFrame.h | 2 +- src/nStaller/Frames/WelcomeFrame.cpp | 22 ++- src/nStaller/Frames/WelcomeFrame.h | 2 +- src/nStaller/Installer.cpp | 208 ++++++++++++++++++++++++ src/nStaller/Installer.h | 58 +++++++ src/nStaller/States/DirectoryState.cpp | 32 ++++ src/nStaller/States/DirectoryState.h | 23 +++ src/nStaller/States/FailState.cpp | 29 ++++ src/nStaller/States/FailState.h | 23 +++ src/nStaller/States/FinishState.cpp | 29 ++++ src/nStaller/States/FinishState.h | 23 +++ src/nStaller/States/InstallState.cpp | 46 ++++++ src/nStaller/States/InstallState.h | 29 ++++ src/nStaller/States/State.h | 26 +++ src/nStaller/States/WelcomeState.cpp | 30 ++++ src/nStaller/States/WelcomeState.h | 23 +++ src/nStaller/nStaller.cpp | 217 +------------------------ 28 files changed, 749 insertions(+), 272 deletions(-) create mode 100644 src/nStaller/Frames/FailFrame.cpp create mode 100644 src/nStaller/Frames/FailFrame.h create mode 100644 src/nStaller/Installer.cpp create mode 100644 src/nStaller/Installer.h create mode 100644 src/nStaller/States/DirectoryState.cpp create mode 100644 src/nStaller/States/DirectoryState.h create mode 100644 src/nStaller/States/FailState.cpp create mode 100644 src/nStaller/States/FailState.h create mode 100644 src/nStaller/States/FinishState.cpp create mode 100644 src/nStaller/States/FinishState.h create mode 100644 src/nStaller/States/InstallState.cpp create mode 100644 src/nStaller/States/InstallState.h create mode 100644 src/nStaller/States/State.h create mode 100644 src/nStaller/States/WelcomeState.cpp create mode 100644 src/nStaller/States/WelcomeState.h diff --git a/src/BufferTools.cpp b/src/BufferTools.cpp index 3092e0b..97d9ab3 100644 --- a/src/BufferTools.cpp +++ b/src/BufferTools.cpp @@ -35,6 +35,10 @@ bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** bool BFT::DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize) { + // Ensure buffer at least *exists* + if (sourceSize <= size_t(sizeof(size_t)) || sourceBuffer == nullptr) + return false; + destinationSize = *reinterpret_cast(sourceBuffer); *destinationBuffer = new char[destinationSize]; auto result = LZ4_decompress_safe( diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 93d2fd9..279e6b4 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -110,6 +110,11 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe { Threader threader; auto & log = TaskLogger::GetInstance(); + if (packSize <= 0ull) { + log << "Critical failure: package buffer has no content.\r\n"; + return false; + } + char * decompressedBuffer(nullptr); size_t decompressedSize(0ull); if (!BFT::DecompressBuffer(packBuffer, packSize, &decompressedBuffer, decompressedSize)) { diff --git a/src/Instructions.cpp b/src/Instructions.cpp index c3666b6..b92da36 100644 --- a/src/Instructions.cpp +++ b/src/Instructions.cpp @@ -44,7 +44,7 @@ size_t Insert_Instruction::SIZE() const { return sizeof(char) + (sizeof(size_t) * 2) + (sizeof(char) * newData.size()); } -void Insert_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const bufferOld, const size_t & oldSize) const { +void Insert_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const, const size_t &) const { for (auto i = index, x = size_t(0ull), length = newData.size(); i < newSize && x < length; ++i, ++x) bufferNew[i] = newData[x]; } @@ -89,7 +89,7 @@ size_t Repeat_Instruction::SIZE() const { return sizeof(char) + (sizeof(size_t) * 2ull) + sizeof(char); } -void Repeat_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const bufferOld, const size_t & oldSize) const { +void Repeat_Instruction::DO(char * bufferNew, const size_t & newSize, const char * const, const size_t &) const { for (auto i = index, x = size_t(0ull); i < newSize && x < amount; ++i, ++x) bufferNew[i] = value; } diff --git a/src/TaskLogger.h b/src/TaskLogger.h index 69c324c..5405671 100644 --- a/src/TaskLogger.h +++ b/src/TaskLogger.h @@ -45,6 +45,9 @@ class TaskLogger { // log << value << value << value return *this; } + inline std::string getLog() const { + return m_log; + } inline void setRange(const size_t & value) { m_range = value; } diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index 614d3c1..0cb2495 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -1,22 +1,22 @@ #include "DirectoryFrame.h" #include -#include #include #include #include constexpr static auto CLASS_NAME = "DIRECTORY_FRAME"; -constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); DirectoryFrame::~DirectoryFrame() { UnregisterClass(CLASS_NAME, m_hinstance); - DeleteObject(m_hwnd); + DestroyWindow(m_hwnd); + DestroyWindow(m_directoryField); + DestroyWindow(m_browseButton); } -DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) { // Try to create window class m_directory = directory; @@ -34,13 +34,12 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInsta m_wcex.lpszClassName = CLASS_NAME; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create extra fields m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - SetWindowLongPtr(m_directoryField, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); } @@ -91,12 +90,12 @@ static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; }; IFACEMETHODIMP OnSelectionChange(IFileDialog *) { return S_OK; }; IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; - IFACEMETHODIMP OnTypeChange(IFileDialog *pfd) { return S_OK; }; + IFACEMETHODIMP OnTypeChange(IFileDialog *) { return S_OK; }; IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; // IFileDialogControlEvents methods - IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *pfdc, DWORD dwIDCtl, DWORD dwIDItem) { return S_OK; }; + IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *, DWORD, DWORD ) { return S_OK; }; IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize *, DWORD, BOOL) { return S_OK; }; IFACEMETHODIMP OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; @@ -112,7 +111,7 @@ static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) return hr; } -static HRESULT OpenFileDialog(std::string & directory) +HRESULT OpenFileDialog(std::string & directory) { // CoCreate the File Open Dialog object. IFileDialog *pfd = NULL; @@ -159,7 +158,7 @@ static HRESULT OpenFileDialog(std::string & directory) return hr; } -static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { @@ -177,13 +176,13 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) }; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); SelectObject(hdc, reg_font); SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 100, text[1], _tcslen(text[1])); - TextOut(hdc, 10, 115, text[2], _tcslen(text[2])); - TextOut(hdc, 10, 420, text[3], _tcslen(text[3])); + TextOut(hdc, 10, 100, text[1], (int)strlen(text[1])); + TextOut(hdc, 10, 115, text[2], (int)strlen(text[2])); + TextOut(hdc, 10, 420, text[3], (int)strlen(text[3])); // Cleanup DeleteObject(big_font); diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index bb0d012..3253c1e 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -11,7 +11,7 @@ class DirectoryFrame : public Frame { public: // Public (de)Constructors ~DirectoryFrame(); - DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); // Public Methods diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp new file mode 100644 index 0000000..0e9ac27 --- /dev/null +++ b/src/nStaller/Frames/FailFrame.cpp @@ -0,0 +1,78 @@ +#include "FailFrame.h" +#include "TaskLogger.h" +#include +#include + + +constexpr static auto CLASS_NAME = "FAIL_FRAME"; +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FailFrame::~FailFrame() +{ + UnregisterClass(CLASS_NAME, m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + TaskLogger::GetInstance().removeCallback_Text(m_logIndex); +} + +FailFrame::FailFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) +{ + // Try to create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = CLASS_NAME; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + + // Failure log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); + auto & logger = TaskLogger::GetInstance(); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)logger.getLog().c_str()); + m_logIndex = logger.addCallback_Text([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + + setVisible(false); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + + // Draw Text + constexpr static char* text[] = { + "Installation incomplete, see log below:", + "Press the 'Close' button to finish . . ." + }; + SelectObject(hdc, big_font); + SetTextColor(hdc, RGB(25, 125, 225)); + TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); + + SelectObject(hdc, reg_font); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, 10, 420, text[1], (int)strlen(text[1])); + + // Cleanup + DeleteObject(big_font); + DeleteObject(reg_font); + + EndPaint(hWnd, &ps); + return 0; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/FailFrame.h b/src/nStaller/Frames/FailFrame.h new file mode 100644 index 0000000..c6be1aa --- /dev/null +++ b/src/nStaller/Frames/FailFrame.h @@ -0,0 +1,22 @@ +#pragma once +#ifndef FAILFRAME_H +#define FAILFRAME_H + +#include "Frame.h" + + +/** Custom frame class, representing the installer 'failure' screen. */ +class FailFrame : public Frame { +public: + // Public (de)Constructors + ~FailFrame(); + FailFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); + + +private: + // Private Attributes + HWND m_hwndLog = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAILFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index 3db85ba..2b65a65 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -1,20 +1,19 @@ #include "FinishFrame.h" -#include #include #include constexpr static auto CLASS_NAME = "FINISH_FRAME"; -constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); FinishFrame::~FinishFrame() { UnregisterClass(CLASS_NAME, m_hinstance); - DeleteObject(m_hwnd); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkbox); } -FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) { // Try to create window class m_openDirOnClose = openDirOnClose; @@ -33,15 +32,15 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, con m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); - auto checkbox = CreateWindow("Button", "Open installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, w-20, 25, m_hwnd, (HMENU)1, hInstance, NULL); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_checkbox = CreateWindow("Button", "Open installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - SetWindowLongPtr(checkbox, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)this); CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); setVisible(false); } -static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { auto ptr = (FinishFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { @@ -57,11 +56,11 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) }; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); SelectObject(hdc, reg_font); SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 420, text[1], _tcslen(text[1])); + TextOut(hdc, 10, 420, text[1], (int)strlen(text[1])); // Cleanup DeleteObject(big_font); diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h index b0ae894..065efb7 100644 --- a/src/nStaller/Frames/FinishFrame.h +++ b/src/nStaller/Frames/FinishFrame.h @@ -10,10 +10,11 @@ class FinishFrame : public Frame { public: // Public (de)Constructors ~FinishFrame(); - FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); bool * m_openDirOnClose = nullptr; + HWND m_checkbox = nullptr; }; #endif // FINISHFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index 58f642d..165f60e 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -1,23 +1,23 @@ #include "InstallFrame.h" #include "TaskLogger.h" #include -#include constexpr static auto CLASS_NAME = "INSTALL_FRAME"; -constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); -constexpr static auto FOREGROUND_COLOR2 = RGB(255, 255, 255); -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); InstallFrame::~InstallFrame() { UnregisterClass(CLASS_NAME, m_hinstance); - DeleteObject(m_hwnd); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + DestroyWindow(m_hwndPrgsText); TaskLogger::GetInstance().removeCallback_Text(m_logIndex); TaskLogger::GetInstance().removeCallback_Progress(m_taskIndex); } -InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) { // Try to create window class m_hinstance = hInstance; @@ -34,14 +34,15 @@ InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, con m_wcex.lpszClassName = CLASS_NAME; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create log box - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, w-20, h-100, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, h - 40, w - 20, 25, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsText = CreateWindowEx(WS_EX_TRANSPARENT, "static", "0%", WS_CHILD | WS_VISIBLE | SS_CENTER, (w / 2)-20, 0, 40, 25, m_hwndPrgsBar, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left)-20, (rc.bottom - rc.top)-100, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 20, 25, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsText = CreateWindowEx(WS_EX_TRANSPARENT, "static", "0%", WS_CHILD | WS_VISIBLE | SS_CENTER, ((rc.right - rc.left) / 2)-20, 0, 40, 25, m_hwndPrgsBar, NULL, hInstance, NULL); auto & logger = TaskLogger::GetInstance(); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)logger.getLog().c_str()); m_logIndex = logger.addCallback_Text([&](const std::string & message) { SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); }); @@ -56,9 +57,8 @@ InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, con setVisible(false); } -static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; auto hdc = BeginPaint(hWnd, &ps); @@ -69,7 +69,7 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) constexpr static auto text = "Installing"; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text, _tcslen(text)); + TextOut(hdc, 10, 10, text, (int)strlen(text)); // Cleanup DeleteObject(big_font); diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h index 8f7d1b3..3660a26 100644 --- a/src/nStaller/Frames/InstallFrame.h +++ b/src/nStaller/Frames/InstallFrame.h @@ -11,7 +11,7 @@ class InstallFrame : public Frame { public: // Public (de)Constructors ~InstallFrame(); - InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); private: diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index 90447cf..bdcae2c 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -1,18 +1,16 @@ #include "WelcomeFrame.h" -#include constexpr static auto CLASS_NAME = "WELCOME_FRAME"; -constexpr static auto FOREGROUND_COLOR = RGB(230, 230, 230); -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); WelcomeFrame::~WelcomeFrame() { UnregisterClass(CLASS_NAME, m_hinstance); - DeleteObject(m_hwnd); + DestroyWindow(m_hwnd); } -WelcomeFrame::WelcomeFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h) +WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND & parent, const RECT & rc) { // Try to create window class m_hinstance = hInstance; @@ -29,11 +27,11 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE & hInstance, const HWND & parent, con m_wcex.lpszClassName = CLASS_NAME; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, x, y, w, h, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); setVisible(false); } -static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_PAINT) { PAINTSTRUCT ps; @@ -52,20 +50,20 @@ static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) }; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], _tcslen(text[0])); + TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); SelectObject(hdc, reg_font); SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 100, text[1], _tcslen(text[1])); - TextOut(hdc, 10, 115, text[2], _tcslen(text[2])); + TextOut(hdc, 10, 100, text[1], (int)strlen(text[1])); + TextOut(hdc, 10, 115, text[2], (int)strlen(text[2])); SelectObject(hdc, reg_font_under); SetTextColor(hdc, RGB(0, 0, 238)); - TextOut(hdc, 35, 130, text[3], _tcslen(text[3])); + TextOut(hdc, 35, 130, text[3], (int)strlen(text[3])); SelectObject(hdc, reg_font); SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 420, text[4], _tcslen(text[4])); + TextOut(hdc, 10, 420, text[4], (int)strlen(text[4])); // Cleanup DeleteObject(big_font); diff --git a/src/nStaller/Frames/WelcomeFrame.h b/src/nStaller/Frames/WelcomeFrame.h index ba0c184..b0d8a3b 100644 --- a/src/nStaller/Frames/WelcomeFrame.h +++ b/src/nStaller/Frames/WelcomeFrame.h @@ -10,7 +10,7 @@ class WelcomeFrame : public Frame { public: // Public (de)Constructors ~WelcomeFrame(); - WelcomeFrame(const HINSTANCE & hInstance, const HWND & parent, const int & x, const int & y, const int & w, const int & h); + WelcomeFrame(const HINSTANCE hInstance, const HWND & parent, const RECT & rc); }; #endif // WELCOMEFRAME_H \ No newline at end of file diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp new file mode 100644 index 0000000..6367c4e --- /dev/null +++ b/src/nStaller/Installer.cpp @@ -0,0 +1,208 @@ +#include "Installer.h" +#include "Common.h" +#include "TaskLogger.h" +#include + +// Starting State +#include "States/WelcomeState.h" +#include "States/FailState.h" + +// Frames used in this GUI application +#include "Frames/WelcomeFrame.h" +#include "Frames/DirectoryFrame.h" +#include "Frames/InstallFrame.h" +#include "Frames/FinishFrame.h" +#include "Frames/FailFrame.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Installer::Installer(const HINSTANCE hInstance) + : m_archive(IDR_ARCHIVE, "ARCHIVE") +{ + // Get user's program files directory + TCHAR pf[MAX_PATH]; + SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); + m_directory = std::string(pf); + + // Check archive integrity + State * startingState = nullptr; + if (!m_archive.exists()) { + TaskLogger::GetInstance() << "Critical Failure: archive doesn't exist!\r\n"; + startingState = new FailState(this); + } + else { + const auto folderSize = *reinterpret_cast(m_archive.getPtr()); + m_packageName = std::string(reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)))), folderSize); + m_directory += "\\" + m_packageName; + m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); + m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); + startingState = new WelcomeState(this); + } + + // Try to create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = "nStaller"; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&wcex); + + // Try to create window object + m_window = CreateWindow( + "nStaller", std::string(m_packageName + " - installer").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); + + auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(m_window, true); + UpdateWindow(m_window); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + + // The portions of the screen that change based on input + m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); + m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, hInstance, m_window, { 170,0,800,450 }); + m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); + m_frames[FINISH_FRAME] = new FinishFrame(&m_openDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); + m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); + setState(startingState); +} + +void Installer::showFrame(const FrameEnums & newIndex) +{ + m_frames[m_currentIndex]->setVisible(false); + m_frames[newIndex]->setVisible(true); + m_currentIndex = newIndex; +} + +void Installer::setState(State * state) +{ + m_state = state; + m_state->enact(); +} + +int Installer::getCurrentIndex() const +{ + return (int)m_currentIndex; +} + +std::string Installer::getDirectory() const +{ + return m_directory; +} + +char * Installer::getPackagePointer() const +{ + return m_packagePtr; +} + +size_t Installer::getPackageSize() const +{ + return m_packageSize; +} + +void Installer::updateButtons(const WORD btnHandle) +{ + if (btnHandle == LOWORD(m_prevBtn)) + m_state->pressPrevious(); + else if (btnHandle == LOWORD(m_nextBtn)) + m_state->pressNext(); + else if (btnHandle == LOWORD(m_exitBtn)) + m_state->pressClose(); + RECT rc = { 0, 0, 160, 450 }; + RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); +} + +void Installer::showButtons(const bool & prev, const bool & next, const bool & close) +{ + ShowWindow(m_prevBtn, prev); + ShowWindow(m_nextBtn, next); + ShowWindow(m_exitBtn, close); +} + +void Installer::enableButtons(const bool & prev, const bool & next, const bool & close) +{ + EnableWindow(m_prevBtn, prev); + EnableWindow(m_nextBtn, next); + EnableWindow(m_exitBtn, close); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (Installer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + auto hdc = BeginPaint(hWnd, &ps); + auto font = CreateFont(25, 10, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + SelectObject(hdc, font); + + // Background + SelectObject(hdc, GetStockObject(DC_BRUSH)); + SelectObject(hdc, GetStockObject(DC_PEN)); + SetDCBrushColor(hdc, RGB(25, 25, 25)); + SetDCPenColor(hdc, RGB(25, 25, 25)); + SetBkColor(hdc, RGB(25, 25, 25)); + Rectangle(hdc, 0, 0, 800, 500); + + // Footer + SetDCBrushColor(hdc, RGB(75, 75, 75)); + SetDCPenColor(hdc, RGB(75, 75, 75)); + Rectangle(hdc, 0, 450, 800, 500); + + // Steps + constexpr static char* step_labels[] = { "Welcome", "Directory", "Install", "Finish" }; + SetDCBrushColor(hdc, RGB(100, 100, 100)); + SetDCPenColor(hdc, RGB(100, 100, 100)); + Rectangle(hdc, 26, 0, 29, 450); + int vertical_offset = 15; + int frameIndex = ptr->getCurrentIndex(); + for (int x = 0; x < 4; ++x) { + // Draw Circle + auto color = x == frameIndex ? RGB(25, 225, 125) : x < frameIndex ? RGB(25, 125, 225) : RGB(255, 255, 255); + if (x == 3 && frameIndex == 4) + color = RGB(225, 25, 25); + SetDCBrushColor(hdc, color); + SetDCPenColor(hdc, color); + SetTextColor(hdc, color); + Ellipse(hdc, 20, vertical_offset + 6, 35, vertical_offset + 21); + + // Draw Text + TextOut(hdc, 50, vertical_offset, step_labels[x], (int)strlen(step_labels[x])); + vertical_offset += 50; + + if (x == 2) + vertical_offset = 420; + } + + DeleteObject(font); + EndPaint(hWnd, &ps); + } + else if (message == WM_DESTROY) + PostQuitMessage(0); + else if (message == WM_COMMAND) { + if (HIWORD(wParam) == BN_CLICKED) { + ptr->updateButtons(LOWORD(lParam)); + return S_OK; + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h new file mode 100644 index 0000000..e3ef5f0 --- /dev/null +++ b/src/nStaller/Installer.h @@ -0,0 +1,58 @@ +#pragma once +#ifndef INSTALLER_H +#define INSTALLER_H + +#include "Resource.h" +#include +#include + + +class Frame; +class State; + +/***/ +class Installer { +public: + // Public (de)Constructors + ~Installer() = default; + Installer(const HINSTANCE hInstance); + + + // Public Enumerations + const enum FrameEnums { + WELCOME_FRAME, DIRECTORY_FRAME, INSTALL_FRAME, FINISH_FRAME, FAIL_FRAME, + FRAME_COUNT + }; + + + // Public Methods + void showFrame(const FrameEnums & newIndex); + void setState(State * state); + int getCurrentIndex() const; + std::string getDirectory() const; + char * getPackagePointer() const; + size_t getPackageSize() const; + void updateButtons(const WORD btnHandle); + void showButtons(const bool & prev, const bool & next, const bool & close); + void enableButtons(const bool & prev, const bool & next, const bool & close); + + +private: + // Private Attributes + Resource m_archive; + std::string m_directory = "", m_packageName = ""; + bool m_openDirectoryOnClose = true; + char * m_packagePtr = nullptr; + size_t m_packageSize = 0ull; + FrameEnums m_currentIndex = WELCOME_FRAME; + Frame * m_frames[FRAME_COUNT]; + State * m_state = nullptr; + HWND + m_window = nullptr, + m_prevBtn = nullptr, + m_nextBtn = nullptr, + m_exitBtn = nullptr; +}; + + +#endif // INSTALLER_H \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp new file mode 100644 index 0000000..e2ddb9c --- /dev/null +++ b/src/nStaller/States/DirectoryState.cpp @@ -0,0 +1,32 @@ +#include "DirectoryState.h" +#include "WelcomeState.h" +#include "InstallState.h" +#include "../Installer.h" + + +DirectoryState::DirectoryState(Installer * installer) + : State(installer) {} + +void DirectoryState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::DIRECTORY_FRAME); + m_installer->showButtons(true, true, true); +} + +void DirectoryState::pressPrevious() +{ + m_installer->setState(new WelcomeState(m_installer)); + delete this; +} + +void DirectoryState::pressNext() +{ + m_installer->setState(new InstallState(m_installer)); + delete this; +} + +void DirectoryState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.h b/src/nStaller/States/DirectoryState.h new file mode 100644 index 0000000..cb674f8 --- /dev/null +++ b/src/nStaller/States/DirectoryState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef DIRECTORYSTATE_H +#define DIRECTORYSTATE_H + +#include "State.h" + + +/***/ +class DirectoryState : public State { +public: + // Public (de)Constructors + ~DirectoryState() = default; + DirectoryState(Installer * installer); + + + // Public Methods + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // DIRECTORYSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp new file mode 100644 index 0000000..e185213 --- /dev/null +++ b/src/nStaller/States/FailState.cpp @@ -0,0 +1,29 @@ +#include "FailState.h" +#include "../Installer.h" + + +FailState::FailState(Installer * installer) + : State(installer) {} + +void FailState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::FAIL_FRAME); + m_installer->showButtons(false, false, true); + m_installer->enableButtons(false, false, true); +} + +void FailState::pressPrevious() +{ + // Should never happen +} + +void FailState::pressNext() +{ + // Should never happen +} + +void FailState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} \ No newline at end of file diff --git a/src/nStaller/States/FailState.h b/src/nStaller/States/FailState.h new file mode 100644 index 0000000..3f35fbc --- /dev/null +++ b/src/nStaller/States/FailState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef FAILSTATE_H +#define FAILSTATE_H + +#include "State.h" + + +/***/ +class FailState: public State { +public: + // Public (de)Constructors + ~FailState() = default; + FailState(Installer * installer); + + + // Public Methods + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp new file mode 100644 index 0000000..b82a91e --- /dev/null +++ b/src/nStaller/States/FinishState.cpp @@ -0,0 +1,29 @@ +#include "FinishState.h" +#include "../Installer.h" + + +FinishState::FinishState(Installer * installer) + : State(installer) {} + +void FinishState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::FINISH_FRAME); + m_installer->showButtons(false, false, true); + m_installer->enableButtons(false, false, true); +} + +void FinishState::pressPrevious() +{ + // Should never happen +} + +void FinishState::pressNext() +{ + // Should never happen +} + +void FinishState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} \ No newline at end of file diff --git a/src/nStaller/States/FinishState.h b/src/nStaller/States/FinishState.h new file mode 100644 index 0000000..901b344 --- /dev/null +++ b/src/nStaller/States/FinishState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef FINISHSTATE_H +#define FINISHSTATE_H + +#include "State.h" + + +/***/ +class FinishState: public State { +public: + // Public (de)Constructors + ~FinishState() = default; + FinishState(Installer * installer); + + + // Public Methods + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp new file mode 100644 index 0000000..cd69414 --- /dev/null +++ b/src/nStaller/States/InstallState.cpp @@ -0,0 +1,46 @@ +#include "InstallState.h" +#include "FinishState.h" +#include "FailState.h" +#include "Common.h" +#include "BufferTools.h" +#include "DirectoryTools.h" +#include "../Installer.h" + + +InstallState::InstallState(Installer * installer) + : State(installer) {} + +void InstallState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::INSTALL_FRAME); + m_installer->showButtons(true, true, true); + m_installer->enableButtons(false, false, false); + + m_thread = std::thread([&]() { + // Unpackage using the rest of the resource file + size_t byteCount(0ull), fileCount(0ull); + auto directory = m_installer->getDirectory(); + sanitize_path(directory); + if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getPackageSize(), byteCount, fileCount)) + m_installer->setState(new FailState(m_installer)); + else + m_installer->enableButtons(false, true, false); + }); + m_thread.detach(); +} + +void InstallState::pressPrevious() +{ + // Should never happen +} + +void InstallState::pressNext() +{ + m_installer->setState(new FinishState(m_installer)); + delete this; +} + +void InstallState::pressClose() +{ + // Should never happen +} \ No newline at end of file diff --git a/src/nStaller/States/InstallState.h b/src/nStaller/States/InstallState.h new file mode 100644 index 0000000..d2d368b --- /dev/null +++ b/src/nStaller/States/InstallState.h @@ -0,0 +1,29 @@ +#pragma once +#ifndef INSTALLSTATE_H +#define INSTALLSTATE_H + +#include "State.h" +#include + + +/***/ +class InstallState : public State { +public: + // Public (de)Constructors + ~InstallState() = default; + InstallState(Installer * installer); + + + // Public Methods + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); + + +private: + // Private Attributes + std::thread m_thread; +}; + +#endif // INSTALLSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/State.h b/src/nStaller/States/State.h new file mode 100644 index 0000000..6e5497d --- /dev/null +++ b/src/nStaller/States/State.h @@ -0,0 +1,26 @@ +#pragma once +#ifndef STATE_H +#define STATE_H + + +class Installer; + +/***/ +class State { +public: + // Public (de)Constructors + State(Installer * installer) : m_installer(installer) {} + + + // Public Methods + virtual void enact() = 0; + virtual void pressPrevious() = 0; + virtual void pressNext() = 0; + virtual void pressClose() = 0; + + + // Public Attributes + Installer * m_installer = nullptr; +}; + +#endif // STATE_H \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.cpp b/src/nStaller/States/WelcomeState.cpp new file mode 100644 index 0000000..e2090db --- /dev/null +++ b/src/nStaller/States/WelcomeState.cpp @@ -0,0 +1,30 @@ +#include "WelcomeState.h" +#include "DirectoryState.h" +#include "../Installer.h" + + +WelcomeState::WelcomeState(Installer * installer) + : State(installer) {} + +void WelcomeState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::WELCOME_FRAME); + m_installer->showButtons(false, true, true); +} + +void WelcomeState::pressPrevious() +{ + // Should never happen +} + +void WelcomeState::pressNext() +{ + m_installer->setState(new DirectoryState(m_installer)); + delete this; +} + +void WelcomeState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.h b/src/nStaller/States/WelcomeState.h new file mode 100644 index 0000000..ba84b3a --- /dev/null +++ b/src/nStaller/States/WelcomeState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef WELCOMESTATE_H +#define WELCOMESTATE_H + +#include "State.h" + + +/***/ +class WelcomeState : public State { +public: + // Public (de)Constructors + ~WelcomeState() = default; + WelcomeState(Installer * installer); + + + // Public Methods + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // WELCOMESTATE_H \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 0f80a23..1f5d5ba 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -1,107 +1,9 @@ -#include "Common.h" -#include "BufferTools.h" -#include "DirectoryTools.h" -#include "Threader.h" -#include "Resource.h" -#include "Frames/WelcomeFrame.h" -#include "Frames/DirectoryFrame.h" -#include "Frames/InstallFrame.h" -#include "Frames/FinishFrame.h" -#include -#include -#include -#include -#include -#include -#include -#include +#include "Installer.h" -static Threader threader(1ull); -static std::vector Frames; -static std::vector> FrameOperations; -static int FrameIndex = 0; -static HWND hwnd_window, hwnd_exitButton, hwnd_prevButton, hwnd_nextButton, hwnd_dialogue = NULL; - -LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -static bool CreateMainWindow(HINSTANCE, const std::string &); - -int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) { - // Get user's program files directory - TCHAR pf[MAX_PATH]; - SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); - std::string writeDirectory(pf), directoryName = ""; - bool openDirectoryOnClose = true; - - // Get installer's payload - Resource archive(IDR_ARCHIVE, "ARCHIVE"); - size_t archiveOffset(0ull); - if (!archive.exists()) { - // Show error screen - } - else { - // Read the package header - const auto folderSize = *reinterpret_cast(archive.getPtr()); - directoryName = std::string(reinterpret_cast(PTR_ADD(archive.getPtr(), size_t(sizeof(size_t)))), folderSize); - writeDirectory += "\\" + directoryName; - archiveOffset = size_t(sizeof(size_t)) + folderSize; - } - - if (!CreateMainWindow(hInstance, directoryName)) - exit(EXIT_FAILURE); - - // Welcome Screen - Frames.emplace_back(new WelcomeFrame(hInstance, hwnd_window, 170, 0, 630, 450)); - FrameOperations.emplace_back([&]() { - ShowWindow(hwnd_exitButton, true); - ShowWindow(hwnd_prevButton, false); - ShowWindow(hwnd_nextButton, true); - Frames[FrameIndex]->setVisible(true); - }); - - // Directory Screen - Frames.emplace_back(new DirectoryFrame(&writeDirectory, hInstance, hwnd_window, 170, 0, 630, 450)); - FrameOperations.emplace_back([&]() { - ShowWindow(hwnd_exitButton, true); - ShowWindow(hwnd_prevButton, true); - ShowWindow(hwnd_nextButton, true); - Frames[FrameIndex]->setVisible(true); - }); - - // Installation Screen - Frames.emplace_back(new InstallFrame(hInstance, hwnd_window, 170, 0, 630, 450)); - FrameOperations.emplace_back([&]() { - EnableWindow(hwnd_exitButton, false); - EnableWindow(hwnd_prevButton, false); - EnableWindow(hwnd_nextButton, false); - Frames[FrameIndex]->setVisible(true); - - - threader.addJob([&writeDirectory, &archive, &archiveOffset]() { - /*// Unpackage using the rest of the resource file - size_t byteCount(0ull), fileCount(0ull); - sanitize_path(writeDirectory); - if (!DRT::DecompressDirectory(writeDirectory, reinterpret_cast(PTR_ADD(archive.getPtr(), archiveOffset)), archive.getSize() - archiveOffset, byteCount, fileCount)) { - // exit_program("Cannot decompress embedded package resource, aborting...\r\n"); - }*/ - EnableWindow(hwnd_nextButton, true); - }); - }); - - // Finish Screen - Frames.emplace_back(new FinishFrame(&openDirectoryOnClose, hInstance, hwnd_window, 170, 0, 630, 450)); - FrameOperations.emplace_back([&]() { - EnableWindow(hwnd_exitButton, true); - ShowWindow(hwnd_exitButton, true); - ShowWindow(hwnd_prevButton, false); - ShowWindow(hwnd_nextButton, false); - Frames[FrameIndex]->setVisible(true); - SetWindowText(hwnd_exitButton, "Close"); - }); - - // Begin first frame - FrameOperations[FrameIndex](); + Installer installer(hInstance); // Main message loop: MSG msg; @@ -110,117 +12,4 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In DispatchMessage(&msg); } return (int)msg.wParam; -} - -static bool CreateMainWindow(HINSTANCE hInstance, const std::string & windowName) -{ - // Try to create window class - WNDCLASSEX wcex; - wcex.cbSize = sizeof(WNDCLASSEX); - wcex.style = CS_HREDRAW | CS_VREDRAW; - wcex.lpfnWndProc = WndProc; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = hInstance; - wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wcex.lpszMenuName = NULL; - wcex.lpszClassName = "nStaller"; - wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); - if (!RegisterClassEx(&wcex)) - return false; - - // Try to create window object - hwnd_window = CreateWindow( - "nStaller", std::string(windowName + " - installer").c_str(), - WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, - CW_USEDEFAULT, CW_USEDEFAULT, - 800, 500, - NULL, NULL, hInstance, NULL - ); - constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - hwnd_prevButton = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - hwnd_nextButton = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - hwnd_exitButton = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, hwnd_window, NULL, hInstance, NULL); - - auto dwStyle = GetWindowLongPtr(hwnd_window, GWL_STYLE); - auto dwExStyle = GetWindowLongPtr(hwnd_window, GWL_EXSTYLE); - RECT rc = { 0, 0, 800, 500 }; - ShowWindow(hwnd_window, true); - UpdateWindow(hwnd_window); - AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); - SetWindowPos(hwnd_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); - return true; -} - -LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - if (message == WM_PAINT) { - PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto font = CreateFont(25, 10, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - SelectObject(hdc, font); - - // Background - SelectObject(hdc, GetStockObject(DC_BRUSH)); - SelectObject(hdc, GetStockObject(DC_PEN)); - SetDCBrushColor(hdc, RGB(25,25,25)); - SetDCPenColor(hdc, RGB(25,25,25)); - SetBkColor(hdc, RGB(25,25,25)); - Rectangle(hdc, 0, 0, 800, 500); - - // Footer - SetDCBrushColor(hdc, RGB(75,75,75)); - SetDCPenColor(hdc, RGB(75, 75, 75)); - Rectangle(hdc, 0, 450, 800, 500); - - // Steps - constexpr static char* step_labels[] = { "Welcome", "Directory", "Install", "Finish" }; - SetDCBrushColor(hdc, RGB(100, 100, 100)); - SetDCPenColor(hdc, RGB(100, 100, 100)); - Rectangle(hdc, 26, 0, 29, 450); - int vertical_offset = 15; - for (int x = 0; x < 4; ++x) { - // Draw Circle - auto color = x == FrameIndex ? RGB(25, 225, 125) : x < FrameIndex ? RGB(25, 125, 225) : RGB(255, 255, 255); - SetDCBrushColor(hdc, color); - SetDCPenColor(hdc, color); - SetTextColor(hdc, color); - Ellipse(hdc, 20, vertical_offset + 6, 35, vertical_offset + 21); - - // Draw Text - TextOut(hdc, 50, vertical_offset, step_labels[x], _tcslen(step_labels[x])); - vertical_offset += 50; - - if (x == 2) - vertical_offset = 420; - } - - DeleteObject(font); - EndPaint(hWnd, &ps); - } - else if (message == WM_DESTROY) - PostQuitMessage(0); - else if (message == WM_COMMAND) { - if (HIWORD(wParam) == BN_CLICKED) { - auto hndl = LOWORD(lParam); - // A button has been clicked, so SOMETHING drastic is going to happen - Frames[FrameIndex]->setVisible(false); - // If exit - if (hndl == LOWORD(hwnd_exitButton)) - PostQuitMessage(0); - // If previous - else if (hndl == LOWORD(hwnd_prevButton)) - FrameOperations[--FrameIndex](); - // If next - else if (hndl == LOWORD(hwnd_nextButton)) - FrameOperations[++FrameIndex](); - - RECT rc = { 0, 0, 160, 450 }; - RedrawWindow(hwnd_window, &rc, NULL, RDW_INVALIDATE); - } - } - else - return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file From 5e194d6f5eafb4e835c0845066f95d71fa23117e Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 5 Apr 2019 15:50:01 -0500 Subject: [PATCH 05/44] Updated documentation + bugfixes Updated documentation, comments Fixed a few bugs Renamed a bunch of stuff Better error handling Added error logging to disk when reaching a fail state --- src/BufferTools.h | 5 +- src/Common.h | 5 +- src/DirectoryTools.cpp | 74 +++++++------- src/TaskLogger.h | 109 +++++++++++++------- src/Threader.h | 5 + src/nStaller/Frames/DirectoryFrame.cpp | 45 ++++---- src/nStaller/Frames/DirectoryFrame.h | 7 +- src/nStaller/Frames/FailFrame.cpp | 30 +++--- src/nStaller/Frames/FailFrame.h | 2 +- src/nStaller/Frames/FinishFrame.cpp | 34 ++++--- src/nStaller/Frames/FinishFrame.h | 4 +- src/nStaller/Frames/Frame.h | 2 +- src/nStaller/Frames/InstallFrame.cpp | 25 ++--- src/nStaller/Frames/InstallFrame.h | 2 +- src/nStaller/Frames/WelcomeFrame.cpp | 13 ++- src/nStaller/Frames/WelcomeFrame.h | 2 +- src/nStaller/Installer.cpp | 124 ++++++++++++++--------- src/nStaller/Installer.h | 29 +++++- src/nStaller/States/DirectoryState.cpp | 2 - src/nStaller/States/DirectoryState.h | 4 +- src/nStaller/States/FailState.cpp | 16 +++ src/nStaller/States/FailState.h | 4 +- src/nStaller/States/FinishState.h | 4 +- src/nStaller/States/InstallState.cpp | 4 +- src/nStaller/States/InstallState.h | 4 +- src/nStaller/States/State.h | 6 +- src/nStaller/States/WelcomeState.cpp | 1 - src/nStaller/States/WelcomeState.h | 4 +- src/nSuite/Commands/DiffCommand.cpp | 13 +-- src/nSuite/Commands/InstallerCommand.cpp | 13 +-- src/nSuite/Commands/PackCommand.cpp | 13 +-- src/nSuite/Commands/PatchCommand.cpp | 13 +-- src/nSuite/Commands/UnpackCommand.cpp | 13 +-- src/nSuite/nSuite.cpp | 5 +- src/nUpdater/nUpdater.cpp | 25 ++--- 35 files changed, 385 insertions(+), 276 deletions(-) diff --git a/src/BufferTools.h b/src/BufferTools.h index 94d8e99..cb3b8c5 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -10,6 +10,7 @@ namespace BFT { @param sourceSize the size in bytes of the source buffer. @param destinationBuffer pointer to the destination buffer, which will hold compressed contents. @param destinationSize reference updated with the size in bytes of the compressed destinationBuffer. + @param headerPadding amount of headroom to allocate for a header on top of the final compressed buffer. (optional, defaults to 0) @return true if compression success, false otherwise. */ bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize, const size_t & headerPadding = 0ull); /** Decompressess a source buffer into an equal or larger sized destination buffer. @@ -27,7 +28,7 @@ namespace BFT { @param size_new the size of the new buffer. @param buffer_diff pointer to store the diff buffer at. @param size_diff reference updated with the size of the compressed diff buffer. - @param instructionCount optional pointer to update with the number of instructions processed. + @param instructionCount pointer to update with the number of instructions processed. (optional) @return true if diff success, false otherwise. */ bool DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_new, const size_t & size_new, char ** buffer_diff, size_t & size_diff, size_t * instructionCount = nullptr); /** Uses a compressed diff buffer to patch a source buffer into an updated destination buffer @@ -38,7 +39,7 @@ namespace BFT { @param size_new reference updated with the size of the new buffer. @param buffer_diff the compressed diff buffer. @param size_diff the size of the compressed diff buffer. - @param instructionCount optional pointer to update with the number of instructions processed. + @param instructionCount pointer to update with the number of instructions processed. (optional) @return true if patch success, false otherwise. */ bool PatchBuffer(char * buffer_old, const size_t & size_old, char ** buffer_new, size_t & size_new, char * buffer_diff, const size_t & size_diff, size_t * instructionCount = nullptr); /** Generate a hash value for the buffer provided. diff --git a/src/Common.h b/src/Common.h index 1cb04a3..13eb733 100644 --- a/src/Common.h +++ b/src/Common.h @@ -2,6 +2,7 @@ #ifndef COMMON_H #define COMMON_H +#include "TaskLogger.h" #include #include #include @@ -56,7 +57,7 @@ inline static std::string get_current_directory() @param message the message to write-out.*/ inline static void exit_program(const char * message) { - std::cout << message; + TaskLogger::PushText(message); system("pause"); exit(EXIT_FAILURE); } @@ -65,7 +66,7 @@ inline static void exit_program(const char * message) @param message pause message to show the user. */ inline static void pause_program(const char * message) { - std::cout << message << " "; + TaskLogger::PushText(message + ' '); system("pause"); std::printf("\033[A\33[2K\r"); std::printf("\033[A\33[2K\r\n"); diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 279e6b4..993340c 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -14,7 +14,6 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer { // Variables Threader threader; - auto & logger = TaskLogger::GetInstance(); const auto absolute_path_length = srcDirectory.size(); const auto directoryArray = get_file_paths(srcDirectory); struct FileData { @@ -27,7 +26,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Get path name const auto srcPath = std::filesystem::path(srcDirectory); if (!std::filesystem::is_directory(srcPath) || !srcPath.has_stem()) { - logger << "Critical failure: the source path specified \"" << srcDirectory << "\" is not a (useable) directory.\r\n"; + TaskLogger::PushText("Critical failure: the source path specified \"" + srcDirectory + "\" is not a (useable) directory.\r\n"); return false; } const auto folderName = srcPath.stem().string(); @@ -89,7 +88,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // Compress the archive, pad the beginning with header-space if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize, sizeof(size_t) + folderName.size())) { - logger << "Critical failure: cannot perform compression operation on the set of joined files.\r\n"; + TaskLogger::PushText("Critical failure: cannot perform compression operation on the set of joined files.\r\n"); return false; } delete[] filebuffer; @@ -109,16 +108,15 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount) { Threader threader; - auto & log = TaskLogger::GetInstance(); if (packSize <= 0ull) { - log << "Critical failure: package buffer has no content.\r\n"; + TaskLogger::PushText("Critical failure: package buffer has no content.\r\n"); return false; } char * decompressedBuffer(nullptr); size_t decompressedSize(0ull); if (!BFT::DecompressBuffer(packBuffer, packSize, &decompressedBuffer, decompressedSize)) { - log << "Critical failure: cannot decompress package file.\r\n"; + TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); return false; } @@ -126,7 +124,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe void * readingPtr = decompressedBuffer; size_t bytesRead(0ull); std::atomic_size_t filesWritten(0ull); - log.setRange(decompressedSize + 100); + TaskLogger::SetRange(decompressedSize + 100); while (bytesRead < decompressedSize) { // Read the total number of characters from the path string, from the archive const auto pathSize = *reinterpret_cast(readingPtr); @@ -134,7 +132,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Read the file path string, from the archive const char * path_array = reinterpret_cast(readingPtr); - const auto path = dstDirectory + std::string(path_array, pathSize); + std::string fullPath = dstDirectory + std::string(path_array, pathSize); readingPtr = PTR_ADD(readingPtr, pathSize); // Read the file size in bytes, from the archive @@ -143,20 +141,23 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Write file out to disk, from the archive void * ptrCopy = readingPtr; // needed for lambda, since readingPtr gets incremented - threader.addJob([ptrCopy, path, fileSize, &filesWritten]() { + threader.addJob([ptrCopy, fullPath, fileSize, &filesWritten]() { // Write-out the file if it doesn't exist yet, if the size is different, or if it's older - std::filesystem::create_directories(std::filesystem::path(path).parent_path()); - std::ofstream file(path, std::ios::binary | std::ios::out); - if (file.is_open()) + std::filesystem::create_directories(std::filesystem::path(fullPath).parent_path()); + std::ofstream file(fullPath, std::ios::binary | std::ios::out); + if (!file.is_open()) + TaskLogger::PushText("Error writing file: \"" + fullPath + "\" to disk"); + else { file.write(reinterpret_cast(ptrCopy), (std::streamsize)fileSize); - file.close(); + file.close(); + } filesWritten++; }); readingPtr = PTR_ADD(readingPtr, fileSize); bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; - log << "Writing file: " + std::string(path_array, pathSize) + "\r\n"; - log.setProgress(bytesRead); + TaskLogger::PushText("Writing file: \"" + std::string(path_array, pathSize) + "\"\r\n"); + TaskLogger::SetProgress(bytesRead); } // Wait for threaded operations to complete @@ -164,7 +165,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe while (!threader.isFinished()) continue; threader.shutdown(); - log.setProgress(bytesRead + 100); + TaskLogger::SetProgress(bytesRead + 100); // Success fileCount = filesWritten; @@ -218,11 +219,10 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne else { // Treat as a snapshot file if it isn't a directory // Open diff file - auto & logger = TaskLogger::GetInstance(); std::ifstream packFile(directory, std::ios::binary | std::ios::beg); const size_t packSize = std::filesystem::file_size(directory); if (!packFile.is_open()) { - logger << "Critical failure: cannot read package file.\r\n"; + TaskLogger::PushText("Critical failure: cannot read package file.\r\n"); return false; } char * compBuffer = new char[packSize]; @@ -232,7 +232,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Decompress size_t snapSize(0ull); if (!BFT::DecompressBuffer(compBuffer, packSize, snapshot, snapSize)) { - logger << "Critical failure: cannot decompress package file.\r\n"; + TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); return false; } delete[] compBuffer; @@ -332,7 +332,6 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne }; // Retrieve all common, added, and removed files - auto & logger = TaskLogger::GetInstance(); PathPairList commonFiles; PathList addedFiles, removedFiles; char * oldSnap(nullptr), * newSnap(nullptr); @@ -354,7 +353,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * buffer(nullptr); size_t size(0ull); if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionCount)) { - logger << "diffing file \"" << cFiles.first->relative << "\"\r\n"; + TaskLogger::PushText("Diffing file: \"" + cFiles.first->relative + "\"\r\n"); writeInstructions(cFiles.first->relative, oldHash, newHash, buffer, size, 'U', vecBuffer); } delete[] buffer; @@ -375,7 +374,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * buffer(nullptr); size_t size(0ull); if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionCount)) { - logger << "adding file \"" << nFile->relative << "\"\r\n"; + TaskLogger::PushText("Adding file: \"" + nFile->relative + "\"\r\n"); writeInstructions(nFile->relative, 0ull, newHash, buffer, size, 'N', vecBuffer); } delete[] buffer; @@ -392,7 +391,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne size_t oldHash(0ull); if (oFile->open(&oldBuffer, oldHash)) { instructionCount++; - logger << "removing file \"" << oFile->relative << "\"\r\n"; + TaskLogger::PushText("Removing file: \"" + oFile->relative + "\"\r\n"); writeInstructions(oFile->relative, oldHash, 0ull, nullptr, 0ull, 'D', vecBuffer); } delete[] oldBuffer; @@ -403,7 +402,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Compress final buffer if (!BFT::CompressBuffer(vecBuffer.data(), vecBuffer.size(), diffBuffer, diffSize)) { - logger << "Critical failure: cannot compress diff file.\r\n"; + TaskLogger::PushText("Critical failure: cannot compress diff file.\r\n"); return false; } return true; @@ -411,11 +410,10 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t & bytesWritten, size_t & instructionsUsed) { - auto & logger = TaskLogger::GetInstance(); char * diffBuffer(nullptr); size_t diffSize(0ull); if (!BFT::DecompressBuffer(diffBufferCompressed, diffSizeCompressed, &diffBuffer, diffSize)) { - logger << "Critical failure: cannot decompress diff file.\r\n"; + TaskLogger::PushText("Critical failure: cannot decompress diff file.\r\n"); return false; } @@ -497,34 +495,34 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) { - logger << "Critical failure: Cannot read source file from disk.\r\n"; + TaskLogger::PushText("Critical failure: Cannot read source file from disk.\r\n"); return false; } // Patch if this source file hasn't been patched yet if (oldHash == file.diff_newHash) - logger << "The file \"" << file.path << "\" is already up to date, skipping...\r\n"; + TaskLogger::PushText("The file \"" + file.path + "\" is already up to date, skipping...\r\n"); else if (oldHash != file.diff_oldHash) { - logger << "Critical failure: the file \"" << file.path << "\" is of an unexpected version. \r\n"; + TaskLogger::PushText("Critical failure: the file \"" + file.path + "\" is of an unexpected version. \r\n"); return false; } else { // Patch buffer - logger << "patching file \"" << file.path << "\"\r\n"; + TaskLogger::PushText("patching file \"" + file.path + "\"\r\n"); size_t newSize(0ull); BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match if (newHash != file.diff_newHash) { - logger << "Critical failure: patched file is corrupted (hash mismatch).\r\n"; + TaskLogger::PushText("Critical failure: patched file is corrupted (hash mismatch).\r\n"); return false; } // Write patched buffer to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - logger << "Critical failure: cannot write patched file to disk.\r\n"; + TaskLogger::PushText("Critical failure: cannot write patched file to disk.\r\n"); return false; } newFile.write(newBuffer, std::streamsize(newSize)); @@ -541,7 +539,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // By this point all files matched, safe to add new ones for each (const auto & file in addedFiles) { std::filesystem::create_directories(std::filesystem::path(file.fullPath).parent_path()); - logger << "adding file \"" << file.path << "\"\r\n"; + TaskLogger::PushText("Writing file: \"" + file.path + "\"\r\n"); // Write the 'insert' instructions // Remember that we use the diff/patch function to add new files too @@ -552,14 +550,14 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Confirm new hashes match if (newHash != file.diff_newHash) { - logger << "Critical failure: new file is corrupted (hash mismatch).\r\n"; + TaskLogger::PushText("Critical failure: new file is corrupted (hash mismatch).\r\n"); return false; } // Write new file to disk std::ofstream newFile(file.fullPath, std::ios::binary | std::ios::out); if (!newFile.is_open()) { - logger << "Critical failure: cannot write new file to disk.\r\n"; + TaskLogger::PushText("Critical failure: cannot write new file to disk.\r\n"); return false; } newFile.write(newBuffer, std::streamsize(newSize)); @@ -579,14 +577,14 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Try to read source file if (!readFile(file.fullPath, oldSize, &oldBuffer, oldHash)) - logger << "The file \"" << file.path << "\" has already been removed, skipping...\r\n"; + TaskLogger::PushText("The file \"" + file.path + "\" has already been removed, skipping...\r\n"); else { // Only remove source files if they match entirely if (oldHash == file.diff_oldHash) if (!std::filesystem::remove(file.fullPath)) - logger << "Error: cannot delete file \"" << file.path << "\" from disk, delete this file manually if you can. \r\n"; + TaskLogger::PushText("Error: cannot delete file \"" + file.path + "\" from disk, delete this file manually if you can. \r\n"); else - logger << "removing file \"" << file.path << "\"\r\n"; + TaskLogger::PushText("Removing file: \"" + file.path + "\"\r\n"); } // Cleanup and finish diff --git a/src/TaskLogger.h b/src/TaskLogger.h index 5405671..1002ca1 100644 --- a/src/TaskLogger.h +++ b/src/TaskLogger.h @@ -6,6 +6,7 @@ #include #include + /*********** SINGLETON ***********/ /* */ /* Performs logging operations */ @@ -13,63 +14,93 @@ /*********** SINGLETON ***********/ class TaskLogger { public: - // Public Methods - inline static TaskLogger & GetInstance() { - static TaskLogger instance; - return instance; - } - inline size_t addCallback_Text(std::function && func) { - auto index = m_textCallbacks.size(); - m_textCallbacks.emplace_back(std::move(func)); - return index; - }inline size_t addCallback_Progress(std::function && func) { - auto index = m_progressCallbacks.size(); - m_progressCallbacks.emplace_back(std::move(func)); + // Public Methods + /** Add a callback method which will be triggered when the log receives new text. + @param func the callback function. (only arg is a string) + @param pullOld if set to true, will immediately call the function, dumping all current log data into it. (optional, defaults true) + @return index to callback in logger, used to remove callback. */ + inline static size_t AddCallback_TextAdded(std::function && func, const bool & pullOld = true) { + auto & instance = GetInstance(); + + // Dump old text if this is true + if (pullOld) + func(instance.PullText()); + + auto index = instance.m_textCallbacks.size(); + instance.m_textCallbacks.emplace_back(std::move(func)); return index; } - inline void removeCallback_Text(const size_t & index) { - m_textCallbacks.erase(m_textCallbacks.begin() + index); + /** Remove a callback method used for when text is added to the log. + @param index the index for the callback within the logger. */ + inline static void RemoveCallback_TextAdded(const size_t & index) { + auto & instance = GetInstance(); + instance.m_textCallbacks.erase(instance.m_textCallbacks.begin() + index); } - inline void removeCallback_Progress(const size_t & index) { - m_progressCallbacks.erase(m_progressCallbacks.begin() + index); + /** Add a callback method which will be triggered when the task progress changes. + @param func the callback function. (args: progress, range)aults true) + @return index to callback in logger, used to remove callback. */ + inline static size_t AddCallback_ProgressUpdated(std::function && func) { + auto & instance = GetInstance(); + auto index = instance.m_progressCallbacks.size(); + instance.m_progressCallbacks.emplace_back(std::move(func)); + return index; } - inline TaskLogger& operator <<(const std::string & message) { + /** Remove a callback method used for when the log progress changes. + @param index the index for the callback within the logger. */ + inline static void RemoveCallback_ProgressUpdated(const size_t & index) { + auto & instance = GetInstance(); + instance.m_progressCallbacks.erase(instance.m_progressCallbacks.begin() + index); + } + /** Push new text into the logger. + @param text the new text to add. */ + inline static void PushText(const std::string & text) { + auto & instance = GetInstance(); + // Add the message to the log - m_log += message; + instance.m_log += text; // Notify all observers that the log has been updated - for each (const auto & callback in m_textCallbacks) - callback(message); - - // Returning a reference so we can chain values like - // log << value << value << value - return *this; + for each (const auto & callback in instance.m_textCallbacks) + callback(text); } - inline std::string getLog() const { - return m_log; + /** Retrieve all text from the logger. + @return the entire message log. */ + inline static std::string PullText() { + return GetInstance().m_log; } - inline void setRange(const size_t & value) { - m_range = value; + /** Set the progress range for the logger. + @param value the upper range value. */ + inline static void SetRange(const size_t & value) { + GetInstance().m_range = value; } - inline size_t getRange() const { - return m_range; + /** Get the progress range for the logger. + @return the upper range value. */ + inline static size_t GetRange() { + return GetInstance().m_range; } - inline void setProgress(const size_t & amount) { - m_pos = amount; + /** Set the current progress value for the logger, <= the upper range. + @param amount the progress value to use. */ + inline static void SetProgress(const size_t & amount) { + auto & instance = GetInstance(); + instance.m_pos = amount > instance.m_range ? instance.m_range : amount; // Notify all observers that the task has updated - for each (const auto & callback in m_progressCallbacks) - callback(m_pos, m_range); + for each (const auto & callback in instance.m_progressCallbacks) + callback(instance.m_pos, instance.m_range); } private: // Private (de)constructors - ~TaskLogger() = default; - TaskLogger() = default; - TaskLogger(TaskLogger const&) = delete; - void operator=(TaskLogger const&) = delete; - + inline ~TaskLogger() = default; + inline TaskLogger() = default; + inline TaskLogger(TaskLogger const&) = delete; + inline void operator=(TaskLogger const&) = delete; + inline static TaskLogger & GetInstance() { + static TaskLogger instance; + return instance; + } + // Private Attributes std::string m_log; diff --git a/src/Threader.h b/src/Threader.h index ae3ed9e..d6e2ae7 100644 --- a/src/Threader.h +++ b/src/Threader.h @@ -57,14 +57,19 @@ class Threader { m_jobs.emplace_back(func); m_jobsStarted++; } + /** Check if the threader has completed all its jobs. + @return true if finished, false otherwise. */ inline bool isFinished() const { return m_jobsStarted == m_jobsFinished; } + /** Prepare the threader for shutdown, notifying threads to complete early. */ inline void prepareForShutdown() { m_keepOpen = false; } + /** Shutsdown the threader, forcing threads to close. */ inline void shutdown() { m_alive = false; + m_keepOpen = false; for (size_t x = 0; x < m_maxThreads && x < m_threads.size(); ++x) { if (m_threads[x].joinable()) m_threads[x].join(); diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index 0cb2495..97af9cb 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -5,20 +5,19 @@ #include -constexpr static auto CLASS_NAME = "DIRECTORY_FRAME"; static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); DirectoryFrame::~DirectoryFrame() { - UnregisterClass(CLASS_NAME, m_hinstance); + UnregisterClass("DIRECTORY_FRAME", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_directoryField); DestroyWindow(m_browseButton); } -DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) +DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { - // Try to create window class + // Create window class m_directory = directory; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); @@ -31,15 +30,16 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE & hInsta m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = CLASS_NAME; + m_wcex.lpszClassName = "DIRECTORY_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - // Create extra fields + // Create directory lookup fields m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_browseButton, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_directoryField, GWLP_USERDATA, (LONG_PTR)m_directory); setVisible(false); } @@ -160,13 +160,12 @@ HRESULT OpenFileDialog(std::string & directory) static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; auto hdc = BeginPaint(hWnd, &ps); auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - + // Draw Text constexpr static char* text[] = { "Where would you like to install to?", @@ -183,7 +182,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l TextOut(hdc, 10, 100, text[1], (int)strlen(text[1])); TextOut(hdc, 10, 115, text[2], (int)strlen(text[2])); TextOut(hdc, 10, 420, text[3], (int)strlen(text[3])); - + // Cleanup DeleteObject(big_font); DeleteObject(reg_font); @@ -192,22 +191,24 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l return 0; } else if (message == WM_COMMAND) { - if (HIWORD(wParam) == BN_CLICKED) { - auto hndl = LOWORD(lParam); - if (hndl == LOWORD(ptr->m_browseButton)) { + const auto notification = HIWORD(wParam); + auto controlHandle = HWND(lParam); + if (notification == BN_CLICKED) { + auto dirFrame = (DirectoryFrame*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); + if (dirFrame) { std::string directory(""); if (SUCCEEDED(OpenFileDialog(directory))) - ptr->setDirectory(directory); + dirFrame->setDirectory(directory); } } - else if (HIWORD(wParam) == EN_CHANGE) { - if (ptr != nullptr) { - int textLength = GetWindowTextLength(ptr->m_directoryField); - char * string = new char[textLength]; - GetWindowText(ptr->m_directoryField, string, textLength); - *ptr->m_directory = std::string(string, textLength); + else if (notification == EN_CHANGE) { + auto dirPtr = (std::string*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); + if (dirPtr) { + std::vector data(GetWindowTextLength(controlHandle) + 1ull); + GetWindowTextA(controlHandle, &data[0], (int)data.size()); + *dirPtr = std::string(data.data()); } } - } + } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index 3253c1e..641aa58 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -11,14 +11,17 @@ class DirectoryFrame : public Frame { public: // Public (de)Constructors ~DirectoryFrame(); - DirectoryFrame(std::string * directory, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); + DirectoryFrame(std::string * directory, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Methods + /** Set the directory to be used by this UI element. + @param dir the directory path. */ void setDirectory(const std::string & dir); - // Public Attributes +private: + // Private Attributes std::string * m_directory = nullptr; HWND m_directoryField = nullptr, m_browseButton = nullptr; }; diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp index 0e9ac27..8ea0f0a 100644 --- a/src/nStaller/Frames/FailFrame.cpp +++ b/src/nStaller/Frames/FailFrame.cpp @@ -4,20 +4,19 @@ #include -constexpr static auto CLASS_NAME = "FAIL_FRAME"; static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); FailFrame::~FailFrame() { - UnregisterClass(CLASS_NAME, m_hinstance); + UnregisterClass("FAIL_FRAME", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_hwndLog); - TaskLogger::GetInstance().removeCallback_Text(m_logIndex); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); } -FailFrame::FailFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) +FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) { - // Try to create window class + // Create window class m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -27,22 +26,19 @@ FailFrame::FailFrame(const HINSTANCE & hInstance, const HWND & parent, const REC m_wcex.hInstance = hInstance; m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = CLASS_NAME; + m_wcex.lpszClassName = "FAIL_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - // Failure log + // Create error log m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - auto & logger = TaskLogger::GetInstance(); SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)logger.getLog().c_str()); - m_logIndex = logger.addCallback_Text([&](const std::string & message) { + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); - + }); setVisible(false); } @@ -56,8 +52,8 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text constexpr static char* text[] = { - "Installation incomplete, see log below:", - "Press the 'Close' button to finish . . ." + "Error Occured:", + "Press the 'Cancel' button to close . . ." }; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); @@ -72,7 +68,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(reg_font); EndPaint(hWnd, &ps); - return 0; + return S_OK; } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/FailFrame.h b/src/nStaller/Frames/FailFrame.h index c6be1aa..08fdbd3 100644 --- a/src/nStaller/Frames/FailFrame.h +++ b/src/nStaller/Frames/FailFrame.h @@ -10,7 +10,7 @@ class FailFrame : public Frame { public: // Public (de)Constructors ~FailFrame(); - FailFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); + FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); private: diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index 2b65a65..ce3cce8 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -3,19 +3,18 @@ #include -constexpr static auto CLASS_NAME = "FINISH_FRAME"; static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); FinishFrame::~FinishFrame() { - UnregisterClass(CLASS_NAME, m_hinstance); + UnregisterClass("FINISH_FRAME", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_checkbox); } -FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) +FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { - // Try to create window class + // Create window class m_openDirOnClose = openDirOnClose; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); @@ -26,23 +25,22 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, con m_wcex.hInstance = hInstance; m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = CLASS_NAME; + m_wcex.lpszClassName = "FINISH_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + // Create checkbox m_checkbox = CreateWindow("Button", "Open installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)m_openDirOnClose); CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); setVisible(false); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (FinishFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; auto hdc = BeginPaint(hWnd, &ps); @@ -52,7 +50,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text constexpr static char* text[] = { "Installation Complete", - "Press the 'Close' button to finish . . ." + "Press the 'Cancel' button to close . . ." }; SelectObject(hdc, big_font); SetTextColor(hdc, RGB(25, 125, 225)); @@ -67,12 +65,18 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(reg_font); EndPaint(hWnd, &ps); - return 0; + return S_OK; } else if (message == WM_COMMAND) { - if (HIWORD(wParam) == BN_CLICKED) { - BOOL checked = IsDlgButtonChecked(hWnd, 1); - *(ptr->m_openDirOnClose) = checked; + const auto notification = HIWORD(wParam); + auto controlHandle = HWND(lParam); + if (notification == BN_CLICKED) { + auto oOCPtr = (bool*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); + if (oOCPtr) { + BOOL checked = IsDlgButtonChecked(hWnd, 1); + *oOCPtr = checked; + return S_OK; + } } } return DefWindowProc(hWnd, message, wParam, lParam); diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h index 065efb7..47eba31 100644 --- a/src/nStaller/Frames/FinishFrame.h +++ b/src/nStaller/Frames/FinishFrame.h @@ -10,9 +10,11 @@ class FinishFrame : public Frame { public: // Public (de)Constructors ~FinishFrame(); - FinishFrame(bool * openDirOnClose, const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); + FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const HWND parent, const RECT & rc); +private: + // Private Attributes bool * m_openDirOnClose = nullptr; HWND m_checkbox = nullptr; }; diff --git a/src/nStaller/Frames/Frame.h b/src/nStaller/Frames/Frame.h index 9b00af1..04b416a 100644 --- a/src/nStaller/Frames/Frame.h +++ b/src/nStaller/Frames/Frame.h @@ -5,7 +5,7 @@ #include -/** Encapsulation of a windows 'window' object.*/ +/** Encapsulation of a windows GDI 'window' object.*/ class Frame { public: // Public Methods diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index 165f60e..ceb4208 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -3,23 +3,22 @@ #include -constexpr static auto CLASS_NAME = "INSTALL_FRAME"; static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); InstallFrame::~InstallFrame() { - UnregisterClass(CLASS_NAME, m_hinstance); + UnregisterClass("INSTALL_FRAME", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_hwndLog); DestroyWindow(m_hwndPrgsBar); DestroyWindow(m_hwndPrgsText); - TaskLogger::GetInstance().removeCallback_Text(m_logIndex); - TaskLogger::GetInstance().removeCallback_Progress(m_taskIndex); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); } -InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc) +InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) { - // Try to create window class + // Create window class m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -31,22 +30,20 @@ InstallFrame::InstallFrame(const HINSTANCE & hInstance, const HWND & parent, con m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = CLASS_NAME; + m_wcex.lpszClassName = "INSTALL_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("INSTALL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - // Create log box + // Create log box and progress bar m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left)-20, (rc.bottom - rc.top)-100, m_hwnd, NULL, hInstance, NULL); m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 20, 25, m_hwnd, NULL, hInstance, NULL); m_hwndPrgsText = CreateWindowEx(WS_EX_TRANSPARENT, "static", "0%", WS_CHILD | WS_VISIBLE | SS_CENTER, ((rc.right - rc.left) / 2)-20, 0, 40, 25, m_hwndPrgsBar, NULL, hInstance, NULL); - auto & logger = TaskLogger::GetInstance(); SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)logger.getLog().c_str()); - m_logIndex = logger.addCallback_Text([&](const std::string & message) { + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); }); - m_taskIndex = logger.addCallback_Progress([&](const size_t & position, const size_t & range) { + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); std::string s = std::to_string( position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f)))+ "%"; @@ -76,7 +73,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(reg_font); EndPaint(hWnd, &ps); - return 0; + return S_OK; } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h index 3660a26..b52c0a5 100644 --- a/src/nStaller/Frames/InstallFrame.h +++ b/src/nStaller/Frames/InstallFrame.h @@ -11,7 +11,7 @@ class InstallFrame : public Frame { public: // Public (de)Constructors ~InstallFrame(); - InstallFrame(const HINSTANCE & hInstance, const HWND & parent, const RECT & rc); + InstallFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); private: diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index bdcae2c..3adc8c4 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -1,18 +1,17 @@ #include "WelcomeFrame.h" -constexpr static auto CLASS_NAME = "WELCOME_FRAME"; static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); WelcomeFrame::~WelcomeFrame() { - UnregisterClass(CLASS_NAME, m_hinstance); + UnregisterClass("WELCOME_FRAME", m_hinstance); DestroyWindow(m_hwnd); } -WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND & parent, const RECT & rc) +WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) { - // Try to create window class + // Create window class m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -24,10 +23,10 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND & parent, const m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = CLASS_NAME; + m_wcex.lpszClassName = "WELCOME_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow(CLASS_NAME, CLASS_NAME, WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); setVisible(false); } @@ -71,7 +70,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(reg_font_under); EndPaint(hWnd, &ps); - return 0; + return S_OK; } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.h b/src/nStaller/Frames/WelcomeFrame.h index b0d8a3b..f84054d 100644 --- a/src/nStaller/Frames/WelcomeFrame.h +++ b/src/nStaller/Frames/WelcomeFrame.h @@ -10,7 +10,7 @@ class WelcomeFrame : public Frame { public: // Public (de)Constructors ~WelcomeFrame(); - WelcomeFrame(const HINSTANCE hInstance, const HWND & parent, const RECT & rc); + WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); }; #endif // WELCOMEFRAME_H \ No newline at end of file diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 6367c4e..f2e719c 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -20,16 +20,16 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Installer::Installer(const HINSTANCE hInstance) : m_archive(IDR_ARCHIVE, "ARCHIVE") { + bool success = true; // Get user's program files directory TCHAR pf[MAX_PATH]; SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); m_directory = std::string(pf); // Check archive integrity - State * startingState = nullptr; if (!m_archive.exists()) { - TaskLogger::GetInstance() << "Critical Failure: archive doesn't exist!\r\n"; - startingState = new FailState(this); + TaskLogger::PushText("Critical failure: archive doesn't exist!\r\n"); + success = false; } else { const auto folderSize = *reinterpret_cast(m_archive.getPtr()); @@ -37,10 +37,8 @@ Installer::Installer(const HINSTANCE hInstance) m_directory += "\\" + m_packageName; m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); - startingState = new WelcomeState(this); } - - // Try to create window class + // Create window class WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -54,37 +52,52 @@ Installer::Installer(const HINSTANCE hInstance) wcex.lpszMenuName = NULL; wcex.lpszClassName = "nStaller"; wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&wcex); - - // Try to create window object - m_window = CreateWindow( - "nStaller", std::string(m_packageName + " - installer").c_str(), - WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, - CW_USEDEFAULT, CW_USEDEFAULT, - 800, 500, - NULL, NULL, hInstance, NULL - ); - SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); - constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); - - auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); - auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); - RECT rc = { 0, 0, 800, 500 }; - ShowWindow(m_window, true); - UpdateWindow(m_window); - AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); - SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); - - // The portions of the screen that change based on input - m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, hInstance, m_window, { 170,0,800,450 }); - m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[FINISH_FRAME] = new FinishFrame(&m_openDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); - m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); - setState(startingState); + if (!RegisterClassEx(&wcex)) { + TaskLogger::PushText("Critical failure: could not create main window.\r\n"); + success = false; + } + else { + m_window = CreateWindow( + "nStaller", std::string(m_packageName + " - installer").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + + // Create + SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); + + auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(m_window, true); + UpdateWindow(m_window); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + + // The portions of the screen that change based on input + m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); + m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, hInstance, m_window, { 170,0,800,450 }); + m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); + m_frames[FINISH_FRAME] = new FinishFrame(&m_openDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); + m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); + setState(new WelcomeState(this)); + } + if (!success) + invalidate(); +} + +void Installer::invalidate() +{ + setState(new FailState(this)); + showButtons(false, false, true); + enableButtons(false, false, true); + m_valid = false; } void Installer::showFrame(const FrameEnums & newIndex) @@ -96,13 +109,21 @@ void Installer::showFrame(const FrameEnums & newIndex) void Installer::setState(State * state) { - m_state = state; - m_state->enact(); + if (!m_valid) + delete state; // refuse new states + else { + if (m_state != nullptr) + delete m_state; + m_state = state; + m_state->enact(); + RECT rc = { 0, 0, 160, 450 }; + RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + } } -int Installer::getCurrentIndex() const +Installer::FrameEnums Installer::getCurrentIndex() const { - return (int)m_currentIndex; + return m_currentIndex; } std::string Installer::getDirectory() const @@ -134,16 +155,20 @@ void Installer::updateButtons(const WORD btnHandle) void Installer::showButtons(const bool & prev, const bool & next, const bool & close) { - ShowWindow(m_prevBtn, prev); - ShowWindow(m_nextBtn, next); - ShowWindow(m_exitBtn, close); + if (m_valid) { + ShowWindow(m_prevBtn, prev); + ShowWindow(m_nextBtn, next); + ShowWindow(m_exitBtn, close); + } } void Installer::enableButtons(const bool & prev, const bool & next, const bool & close) { - EnableWindow(m_prevBtn, prev); - EnableWindow(m_nextBtn, next); - EnableWindow(m_exitBtn, close); + if (m_valid) { + EnableWindow(m_prevBtn, prev); + EnableWindow(m_nextBtn, next); + EnableWindow(m_exitBtn, close); + } } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) @@ -174,10 +199,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SetDCPenColor(hdc, RGB(100, 100, 100)); Rectangle(hdc, 26, 0, 29, 450); int vertical_offset = 15; - int frameIndex = ptr->getCurrentIndex(); + int frameIndex = (int)ptr->getCurrentIndex(); for (int x = 0; x < 4; ++x) { // Draw Circle - auto color = x == frameIndex ? RGB(25, 225, 125) : x < frameIndex ? RGB(25, 125, 225) : RGB(255, 255, 255); + auto color = x == frameIndex ? RGB(25, 225, 125) : RGB(255, 255, 255); if (x == 3 && frameIndex == 4) color = RGB(225, 25, 25); SetDCBrushColor(hdc, color); @@ -195,6 +220,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(font); EndPaint(hWnd, &ps); + return S_OK; } else if (message == WM_DESTROY) PostQuitMessage(0); diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index e3ef5f0..20e4d81 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -10,7 +10,7 @@ class Frame; class State; -/***/ +/** Encapsulates the logical features of the installer. */ class Installer { public: // Public (de)Constructors @@ -26,19 +26,44 @@ class Installer { // Public Methods + /** When called, invalidates the installer, halting it from progressing. */ + void invalidate(); + /** Displays the screen matching the supplied enumeration. + @param newIndex the screen to make visible (makes current screen invisible). */ void showFrame(const FrameEnums & newIndex); + /** Override the current state, making the supplied state active. + @param state the new state to use. */ void setState(State * state); - int getCurrentIndex() const; + /** Retrieves the current frame's enumeration. + @return the current frame's index, as an enumeration. */ + FrameEnums getCurrentIndex() const; + /** Retrieves the current directory chosen for installation. + @return active installation directory. */ std::string getDirectory() const; + /** Retrieves the pointer to the compressed packaged contents. + @return the package pointer (offset of folder name data). */ char * getPackagePointer() const; + /** Retrieves the size of the compressed package. + @return the package size (minus the folder name data). */ size_t getPackageSize() const; + /** Check which button has been active, and perform it's state operation. + @param btnHandle handle to the currently active button. */ void updateButtons(const WORD btnHandle); + /** Sets the visibility state of all 3 buttons. + @param prev make the 'previous' button visible. + @param next make the 'next' button visible. + @param close make the 'close' button visible. */ void showButtons(const bool & prev, const bool & next, const bool & close); + /** Sets the enable state of all 3 buttons. + @param prev make the 'previous' button enabled. + @param next make the 'next' button enabled. + @param close make the 'close' button enabled. */ void enableButtons(const bool & prev, const bool & next, const bool & close); private: // Private Attributes + bool m_valid = true; Resource m_archive; std::string m_directory = "", m_packageName = ""; bool m_openDirectoryOnClose = true; diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp index e2ddb9c..e23cdd4 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/States/DirectoryState.cpp @@ -16,13 +16,11 @@ void DirectoryState::enact() void DirectoryState::pressPrevious() { m_installer->setState(new WelcomeState(m_installer)); - delete this; } void DirectoryState::pressNext() { m_installer->setState(new InstallState(m_installer)); - delete this; } void DirectoryState::pressClose() diff --git a/src/nStaller/States/DirectoryState.h b/src/nStaller/States/DirectoryState.h index cb674f8..08bde84 100644 --- a/src/nStaller/States/DirectoryState.h +++ b/src/nStaller/States/DirectoryState.h @@ -5,7 +5,7 @@ #include "State.h" -/***/ +/** This state encapuslates the "Choose a directory - Screen" state. */ class DirectoryState : public State { public: // Public (de)Constructors @@ -13,7 +13,7 @@ class DirectoryState : public State { DirectoryState(Installer * installer); - // Public Methods + // Public Interface Implementations virtual void enact(); virtual void pressPrevious(); virtual void pressNext(); diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp index e185213..b22821c 100644 --- a/src/nStaller/States/FailState.cpp +++ b/src/nStaller/States/FailState.cpp @@ -1,5 +1,8 @@ #include "FailState.h" +#include "Common.h" +#include "TaskLogger.h" #include "../Installer.h" +#include FailState::FailState(Installer * installer) @@ -10,6 +13,19 @@ void FailState::enact() m_installer->showFrame(Installer::FrameEnums::FAIL_FRAME); m_installer->showButtons(false, false, true); m_installer->enableButtons(false, false, true); + + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto data = "Installer error log:\r\n" + TaskLogger::PullText(); + + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else { + file.write(data.c_str(), (std::streamsize)data.size()); + file.close(); + } } void FailState::pressPrevious() diff --git a/src/nStaller/States/FailState.h b/src/nStaller/States/FailState.h index 3f35fbc..b3ae483 100644 --- a/src/nStaller/States/FailState.h +++ b/src/nStaller/States/FailState.h @@ -5,7 +5,7 @@ #include "State.h" -/***/ +/** This state encapuslates the "Failure - Screen" state. */ class FailState: public State { public: // Public (de)Constructors @@ -13,7 +13,7 @@ class FailState: public State { FailState(Installer * installer); - // Public Methods + // Public Interface Implementations virtual void enact(); virtual void pressPrevious(); virtual void pressNext(); diff --git a/src/nStaller/States/FinishState.h b/src/nStaller/States/FinishState.h index 901b344..9db1ea4 100644 --- a/src/nStaller/States/FinishState.h +++ b/src/nStaller/States/FinishState.h @@ -5,7 +5,7 @@ #include "State.h" -/***/ +/** This state encapuslates the "Finished - Screen" state. */ class FinishState: public State { public: // Public (de)Constructors @@ -13,7 +13,7 @@ class FinishState: public State { FinishState(Installer * installer); - // Public Methods + // Public Interface Implementations virtual void enact(); virtual void pressPrevious(); virtual void pressNext(); diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp index cd69414..42a9c99 100644 --- a/src/nStaller/States/InstallState.cpp +++ b/src/nStaller/States/InstallState.cpp @@ -1,6 +1,5 @@ #include "InstallState.h" #include "FinishState.h" -#include "FailState.h" #include "Common.h" #include "BufferTools.h" #include "DirectoryTools.h" @@ -22,7 +21,7 @@ void InstallState::enact() auto directory = m_installer->getDirectory(); sanitize_path(directory); if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getPackageSize(), byteCount, fileCount)) - m_installer->setState(new FailState(m_installer)); + m_installer->invalidate(); else m_installer->enableButtons(false, true, false); }); @@ -37,7 +36,6 @@ void InstallState::pressPrevious() void InstallState::pressNext() { m_installer->setState(new FinishState(m_installer)); - delete this; } void InstallState::pressClose() diff --git a/src/nStaller/States/InstallState.h b/src/nStaller/States/InstallState.h index d2d368b..f764819 100644 --- a/src/nStaller/States/InstallState.h +++ b/src/nStaller/States/InstallState.h @@ -6,7 +6,7 @@ #include -/***/ +/** This state encapuslates the "Installing - Screen" state. */ class InstallState : public State { public: // Public (de)Constructors @@ -14,7 +14,7 @@ class InstallState : public State { InstallState(Installer * installer); - // Public Methods + // Public Interface Implementations virtual void enact(); virtual void pressPrevious(); virtual void pressNext(); diff --git a/src/nStaller/States/State.h b/src/nStaller/States/State.h index 6e5497d..e8a7691 100644 --- a/src/nStaller/States/State.h +++ b/src/nStaller/States/State.h @@ -12,10 +12,14 @@ class State { State(Installer * installer) : m_installer(installer) {} - // Public Methods + // Public Interface Declarations + /** Trigger this state to perform its screen action. */ virtual void enact() = 0; + /** Cause this state to process the "previous" action. */ virtual void pressPrevious() = 0; + /** Cause this state to process the "next" action. */ virtual void pressNext() = 0; + /** Cause this state to process the "close" action. */ virtual void pressClose() = 0; diff --git a/src/nStaller/States/WelcomeState.cpp b/src/nStaller/States/WelcomeState.cpp index e2090db..64c5877 100644 --- a/src/nStaller/States/WelcomeState.cpp +++ b/src/nStaller/States/WelcomeState.cpp @@ -20,7 +20,6 @@ void WelcomeState::pressPrevious() void WelcomeState::pressNext() { m_installer->setState(new DirectoryState(m_installer)); - delete this; } void WelcomeState::pressClose() diff --git a/src/nStaller/States/WelcomeState.h b/src/nStaller/States/WelcomeState.h index ba84b3a..17a03f3 100644 --- a/src/nStaller/States/WelcomeState.h +++ b/src/nStaller/States/WelcomeState.h @@ -5,7 +5,7 @@ #include "State.h" -/***/ +/** This state encapuslates the "Welcome - Screen" state. */ class WelcomeState : public State { public: // Public (de)Constructors @@ -13,7 +13,7 @@ class WelcomeState : public State { WelcomeState(Installer * installer); - // Public Methods + // Public Interface Implementations virtual void enact(); virtual void pressPrevious(); virtual void pressNext(); diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index d5bd527..2ec6859 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -9,13 +9,13 @@ void DiffCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - auto & logger = TaskLogger::GetInstance(); - logger << + TaskLogger::PushText( " ~\r\n" " Patch Maker /\r\n" " ~-----------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Check command line arguments std::string oldDirectory(""), newDirectory(""), dstDirectory(""); @@ -71,7 +71,8 @@ void DiffCommand::execute(const int & argc, char * argv[]) const delete[] diffBuffer; // Output results - logger - << "Instruction(s): " << std::to_string(instructionCount) << "\r\n" - << "Bytes written: " << std::to_string(diffSize) << "\r\n"; + TaskLogger::PushText( + "Instruction(s): " + std::to_string(instructionCount) + "\r\n" + + "Bytes written: " + std::to_string(diffSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index a9e8864..45eaa07 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -10,13 +10,13 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - auto & logger = TaskLogger::GetInstance(); - logger << + TaskLogger::PushText( " ~\r\n" " Installer Maker /\r\n" " ~-----------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -71,7 +71,8 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const delete[] packBuffer; // Output results - logger - << "Files packaged: " << std::to_string(fileCount) << "\r\n" - << "Bytes packaged: " << std::to_string(packSize) << "\r\n"; + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(packSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index 9e41ed9..d7bde64 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -9,13 +9,13 @@ void PackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - auto & logger = TaskLogger::GetInstance(); - logger << + TaskLogger::PushText( " ~\r\n" " Packager /\r\n" " ~-----------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -59,7 +59,8 @@ void PackCommand::execute(const int & argc, char * argv[]) const delete[] packBuffer; // Output results - logger - << "Files packaged: " << std::to_string(fileCount) << "\r\n" - << "Bytes packaged: " << std::to_string(packSize) << "\r\n"; + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(packSize) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 52c3762..2361599 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -9,13 +9,13 @@ void PatchCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - auto & logger = TaskLogger::GetInstance(); - logger << + TaskLogger::PushText( " ~\r\n" " Patcher /\r\n" " ~-----------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -51,7 +51,8 @@ void PatchCommand::execute(const int & argc, char * argv[]) const delete[] diffBuffer; // Output results - logger - << "Instruction(s): " << std::to_string(instructionsUsed) << "\r\n" - << "Bytes written: " << std::to_string(bytesWritten) << "\r\n"; + TaskLogger::PushText( + "Instruction(s): " + std::to_string(instructionsUsed) + "\r\n" + + "Bytes written: " + std::to_string(bytesWritten) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index ff8ee03..ac987e2 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -9,13 +9,13 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console - auto & logger = TaskLogger::GetInstance(); - logger << + TaskLogger::PushText( " ~\r\n" " Unpacker /\r\n" " ~-----------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Check command line arguments std::string srcDirectory(""), dstDirectory(""); @@ -61,7 +61,8 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const delete[] packBuffer; // Output results - logger - << "Files written: " << std::to_string(fileCount) << "\r\n" - << "Bytes processed: " << std::to_string(byteCount) << "\r\n"; + TaskLogger::PushText( + "Files written: " + std::to_string(fileCount) + "\r\n" + + "Bytes processed: " + std::to_string(byteCount) + "\r\n" + ); } \ No newline at end of file diff --git a/src/nSuite/nSuite.cpp b/src/nSuite/nSuite.cpp index 5842027..7bc8582 100644 --- a/src/nSuite/nSuite.cpp +++ b/src/nSuite/nSuite.cpp @@ -24,8 +24,7 @@ int main(int argc, char *argv[]) { "-diff" , new DiffCommand() }, { "-patch" , new PatchCommand() } }; - auto & logger = TaskLogger::GetInstance(); - logger.addCallback_Text([&](const std::string & message) { + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { std::cout << message; }); @@ -52,7 +51,7 @@ int main(int argc, char *argv[]) // Output results and finish const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - logger << "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n"; + TaskLogger::PushText("Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n"); system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/nUpdater/nUpdater.cpp b/src/nUpdater/nUpdater.cpp index 7e148b4..d6c96cf 100644 --- a/src/nUpdater/nUpdater.cpp +++ b/src/nUpdater/nUpdater.cpp @@ -20,8 +20,7 @@ static auto get_patches(const std::string & srcDirectory) /** Entry point. */ int main() { - auto & logger = TaskLogger::GetInstance(); - logger.addCallback_Text([&](const std::string & message) { + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { std::cout << message; }); @@ -30,14 +29,15 @@ int main() const auto patches = get_patches(dstDirectory); // Report an overview of supplied procedure - logger << + TaskLogger::PushText( " ~\r\n" " Updater /\r\n" " ~-----------------~\r\n" " /\r\n" "~\r\n\r\n" - "There are " << std::to_string(patches.size()) << " patches(s) found.\r\n" - "\r\n"; + "There are " + std::to_string(patches.size()) + " patches(s) found.\r\n" + "\r\n" + ); if (patches.size()) { pause_program("Ready to update?"); @@ -49,7 +49,7 @@ int main() std::ifstream diffFile(patch, std::ios::binary | std::ios::beg); const size_t diffSize = std::filesystem::file_size(patch); if (!diffFile.is_open()) { - logger << "Cannot read diff file, skipping...\r\n"; + TaskLogger::PushText("Cannot read diff file, skipping...\r\n"); continue; } else { @@ -58,14 +58,14 @@ int main() diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) { - logger << "skipping patch...\r\n"; + TaskLogger::PushText("skipping patch...\r\n"); delete[] diffBuffer; continue; } // Delete patch file at very end if (!std::filesystem::remove(patch)) - logger << "Cannot delete diff file \"" << patch.path().string() << "\" from disk, make sure to delete it manually.\r\n"; + TaskLogger::PushText("Cannot delete diff file \"" + patch.path().string() + "\" from disk, make sure to delete it manually.\r\n"); patchesApplied++; delete[] diffBuffer; } @@ -74,10 +74,11 @@ int main() // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - logger - << "Patches used: " << std::to_string(patchesApplied) << " out of " << std::to_string(patches.size()) << "\r\n" - << "Bytes written: " << std::to_string(bytesWritten) << "\r\n" - << "Total duration: " << std::to_string(elapsed_seconds.count()) << " seconds\r\n\r\n"; + TaskLogger::PushText( + "Patches used: " + std::to_string(patchesApplied) + " out of " + std::to_string(patches.size()) + "\r\n" + + "Bytes written: " + std::to_string(bytesWritten) + "\r\n" + + "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n" + ); } system("pause"); From 38f70dbd8e33b05aca716dc9afd7cc59f16434b7 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 8 Apr 2019 15:26:25 -0500 Subject: [PATCH 06/44] More features Added "show directory on close" function Safetied improper installation directory with an error box Fixed all level 4 warnings --- README.md | 18 +++++++++--------- src/Common.h | 10 ++++++++++ src/nStaller/Frames/DirectoryFrame.cpp | 11 ++++++++--- src/nStaller/Frames/FinishFrame.cpp | 2 +- src/nStaller/Installer.cpp | 12 +++++++++--- src/nStaller/Installer.h | 6 ++++-- src/nStaller/States/DirectoryState.cpp | 13 ++++++++++++- src/nStaller/nStaller.cpp | 6 ++++++ src/nSuite/Commands/DiffCommand.cpp | 3 +-- src/nSuite/Commands/InstallerCommand.cpp | 3 +-- src/nSuite/Commands/PackCommand.cpp | 3 +-- src/nSuite/Commands/PatchCommand.cpp | 3 +-- src/nSuite/Commands/UnpackCommand.cpp | 3 +-- 13 files changed, 64 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 66ee323..378fee9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# nStaller Tools +# nSuite Packaging Tools -This project is both a library and a toolset that allows developers to generate and distribute portable installers for a given input directory, as well as diff the contents of 2 input directories into a single patch file. -The library includes functions to compress/decompress and diff/patch both files and directoires. -The toolset wraps this functionality into a few example programs. +This project is is a toolset that allows users to easily package folders into portable installers, as well as easily diff 2 directories, generating patchfiles that can be used by our updater tool. +The project executables are made to work for Windows 7/10. +However, the library was made in C++17, and the functions provided in BufferTools/DirectoryTools should prove general enough to be adapted for other OS's. -## nSuite.exe -The nSuite tool is intended to be used by developers or those who wish to package/diff/distribute one or many files. It is run by command-line, and requires one of the following sets of arguments to be fulfilled: +## nSuite +The nSuite program is intended to be used by developers or those who wish to package/diff/distribute one or many files. It is run by command-line, and requires one of the following sets of arguments to be fulfilled: - #### `-installer -src= -dst=` - Packages + compress an entire **source** directory into a **destionation** installer *.exe file* (filename optional) @@ -25,14 +25,14 @@ The nSuite tool is intended to be used by developers or those who wish to packag ## Installer -The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The installer is very basic and installs to the directory it runs from. +The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The current version of the installer is implemented using Windows GDI (whereas the remaining tools are all command-line). ## Updater The updater tool is a portable version of the patch command. It automatically applies all *.ndiff* files it can find, and if successfull, deletes them after. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers. For instance, if patches were found that would modify an app from v.1 -> v.2 -> v.3 -> v.1, the updater won't try to stop at v.3 (as there are no version headers applied to patches). Further, this updater cannot connect to any servers to fetch patch data, but that would be the next logical step after implementing versioning. # Dependencies/Requirements - 64-bit only - - Might only work in Windows + - Windows 7/8/10 - Uses [CMake](https://cmake.org/) - Requires the [LZ4 - Compression Library](https://github.com/lz4/lz4) to build, but **does not** come bundled with it - - Using BSD-3-Clause license + - Using BSD-3-Clause license \ No newline at end of file diff --git a/src/Common.h b/src/Common.h index 13eb733..2dc8581 100644 --- a/src/Common.h +++ b/src/Common.h @@ -11,6 +11,16 @@ #include +/** Changes an input string to lower case, and returns it. +@param string the input string. +@return lower case version of the string. */ +inline static std::string string_to_lower(const std::string & string) +{ + std::string input = string; + std::transform(input.begin(), input.end(), input.begin(), [](const int & character){ return static_cast(::tolower(character)); }); + return input; +} + /** Increment a pointer's address by the offset provided. @param ptr the pointer to increment by the offset amount. @param offset the offset amount to apply to the pointer's address. diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index 97af9cb..a47e8b5 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -188,7 +188,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l DeleteObject(reg_font); EndPaint(hWnd, &ps); - return 0; + return S_OK; } else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); @@ -197,8 +197,12 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l auto dirFrame = (DirectoryFrame*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); if (dirFrame) { std::string directory(""); - if (SUCCEEDED(OpenFileDialog(directory))) - dirFrame->setDirectory(directory); + if (SUCCEEDED(OpenFileDialog(directory))) { + if (directory != "" && directory.length() > 2ull) { + dirFrame->setDirectory(directory); + return S_OK; + } + } } } else if (notification == EN_CHANGE) { @@ -207,6 +211,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l std::vector data(GetWindowTextLength(controlHandle) + 1ull); GetWindowTextA(controlHandle, &data[0], (int)data.size()); *dirPtr = std::string(data.data()); + return S_OK; } } } diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index ce3cce8..bc9ee3f 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -33,7 +33,7 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create checkbox - m_checkbox = CreateWindow("Button", "Open installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); + m_checkbox = CreateWindow("Button", "Show installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)m_openDirOnClose); CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); setVisible(false); diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index f2e719c..1393496 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -84,7 +84,7 @@ Installer::Installer(const HINSTANCE hInstance) m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, hInstance, m_window, { 170,0,800,450 }); m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[FINISH_FRAME] = new FinishFrame(&m_openDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); + m_frames[FINISH_FRAME] = new FinishFrame(&m_showDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); setState(new WelcomeState(this)); } @@ -131,6 +131,11 @@ std::string Installer::getDirectory() const return m_directory; } +bool Installer::shouldShowDirectory() const +{ + return m_showDirectoryOnClose && m_valid; +} + char * Installer::getPackagePointer() const { return m_packagePtr; @@ -199,7 +204,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SetDCPenColor(hdc, RGB(100, 100, 100)); Rectangle(hdc, 26, 0, 29, 450); int vertical_offset = 15; - int frameIndex = (int)ptr->getCurrentIndex(); + const auto frameIndex = (int)ptr->getCurrentIndex(); for (int x = 0; x < 4; ++x) { // Draw Circle auto color = x == frameIndex ? RGB(25, 225, 125) : RGB(255, 255, 255); @@ -212,10 +217,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text TextOut(hdc, 50, vertical_offset, step_labels[x], (int)strlen(step_labels[x])); - vertical_offset += 50; if (x == 2) vertical_offset = 420; + else + vertical_offset += 50; } DeleteObject(font); diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 20e4d81..cf5b44d 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -40,6 +40,9 @@ class Installer { /** Retrieves the current directory chosen for installation. @return active installation directory. */ std::string getDirectory() const; + /** Retrieve if the chosen directory should open when the installer closes. + @return true if should show, false otherwise. */ + bool shouldShowDirectory() const; /** Retrieves the pointer to the compressed packaged contents. @return the package pointer (offset of folder name data). */ char * getPackagePointer() const; @@ -63,10 +66,9 @@ class Installer { private: // Private Attributes - bool m_valid = true; Resource m_archive; std::string m_directory = "", m_packageName = ""; - bool m_openDirectoryOnClose = true; + bool m_valid = true, m_showDirectoryOnClose = true; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull; FrameEnums m_currentIndex = WELCOME_FRAME; diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp index e23cdd4..90f05eb 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/States/DirectoryState.cpp @@ -2,6 +2,7 @@ #include "WelcomeState.h" #include "InstallState.h" #include "../Installer.h" +#include DirectoryState::DirectoryState(Installer * installer) @@ -20,7 +21,17 @@ void DirectoryState::pressPrevious() void DirectoryState::pressNext() { - m_installer->setState(new InstallState(m_installer)); + auto directory = m_installer->getDirectory(); + + if (directory == "" || directory == " " || directory.length() < 3 || !std::filesystem::is_directory(directory)) + MessageBox( + NULL, + "Please enter a valid directory before proceeding.", + "Invalid path!", + MB_OK | MB_ICONERROR | MB_TASKMODAL + ); + else + m_installer->setState(new InstallState(m_installer)); } void DirectoryState::pressClose() diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 1f5d5ba..697912b 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -11,5 +11,11 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ TranslateMessage(&msg); DispatchMessage(&msg); } + + // Open the installation directory if finished successfully + if user wants it to + if (installer.shouldShowDirectory()) + ShellExecute(NULL, "open", installer.getDirectory().c_str(), NULL, NULL, SW_SHOWDEFAULT); + + // Close return (int)msg.wParam; } \ No newline at end of file diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 2ec6859..4150e62 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -20,8 +20,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const // Check command line arguments std::string oldDirectory(""), newDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-old=") oldDirectory = std::string(&argv[x][5]); else if (command == "-new=") diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index 45eaa07..d8ec6df 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -21,8 +21,7 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") srcDirectory = std::string(&argv[x][5]); else if (command == "-dst=") diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index d7bde64..465441e 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -20,8 +20,7 @@ void PackCommand::execute(const int & argc, char * argv[]) const // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") srcDirectory = std::string(&argv[x][5]); else if (command == "-dst=") diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 2361599..3df08bd 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -20,8 +20,7 @@ void PatchCommand::execute(const int & argc, char * argv[]) const // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") srcDirectory = std::string(&argv[x][5]); else if (command == "-dst=") diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index ac987e2..e3c54ad 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -20,8 +20,7 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const // Check command line arguments std::string srcDirectory(""), dstDirectory(""); for (int x = 2; x < argc; ++x) { - std::string command(argv[x], 5); - std::transform(command.begin(), command.end(), command.begin(), ::tolower); + std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") srcDirectory = std::string(&argv[x][5]); else if (command == "-dst=") From b4b7fc482b06faa40599b7717c26735dbfee98a3 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 8 Apr 2019 18:15:03 -0500 Subject: [PATCH 07/44] GDI+ Changed the graphics -> gradient backgrounds implemented --- CMakeLists.txt | 3 +- README.md | 4 +- src/nStaller/Frames/DirectoryFrame.cpp | 52 ++++++++++-------- src/nStaller/Frames/FailFrame.cpp | 40 ++++++++------ src/nStaller/Frames/FinishFrame.cpp | 42 ++++++++------- src/nStaller/Frames/Frame.h | 10 ++++ src/nStaller/Frames/InstallFrame.cpp | 35 +++++++----- src/nStaller/Frames/WelcomeFrame.cpp | 67 ++++++++++++----------- src/nStaller/Installer.cpp | 72 +++++++++++++------------ src/nStaller/Installer.h | 4 +- src/nStaller/States/DirectoryState.cpp | 3 +- src/nStaller/States/FinishState.cpp | 1 + src/nStaller/nStaller.cpp | 7 +++ src/nStaller/nStaller.rc | Bin 1322 -> 2996 bytes src/resource.h | 1 + 15 files changed, 199 insertions(+), 142 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8254cb3..7ac6389 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,8 @@ include_directories( # Add libraries common throughout entire project link_libraries( debug ${LZ4_DIR}/x64_Debug/liblz4_static.lib - optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib + optimized ${LZ4_DIR}/x64_Release/liblz4_static.lib + Gdiplus.lib ) # add all sub-projects and plugins here diff --git a/README.md b/README.md index 378fee9..b525c12 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ The nSuite program is intended to be used by developers or those who wish to pac ## Installer -The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The current version of the installer is implemented using Windows GDI (whereas the remaining tools are all command-line). +The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The current version of the installer is implemented using Windows GDI+ (whereas the remaining tools are all command-line). ## Updater -The updater tool is a portable version of the patch command. It automatically applies all *.ndiff* files it can find, and if successfull, deletes them after. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers. For instance, if patches were found that would modify an app from v.1 -> v.2 -> v.3 -> v.1, the updater won't try to stop at v.3 (as there are no version headers applied to patches). Further, this updater cannot connect to any servers to fetch patch data, but that would be the next logical step after implementing versioning. +The updater tool is a portable version of the patch command. It automatically applies all *.ndiff* files it can find, and if successfull, deletes them afterwards. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers. For instance, if patches were found that would modify an app from v.1 -> v.3 -> v.2, the updater won't stop at v.3, it will proceed to v.2. Further, this updater cannot connect to any servers to fetch patch data, but that would be the next logical step after implementing versioning. # Dependencies/Requirements - 64-bit only diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index a47e8b5..e88f3b7 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -34,6 +34,9 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE hInstanc m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + m_width = rc.right - rc.left; + m_height = rc.bottom - rc.top; // Create directory lookup fields m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); @@ -162,30 +165,37 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + Graphics graphics(BeginPaint(hWnd, &ps)); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(frame->m_width, frame->m_height), + Color(255, 255, 255, 255), + Color(50, 25, 125, 225) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text - constexpr static char* text[] = { - "Where would you like to install to?", - "Choose a folder by pressing the 'Browse' button.", - "Alternatively, type a specific directory into the box below.", - "Press the 'Next' button to begin installing . . ." + constexpr static wchar_t* text[] = { + L"Where would you like to install to?", + L"Choose a folder by pressing the 'Browse' button.", + L"Alternatively, type a specific directory into the box below.", + L"Press the 'Next' button to begin installing . . ." }; - SelectObject(hdc, big_font); - SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); - - SelectObject(hdc, reg_font); - SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 100, text[1], (int)strlen(text[1])); - TextOut(hdc, 10, 115, text[2], (int)strlen(text[2])); - TextOut(hdc, 10, 420, text[3], (int)strlen(text[3])); - - // Cleanup - DeleteObject(big_font); - DeleteObject(reg_font); + graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString(text[3], -1, ®Font, PointF{ 10, 420 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp index 8ea0f0a..120391c 100644 --- a/src/nStaller/Frames/FailFrame.cpp +++ b/src/nStaller/Frames/FailFrame.cpp @@ -32,6 +32,9 @@ FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + m_width = rc.right - rc.left; + m_height = rc.bottom - rc.top; // Create error log m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); @@ -46,26 +49,29 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + Graphics graphics(BeginPaint(hWnd, &ps)); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - // Draw Text - constexpr static char* text[] = { - "Error Occured:", - "Press the 'Cancel' button to close . . ." - }; - SelectObject(hdc, big_font); - SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(frame->m_width, frame->m_height), + Color(255, 255, 255, 255), + Color(50, 225, 25, 75) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); - SelectObject(hdc, reg_font); - SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 420, text[1], (int)strlen(text[1])); + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); - // Cleanup - DeleteObject(big_font); - DeleteObject(reg_font); + // Draw Text + graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Press the 'Cancel' button to close . . .", -1, ®Font, PointF{ 10, 420 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index bc9ee3f..4d773fe 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -31,6 +31,9 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + m_width = rc.right - rc.left; + m_height = rc.bottom - rc.top; // Create checkbox m_checkbox = CreateWindow("Button", "Show installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); @@ -43,27 +46,30 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + Graphics graphics(BeginPaint(hWnd, &ps)); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - // Draw Text - constexpr static char* text[] = { - "Installation Complete", - "Press the 'Cancel' button to close . . ." - }; - SelectObject(hdc, big_font); - SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); - - SelectObject(hdc, reg_font); - SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 420, text[1], (int)strlen(text[1])); + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(frame->m_width, frame->m_height), + Color(255, 255, 255, 255), + Color(50, 25, 255, 125) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); - // Cleanup - DeleteObject(big_font); - DeleteObject(reg_font); + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + // Draw Text + graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Press the 'Cancel' button to close . . .", -1, ®Font, PointF{ 10, 420 }, &blackBrush); + EndPaint(hWnd, &ps); return S_OK; } diff --git a/src/nStaller/Frames/Frame.h b/src/nStaller/Frames/Frame.h index 04b416a..90a3bbf 100644 --- a/src/nStaller/Frames/Frame.h +++ b/src/nStaller/Frames/Frame.h @@ -3,8 +3,14 @@ #define FRAME_H #include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) +using namespace Gdiplus; + /** Encapsulation of a windows GDI 'window' object.*/ class Frame { public: @@ -17,6 +23,10 @@ class Frame { } + // Public Attributes + int m_width = 0, m_height = 0; + + protected: // Private Attributes WNDCLASSEX m_wcex; diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index ceb4208..601a498 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -34,6 +34,9 @@ InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("INSTALL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + m_width = rc.right - rc.left; + m_height = rc.bottom - rc.top; // Create log box and progress bar m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left)-20, (rc.bottom - rc.top)-100, m_hwnd, NULL, hInstance, NULL); @@ -50,7 +53,6 @@ InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const R SetWindowText(m_hwndPrgsText, s.c_str()); }); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); } @@ -58,19 +60,26 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); + Graphics graphics(BeginPaint(hWnd, &ps)); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(frame->m_width, frame->m_height), + Color(255, 255, 255, 255), + Color(50, 25, 125, 225) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); - // Draw Title - constexpr static auto text = "Installing"; - SelectObject(hdc, big_font); - SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text, (int)strlen(text)); - - // Cleanup - DeleteObject(big_font); - DeleteObject(reg_font); + // Draw Text + graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index 3adc8c4..ef3c0d2 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -27,6 +27,9 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + m_width = rc.right - rc.left; + m_height = rc.bottom - rc.top; setVisible(false); } @@ -34,41 +37,41 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto big_font = CreateFont(35, 15, 0, 0, FW_ULTRABOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - auto reg_font_under = CreateFont(17, 7, 0, 0, FW_NORMAL, FALSE, TRUE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - - // Draw Text - constexpr static char* text[] = { - "Welcome", - "This custom installer was generated using nSuite", - "Source-code can be found at:", - "https://github.com/Yattabyte/nStallr/", - "Press the 'Next' button to continue . . ." - }; - SelectObject(hdc, big_font); - SetTextColor(hdc, RGB(25, 125, 225)); - TextOut(hdc, 10, 10, text[0], (int)strlen(text[0])); - - SelectObject(hdc, reg_font); - SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 100, text[1], (int)strlen(text[1])); - TextOut(hdc, 10, 115, text[2], (int)strlen(text[2])); + Graphics graphics(BeginPaint(hWnd, &ps)); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - SelectObject(hdc, reg_font_under); - SetTextColor(hdc, RGB(0, 0, 238)); - TextOut(hdc, 35, 130, text[3], (int)strlen(text[3])); + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(frame->m_width, frame->m_height), + Color(255, 255, 255, 255), + Color(50, 25, 125, 225) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); - SelectObject(hdc, reg_font); - SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, 10, 420, text[4], (int)strlen(text[4])); - - // Cleanup - DeleteObject(big_font); - DeleteObject(reg_font); - DeleteObject(reg_font_under); + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + // Draw Text + constexpr static wchar_t* text[] = { + L"Welcome", + L"This custom installer was generated using nSuite", + L"Source-code can be found at:", + L"https://github.com/Yattabyte/nSuite", + L"Press the 'Next' button to continue . . ." + }; + graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString(text[3], -1, ®UnderFont, PointF{ 35, 130 }, &blueBrush); + graphics.DrawString(text[4], -1, ®Font, PointF{ 10, 420 }, &blackBrush); + EndPaint(hWnd, &ps); return S_OK; } diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 1393496..7b77dad 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -46,7 +46,7 @@ Installer::Installer(const HINSTANCE hInstance) wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; - wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + wcex.hIcon = LoadIcon(hInstance, (LPCSTR)IDI_ICON1); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; @@ -58,7 +58,7 @@ Installer::Installer(const HINSTANCE hInstance) } else { m_window = CreateWindow( - "nStaller", std::string(m_packageName + " - installer").c_str(), + "nStaller", std::string(m_packageName + " Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, @@ -88,8 +88,11 @@ Installer::Installer(const HINSTANCE hInstance) m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); setState(new WelcomeState(this)); } + +#ifndef DEBUG if (!success) invalidate(); +#endif } void Installer::invalidate() @@ -100,6 +103,11 @@ void Installer::invalidate() m_valid = false; } +void Installer::finish() +{ + m_finished = true; +} + void Installer::showFrame(const FrameEnums & newIndex) { m_frames[m_currentIndex]->setVisible(false); @@ -133,7 +141,7 @@ std::string Installer::getDirectory() const bool Installer::shouldShowDirectory() const { - return m_showDirectoryOnClose && m_valid; + return m_showDirectoryOnClose && m_valid && m_finished; } char * Installer::getPackagePointer() const @@ -181,50 +189,44 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l auto ptr = (Installer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; - auto hdc = BeginPaint(hWnd, &ps); - auto font = CreateFont(25, 10, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, "Segoe UI"); - SelectObject(hdc, font); - - // Background - SelectObject(hdc, GetStockObject(DC_BRUSH)); - SelectObject(hdc, GetStockObject(DC_PEN)); - SetDCBrushColor(hdc, RGB(25, 25, 25)); - SetDCPenColor(hdc, RGB(25, 25, 25)); - SetBkColor(hdc, RGB(25, 25, 25)); - Rectangle(hdc, 0, 0, 800, 500); - - // Footer - SetDCBrushColor(hdc, RGB(75, 75, 75)); - SetDCPenColor(hdc, RGB(75, 75, 75)); - Rectangle(hdc, 0, 450, 800, 500); - - // Steps - constexpr static char* step_labels[] = { "Welcome", "Directory", "Install", "Finish" }; - SetDCBrushColor(hdc, RGB(100, 100, 100)); - SetDCPenColor(hdc, RGB(100, 100, 100)); - Rectangle(hdc, 26, 0, 29, 450); - int vertical_offset = 15; + Graphics graphics(BeginPaint(hWnd, &ps)); + // Draw Background + const LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 800, 500); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + + // Draw Steps + const SolidBrush lineBrush(Color(255,100,100,100)); + graphics.FillRectangle(&lineBrush, 28, 0, 4, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"Directory", L"Install", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; const auto frameIndex = (int)ptr->getCurrentIndex(); for (int x = 0; x < 4; ++x) { // Draw Circle - auto color = x == frameIndex ? RGB(25, 225, 125) : RGB(255, 255, 255); + auto color = x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); if (x == 3 && frameIndex == 4) - color = RGB(225, 25, 25); - SetDCBrushColor(hdc, color); - SetDCPenColor(hdc, color); - SetTextColor(hdc, color); - Ellipse(hdc, 20, vertical_offset + 6, 35, vertical_offset + 21); + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20 ); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20 ); // Draw Text - TextOut(hdc, 50, vertical_offset, step_labels[x], (int)strlen(step_labels[x])); + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); if (x == 2) - vertical_offset = 420; + vertical_offset = 460; else vertical_offset += 50; } - DeleteObject(font); EndPaint(hWnd, &ps); return S_OK; } diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index cf5b44d..2b19303 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -28,6 +28,8 @@ class Installer { // Public Methods /** When called, invalidates the installer, halting it from progressing. */ void invalidate(); + /** Flags the installation as complete. */ + void finish(); /** Displays the screen matching the supplied enumeration. @param newIndex the screen to make visible (makes current screen invisible). */ void showFrame(const FrameEnums & newIndex); @@ -68,7 +70,7 @@ class Installer { // Private Attributes Resource m_archive; std::string m_directory = "", m_packageName = ""; - bool m_valid = true, m_showDirectoryOnClose = true; + bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull; FrameEnums m_currentIndex = WELCOME_FRAME; diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp index 90f05eb..8082d99 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/States/DirectoryState.cpp @@ -2,7 +2,6 @@ #include "WelcomeState.h" #include "InstallState.h" #include "../Installer.h" -#include DirectoryState::DirectoryState(Installer * installer) @@ -23,7 +22,7 @@ void DirectoryState::pressNext() { auto directory = m_installer->getDirectory(); - if (directory == "" || directory == " " || directory.length() < 3 || !std::filesystem::is_directory(directory)) + if (directory == "" || directory == " " || directory.length() < 3) MessageBox( NULL, "Please enter a valid directory before proceeding.", diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp index b82a91e..c0cb182 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/States/FinishState.cpp @@ -10,6 +10,7 @@ void FinishState::enact() m_installer->showFrame(Installer::FrameEnums::FINISH_FRAME); m_installer->showButtons(false, false, true); m_installer->enableButtons(false, false, true); + m_installer->finish(); } void FinishState::pressPrevious() diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 697912b..25f0a06 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -1,8 +1,15 @@ #include "Installer.h" +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) { + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); Installer installer(hInstance); // Main message loop: diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc index c451470029e76c508c80ca90fc72600b5e0bb206..5a04bb28debb4789b202c77bd10bf00ecf66b03c 100644 GIT binary patch literal 2996 zcmchZTWcCo5Xa}Wg?@+A%}WaGWrc{t96L$v*wU|JyLGAFYu;UgK z&sX8QN=`n>2liUI+j;l;Y;5m@_9oIhg8qM`3eTrqo<}^qvY0h^&vq5MM|0x_c>=%hQ(bCR8kF)kq5R z(IUF$?(rCS!*jdFz}2OW3H8yQim)(}ukr{khw=m5UGde2u3Z(8E5+-S`!%~$+gXl1 z=nT)S?ht+r8+T zbwKJUZ(#Cnd61pa%|p>=ws2Aacr&J>*@ zzDDaij=5a4$$LOtF}Qh-&HCJiW?j7xRn)I#Y`jE#4Q!3*BNnZHVS~4+ z-`df8AZ>Z7dYaDI|8eRoTGisqp87>5UCs2S(QN(~($lp|-OiYdzQQUM6Mq}Z1$X5r z+U(Rb&Y9-?nVbI~cu6gMCuVbhu%nH<|IbL){{|zu1T%gOS7w3k<$HCwqgT7w%17TO NL~i?|U9aM@?myc*fnNXs delta 16 YcmdlYzKUyu3)AETZmG>LIL Date: Mon, 8 Apr 2019 19:28:16 -0500 Subject: [PATCH 08/44] More graphics updates Changed gradient directions Made bottom footer blend into central frames Fixed progress bar text (separate text rendering) --- src/nStaller/Frames/DirectoryFrame.cpp | 20 ++++++----------- src/nStaller/Frames/FailFrame.cpp | 20 ++++++----------- src/nStaller/Frames/FinishFrame.cpp | 24 +++++++++------------ src/nStaller/Frames/Frame.h | 4 ---- src/nStaller/Frames/InstallFrame.cpp | 30 +++++++++++++------------- src/nStaller/Frames/InstallFrame.h | 6 +++++- src/nStaller/Frames/WelcomeFrame.cpp | 18 ++++++---------- src/nStaller/Installer.cpp | 12 +++++------ 8 files changed, 56 insertions(+), 78 deletions(-) diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index e88f3b7..b155653 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -33,10 +33,7 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE hInstanc m_wcex.lpszClassName = "DIRECTORY_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - m_width = rc.right - rc.left; - m_height = rc.bottom - rc.top; + m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create directory lookup fields m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); @@ -165,18 +162,16 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + Graphics graphics(BeginPaint(hWnd, &ps)); // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(frame->m_width, frame->m_height), - Color(255, 255, 255, 255), - Color(50, 25, 125, 225) + Point(0, 500), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); @@ -190,12 +185,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l L"Where would you like to install to?", L"Choose a folder by pressing the 'Browse' button.", L"Alternatively, type a specific directory into the box below.", - L"Press the 'Next' button to begin installing . . ." }; + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); - graphics.DrawString(text[3], -1, ®Font, PointF{ 10, 420 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp index 120391c..e886e69 100644 --- a/src/nStaller/Frames/FailFrame.cpp +++ b/src/nStaller/Frames/FailFrame.cpp @@ -31,10 +31,7 @@ FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & m_wcex.lpszClassName = "FAIL_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - m_width = rc.right - rc.left; - m_height = rc.bottom - rc.top; + m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create error log m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); @@ -50,29 +47,26 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(frame->m_width, frame->m_height), - Color(255, 255, 255, 255), - Color(50, 225, 25, 75) + Point(0, 500), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Press the 'Cancel' button to close . . .", -1, ®Font, PointF{ 10, 420 }, &blackBrush); - + EndPaint(hWnd, &ps); return S_OK; } diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index 4d773fe..f1e6fca 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -30,13 +30,10 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const m_wcex.lpszClassName = "FINISH_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - m_width = rc.right - rc.left; - m_height = rc.bottom - rc.top; + m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); // Create checkbox - m_checkbox = CreateWindow("Button", "Show installation directory on close.", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, rc.right - rc.left -20, 25, m_hwnd, (HMENU)1, hInstance, NULL); + m_checkbox = CreateWindow("Button", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_BORDER | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 15, 15, m_hwnd, (HMENU)1, hInstance, NULL); SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)m_openDirOnClose); CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); setVisible(false); @@ -47,17 +44,15 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - + // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(frame->m_width, frame->m_height), - Color(255, 255, 255, 255), - Color(50, 25, 255, 125) + Point(0, 500), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); @@ -67,9 +62,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Press the 'Cancel' button to close . . .", -1, ®Font, PointF{ 10, 420 }, &blackBrush); - + graphics.DrawString(L"Show installation directory on close.", -1, ®Font, PointF{ 30, 147 }, &blackBrush); + EndPaint(hWnd, &ps); return S_OK; } diff --git a/src/nStaller/Frames/Frame.h b/src/nStaller/Frames/Frame.h index 90a3bbf..9c7309b 100644 --- a/src/nStaller/Frames/Frame.h +++ b/src/nStaller/Frames/Frame.h @@ -23,10 +23,6 @@ class Frame { } - // Public Attributes - int m_width = 0, m_height = 0; - - protected: // Private Attributes WNDCLASSEX m_wcex; diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index 601a498..50dc2cd 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -11,7 +11,6 @@ InstallFrame::~InstallFrame() DestroyWindow(m_hwnd); DestroyWindow(m_hwndLog); DestroyWindow(m_hwndPrgsBar); - DestroyWindow(m_hwndPrgsText); TaskLogger::RemoveCallback_TextAdded(m_logIndex); TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); } @@ -33,15 +32,12 @@ InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.lpszClassName = "INSTALL_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("INSTALL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("INSTALL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - m_width = rc.right - rc.left; - m_height = rc.bottom - rc.top; // Create log box and progress bar m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left)-20, (rc.bottom - rc.top)-100, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 20, 25, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsText = CreateWindowEx(WS_EX_TRANSPARENT, "static", "0%", WS_CHILD | WS_VISIBLE | SS_CENTER, ((rc.right - rc.left) / 2)-20, 0, 40, 25, m_hwndPrgsBar, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); @@ -49,8 +45,9 @@ InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const R m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); - std::string s = std::to_string( position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f)))+ "%"; - SetWindowText(m_hwndPrgsText, s.c_str()); + m_progress = std::to_wstring( position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f)))+ L"%"; + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); }); setVisible(false); @@ -61,25 +58,28 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - + auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(frame->m_width, frame->m_height), - Color(255, 255, 255, 255), - Color(50, 25, 125, 225) + Point(0, 500), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h index b52c0a5..ed858a1 100644 --- a/src/nStaller/Frames/InstallFrame.h +++ b/src/nStaller/Frames/InstallFrame.h @@ -14,9 +14,13 @@ class InstallFrame : public Frame { InstallFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); + // Public Attributes + std::wstring m_progress = L"0%"; + + private: // Private Attributes - HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_hwndPrgsText = nullptr; + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; size_t m_logIndex = 0ull, m_taskIndex = 0ull; }; diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index ef3c0d2..248d3a8 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -26,10 +26,7 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.lpszClassName = "WELCOME_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_DLGFRAME, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - m_width = rc.right - rc.left; - m_height = rc.bottom - rc.top; + m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); setVisible(false); } @@ -38,17 +35,15 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - auto frame = (Frame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(frame->m_width, frame->m_height), - Color(255, 255, 255, 255), - Color(50, 25, 125, 225) + Point(0, 500), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, frame->m_width, frame->m_height); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); @@ -64,13 +59,12 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l L"This custom installer was generated using nSuite", L"Source-code can be found at:", L"https://github.com/Yattabyte/nSuite", - L"Press the 'Next' button to continue . . ." }; + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); graphics.DrawString(text[3], -1, ®UnderFont, PointF{ 35, 130 }, &blueBrush); - graphics.DrawString(text[4], -1, ®Font, PointF{ 10, 420 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 7b77dad..7ee33ee 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -124,7 +124,7 @@ void Installer::setState(State * state) delete m_state; m_state = state; m_state->enact(); - RECT rc = { 0, 0, 160, 450 }; + RECT rc = { 0, 0, 160, 500 }; RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); } } @@ -162,7 +162,7 @@ void Installer::updateButtons(const WORD btnHandle) m_state->pressNext(); else if (btnHandle == LOWORD(m_exitBtn)) m_state->pressClose(); - RECT rc = { 0, 0, 160, 450 }; + RECT rc = { 0, 0, 160, 500 }; RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); } @@ -191,18 +191,17 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); // Draw Background - const LinearGradientBrush backgroundGradient( + const LinearGradientBrush backgroundGradient1( Point(0, 0), Point(0, 500), Color(255, 25, 25, 25), Color(255, 75, 75, 75) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 800, 500); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); // Draw Steps const SolidBrush lineBrush(Color(255,100,100,100)); - graphics.FillRectangle(&lineBrush, 28, 0, 4, 500); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); constexpr static wchar_t* step_labels[] = { L"Welcome", L"Directory", L"Install", L"Finish" }; FontFamily fontFamily(L"Segoe UI"); Font font(&fontFamily, 15, FontStyleBold, UnitPixel); @@ -215,6 +214,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l color = Color(255, 225, 25, 75); const SolidBrush brush(color); Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20 ); graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20 ); From 2623d61ee759babda8290347d8a8c913acc9de6a Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Thu, 11 Apr 2019 15:49:51 -0500 Subject: [PATCH 09/44] Added space information On the directory screen, the disk space available, required, capacity are all updated based on the path chosen, and formatted to be readable. --- src/nStaller/Frames/DirectoryFrame.cpp | 72 ++++++++++++++++++++------ src/nStaller/Frames/DirectoryFrame.h | 6 ++- src/nStaller/Frames/FailFrame.cpp | 4 +- src/nStaller/Frames/FinishFrame.cpp | 4 +- src/nStaller/Frames/InstallFrame.cpp | 4 +- src/nStaller/Frames/WelcomeFrame.cpp | 18 +++---- src/nStaller/Installer.cpp | 7 +-- src/nStaller/Installer.h | 2 +- 8 files changed, 78 insertions(+), 39 deletions(-) diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index b155653..fadca5a 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -1,8 +1,9 @@ #include "DirectoryFrame.h" -#include -#include +#include #include #include +#include +#include static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); @@ -15,10 +16,11 @@ DirectoryFrame::~DirectoryFrame() DestroyWindow(m_browseButton); } -DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE hInstance, const HWND parent, const RECT & rc) +DirectoryFrame::DirectoryFrame(std::string * directory, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class m_directory = directory; + m_requiredSize = requiredSize; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -34,12 +36,14 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const HINSTANCE hInstanc m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); // Create directory lookup fields - m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory->c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); SetWindowLongPtr(m_browseButton, GWLP_USERDATA, (LONG_PTR)this); SetWindowLongPtr(m_directoryField, GWLP_USERDATA, (LONG_PTR)m_directory); + setDirectory(*directory); setVisible(false); } @@ -49,6 +53,20 @@ void DirectoryFrame::setDirectory(const std::string & dir) SetWindowText(m_directoryField, dir.c_str()); } +void DirectoryFrame::getSizes(size_t & capacity, size_t & available, size_t & required) const +{ + try { + const auto spaceInfo = std::filesystem::space(std::filesystem::path(*m_directory).root_path()); + capacity = spaceInfo.capacity; + available = spaceInfo.available; + } + catch (std::filesystem::filesystem_error &) { + capacity = 0ull; + available = 0ull; + } + required = m_requiredSize; +} + static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) { /** File Dialog Event Handler */ @@ -162,16 +180,21 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l { if (message == WM_PAINT) { PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); + Graphics graphics(BeginPaint(hWnd, &ps)); + auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); // Draw Background + SolidBrush solidWhiteBackground( + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 450); LinearGradientBrush backgroundGradient( Point(0, 0), - Point(0, 500), + Point(0, 450), Color(50, 25, 125, 225), Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); @@ -181,15 +204,30 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text - constexpr static wchar_t* text[] = { - L"Where would you like to install to?", - L"Choose a folder by pressing the 'Browse' button.", - L"Alternatively, type a specific directory into the box below.", - }; graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); + + constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { + auto remainingSize = (double)size; + constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB" }; + int i = 0; + while (remainingSize > 1024.00) { + remainingSize /= 1024.00; + ++i; + } + std::wstringstream stream; + stream << std::fixed << std::setprecision(2) << remainingSize; + return stream.str() + units[i] + L"\t(" + std::to_wstring(size) + L" bytes )"; + }; + size_t capacity, available, required; + ptr->getSizes(capacity, available, required); + graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); + graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(capacity)).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); + graphics.DrawString((L" Available:\t\t\t" + readableFileSize(available)).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); + graphics.DrawString((L" Required:\t\t\t" + readableFileSize(required)).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); + EndPaint(hWnd, &ps); return S_OK; @@ -204,6 +242,8 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l if (SUCCEEDED(OpenFileDialog(directory))) { if (directory != "" && directory.length() > 2ull) { dirFrame->setDirectory(directory); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); return S_OK; } } @@ -215,6 +255,8 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l std::vector data(GetWindowTextLength(controlHandle) + 1ull); GetWindowTextA(controlHandle, &data[0], (int)data.size()); *dirPtr = std::string(data.data()); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); return S_OK; } } diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index 641aa58..a69e233 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -11,18 +11,20 @@ class DirectoryFrame : public Frame { public: // Public (de)Constructors ~DirectoryFrame(); - DirectoryFrame(std::string * directory, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + DirectoryFrame(std::string * directory, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Methods /** Set the directory to be used by this UI element. @param dir the directory path. */ void setDirectory(const std::string & dir); - + /***/ + void getSizes(size_t & capacity, size_t & available, size_t & required) const; private: // Private Attributes std::string * m_directory = nullptr; + size_t m_requiredSize = 0ull; HWND m_directoryField = nullptr, m_browseButton = nullptr; }; diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp index e886e69..b189b4e 100644 --- a/src/nStaller/Frames/FailFrame.cpp +++ b/src/nStaller/Frames/FailFrame.cpp @@ -51,11 +51,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(0, 500), + Point(0, 450), Color(50, 225, 25, 75), Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index f1e6fca..08d4bdb 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -48,11 +48,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(0, 500), + Point(0, 450), Color(50, 25, 255, 125), Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index 50dc2cd..9a2608a 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -63,11 +63,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(0, 500), + Point(0, 450), Color(50, 25, 125, 225), Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index 248d3a8..c896ac9 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -39,11 +39,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Background LinearGradientBrush backgroundGradient( Point(0, 0), - Point(0, 500), + Point(0, 450), Color(50, 25, 125, 225), Color(255, 255, 255, 255) ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 500); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); @@ -54,17 +54,11 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text - constexpr static wchar_t* text[] = { - L"Welcome", - L"This custom installer was generated using nSuite", - L"Source-code can be found at:", - L"https://github.com/Yattabyte/nSuite", - }; graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(text[0], -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(text[1], -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(text[2], -1, ®Font, PointF{ 10, 115 }, &blackBrush); - graphics.DrawString(text[3], -1, ®UnderFont, PointF{ 35, 130 }, &blueBrush); + graphics.DrawString(L"Welcome", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"This custom installer was generated using nSuite", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(L"Source-code can be found at:", -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 35, 130 }, &blueBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 7ee33ee..df4ee81 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -37,6 +37,7 @@ Installer::Installer(const HINSTANCE hInstance) m_directory += "\\" + m_packageName; m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); + m_maxSize = *reinterpret_cast(m_packagePtr); } // Create window class WNDCLASSEX wcex; @@ -82,7 +83,7 @@ Installer::Installer(const HINSTANCE hInstance) // The portions of the screen that change based on input m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, hInstance, m_window, { 170,0,800,450 }); + m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, m_maxSize, hInstance, m_window, { 170,0,800,450 }); m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); m_frames[FINISH_FRAME] = new FinishFrame(&m_showDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); @@ -209,7 +210,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l const auto frameIndex = (int)ptr->getCurrentIndex(); for (int x = 0; x < 4; ++x) { // Draw Circle - auto color = x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); if (x == 3 && frameIndex == 4) color = Color(255, 225, 25, 75); const SolidBrush brush(color); @@ -239,4 +240,4 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l } } return DefWindowProc(hWnd, message, wParam, lParam); -} +} \ No newline at end of file diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 2b19303..460f59e 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -72,7 +72,7 @@ class Installer { std::string m_directory = "", m_packageName = ""; bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; char * m_packagePtr = nullptr; - size_t m_packageSize = 0ull; + size_t m_packageSize = 0ull, m_maxSize = 0ull; FrameEnums m_currentIndex = WELCOME_FRAME; Frame * m_frames[FRAME_COUNT]; State * m_state = nullptr; From caba4e69986cf6c164be49052872a3f4c915ce1d Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Thu, 11 Apr 2019 19:46:14 -0500 Subject: [PATCH 10/44] Moved watermark to footer Moved watermark to footer --- src/nStaller/Frames/WelcomeFrame.cpp | 7 +------ src/nStaller/Installer.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index c896ac9..37de34a 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -48,17 +48,12 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Welcome", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"This custom installer was generated using nSuite", -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(L"Source-code can be found at:", -1, ®Font, PointF{ 10, 115 }, &blackBrush); - graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 35, 130 }, &blueBrush); + EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index df4ee81..1753f1e 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -228,6 +228,14 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l vertical_offset += 50; } + // Draw -watermark- + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + graphics.DrawString(L"This custom installer was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 180, 475 }, &blueishBrush); + EndPaint(hWnd, &ps); return S_OK; } From 71b552b425033b0e69822048404b59a2bd57b8f3 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Thu, 11 Apr 2019 22:03:02 -0500 Subject: [PATCH 11/44] Starting manifest file prerequisites - Added resource files to the installer for the name, version, description, as entries in a string table The plan is to get nSuite to parse a manifest file within the installation folder, and update these resources in the installer exe as well. - Updated setDirectory logic, such that when set from the file dialog, it always appends the package directory to the end. Can manually be changed still using the textbox. --- src/nStaller/Frames/DirectoryFrame.cpp | 10 ++++++-- src/nStaller/Frames/DirectoryFrame.h | 11 +++++--- src/nStaller/Frames/WelcomeFrame.cpp | 15 ++++++++--- src/nStaller/Installer.cpp | 5 ++-- src/nStaller/nStaller.rc | Bin 2996 -> 3220 bytes src/resource.h | 34 ++++++++++++++++++++----- 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index fadca5a..d3341ea 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -16,10 +16,11 @@ DirectoryFrame::~DirectoryFrame() DestroyWindow(m_browseButton); } -DirectoryFrame::DirectoryFrame(std::string * directory, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc) +DirectoryFrame::DirectoryFrame(std::string * directory, const std::string & packageName, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class m_directory = directory; + m_packageName = packageName; m_requiredSize = requiredSize; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); @@ -50,7 +51,12 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const size_t & requiredS void DirectoryFrame::setDirectory(const std::string & dir) { *m_directory = dir; - SetWindowText(m_directoryField, dir.c_str()); + // Ensure last character is a backslash + if (dir.size() && dir[dir.size() - 1ull] != '\\') + *m_directory += '\\' + m_packageName; + else + *m_directory += m_packageName; + SetWindowText(m_directoryField, m_directory->c_str()); } void DirectoryFrame::getSizes(size_t & capacity, size_t & available, size_t & required) const diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index a69e233..4ea0f34 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -11,19 +11,22 @@ class DirectoryFrame : public Frame { public: // Public (de)Constructors ~DirectoryFrame(); - DirectoryFrame(std::string * directory, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + DirectoryFrame(std::string * directory, const std::string & packageName, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Methods /** Set the directory to be used by this UI element. - @param dir the directory path. */ + @param dir the directory path. */ void setDirectory(const std::string & dir); - /***/ + /** Retrieve the drive capacity, availabilty, and required size for this package. + @param capacity reference updated with the size in bytes of the drive found. + @param available reference updated with the size in bytes remaining on the drive found. + @param required reference updated with the size in bytes of the package to be installed. */ void getSizes(size_t & capacity, size_t & available, size_t & required) const; private: // Private Attributes - std::string * m_directory = nullptr; + std::string * m_directory = nullptr, m_packageName = ""; size_t m_requiredSize = 0ull; HWND m_directoryField = nullptr, m_browseButton = nullptr; }; diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index 37de34a..cc5dc3b 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -1,7 +1,11 @@ #include "WelcomeFrame.h" +#include "Resource.h" static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static std::wstring name = Resource::WString(IDS_PRODUCT_NAME); +static std::wstring version = Resource::WString(IDS_PRODUCT_VERSION); +static std::wstring description = Resource::WString(IDS_PRODUCT_DESCRIPTION); WelcomeFrame::~WelcomeFrame() { @@ -26,7 +30,7 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.lpszClassName = "WELCOME_FRAME"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); setVisible(false); } @@ -48,12 +52,17 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Welcome", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - + graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = name + L" " + version; + if (name.empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(description.c_str(), -1, ®Font, PointF{ 10, 115 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 1753f1e..8ed7f96 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -34,7 +34,6 @@ Installer::Installer(const HINSTANCE hInstance) else { const auto folderSize = *reinterpret_cast(m_archive.getPtr()); m_packageName = std::string(reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)))), folderSize); - m_directory += "\\" + m_packageName; m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); m_maxSize = *reinterpret_cast(m_packagePtr); @@ -59,7 +58,7 @@ Installer::Installer(const HINSTANCE hInstance) } else { m_window = CreateWindow( - "nStaller", std::string(m_packageName + " Installer").c_str(), + "nStaller", (Resource::String(IDS_PRODUCT_NAME) + " Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, @@ -83,7 +82,7 @@ Installer::Installer(const HINSTANCE hInstance) // The portions of the screen that change based on input m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, m_maxSize, hInstance, m_window, { 170,0,800,450 }); + m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, m_packageName, m_maxSize, hInstance, m_window, { 170,0,800,450 }); m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); m_frames[FINISH_FRAME] = new FinishFrame(&m_showDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc index 5a04bb28debb4789b202c77bd10bf00ecf66b03c..77b4d804e6d4a5242ff962d313520c5a3b344ce2 100644 GIT binary patch delta 215 zcmdlYK1Fha3zKLtLkL3S RIsnrMSD=wDo2{8DIRT#M9+Ch6 delta 12 TcmbOtxkY?~3)5y5<^oOt98Uwn diff --git a/src/resource.h b/src/resource.h index faaf34c..0d32990 100644 --- a/src/resource.h +++ b/src/resource.h @@ -3,15 +3,17 @@ #define RESOURCE_H // Used for icons -#define IDI_ICON1 101 -// Used by installer.rc -#define IDR_ARCHIVE 102 +#define IDI_ICON1 101 // Used by installerMaker.rc -#define IDR_INSTALLER 103 -#define VS_VERSION_INFO 1 +#define IDR_INSTALLER 102 +// Used by installer.rc +#define IDR_ARCHIVE 103 +#define IDS_PRODUCT_NAME 104 +#define IDS_PRODUCT_VERSION 105 +#define IDS_PRODUCT_DESCRIPTION 106 #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_RESOURCE_VALUE 107 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 @@ -56,6 +58,26 @@ class Resource { inline bool exists() const { return (m_hResource && m_hMemory && m_ptr) && (m_size > 0ull); } + /** Retrieve a wide-string resource from a string table.*/ + inline static std::wstring WString(const int & resourceID, const HMODULE & moduleHandle = nullptr) { + if (MAKEINTRESOURCE(resourceID)) { + wchar_t * buffer = nullptr; + if (auto length = LoadStringW(moduleHandle, resourceID, (LPWSTR)(&buffer), 0)) + return std::wstring(buffer, length); + } + return std::wstring(); + } + /** Retrieve a string resource from a string table.*/ + inline static std::string String(const int & resourceID, const HMODULE & moduleHandle = nullptr) { + const auto wstr = WString(resourceID, moduleHandle); + if (!wstr.empty()) + if (const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL)) { + std::string strTo(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); + return strTo; + } + return std::string(); + } private: From 74c5865620208f1de2daa8385f5db11458823929 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 13 Apr 2019 14:56:40 -0500 Subject: [PATCH 12/44] Manifest Implementation - The installer now has an embedded manifest file that it uses for populating many strings, used in the GUI. I tried to make this as easy to maintain as possible, using a map for variable names/strings. - nSuite now checks for the existance of a "manifest.nman" file within the root installation directory, and if so, will embed that resource into the installer as well. --- src/nStaller/Frames/DirectoryFrame.cpp | 94 ++++++++------------ src/nStaller/Frames/DirectoryFrame.h | 21 ++--- src/nStaller/Frames/FailFrame.cpp | 6 +- src/nStaller/Frames/FailFrame.h | 8 +- src/nStaller/Frames/FinishFrame.cpp | 19 ++-- src/nStaller/Frames/FinishFrame.h | 9 +- src/nStaller/Frames/InstallFrame.cpp | 6 +- src/nStaller/Frames/InstallFrame.h | 11 ++- src/nStaller/Frames/WelcomeFrame.cpp | 15 ++-- src/nStaller/Frames/WelcomeFrame.h | 8 +- src/nStaller/Installer.cpp | 88 +++++++++++++++--- src/nStaller/Installer.h | 37 +++++++- src/nStaller/States/InstallState.cpp | 2 +- src/nStaller/{archive.dat => archive.npack} | 0 src/nStaller/manifest.nman | 0 src/nStaller/nStaller.rc | Bin 3220 -> 3100 bytes src/nSuite/Commands/InstallerCommand.cpp | 17 ++++ src/resource.h | 6 +- 18 files changed, 218 insertions(+), 129 deletions(-) rename src/nStaller/{archive.dat => archive.npack} (100%) create mode 100644 src/nStaller/manifest.nman diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index d3341ea..dbaf075 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -1,5 +1,6 @@ #include "DirectoryFrame.h" -#include +#include "../Installer.h" +#include #include #include #include @@ -8,6 +9,17 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static std::string directory_and_package(const std::string & dir, const std::string & pack) +{ + std::string directory = dir; + // Ensure last character is a backslash + if (dir.size() && dir[dir.size() - 1ull] != '\\') + directory += '\\' + pack; + else + directory += pack; + return directory; +} + DirectoryFrame::~DirectoryFrame() { UnregisterClass("DIRECTORY_FRAME", m_hinstance); @@ -16,12 +28,10 @@ DirectoryFrame::~DirectoryFrame() DestroyWindow(m_browseButton); } -DirectoryFrame::DirectoryFrame(std::string * directory, const std::string & packageName, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc) +DirectoryFrame::DirectoryFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class - m_directory = directory; - m_packageName = packageName; - m_requiredSize = requiredSize; + m_installer = installer; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -40,39 +50,11 @@ DirectoryFrame::DirectoryFrame(std::string * directory, const std::string & pack SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); // Create directory lookup fields - m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory_and_package(m_installer->getDirectory(), m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); - SetWindowLongPtr(m_browseButton, GWLP_USERDATA, (LONG_PTR)this); - SetWindowLongPtr(m_directoryField, GWLP_USERDATA, (LONG_PTR)m_directory); - setDirectory(*directory); setVisible(false); } -void DirectoryFrame::setDirectory(const std::string & dir) -{ - *m_directory = dir; - // Ensure last character is a backslash - if (dir.size() && dir[dir.size() - 1ull] != '\\') - *m_directory += '\\' + m_packageName; - else - *m_directory += m_packageName; - SetWindowText(m_directoryField, m_directory->c_str()); -} - -void DirectoryFrame::getSizes(size_t & capacity, size_t & available, size_t & required) const -{ - try { - const auto spaceInfo = std::filesystem::space(std::filesystem::path(*m_directory).root_path()); - capacity = spaceInfo.capacity; - available = spaceInfo.available; - } - catch (std::filesystem::filesystem_error &) { - capacity = 0ull; - available = 0ull; - } - required = m_requiredSize; -} - static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) { /** File Dialog Event Handler */ @@ -184,10 +166,10 @@ HRESULT OpenFileDialog(std::string & directory) static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); // Draw Background SolidBrush solidWhiteBackground( @@ -227,12 +209,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l stream << std::fixed << std::setprecision(2) << remainingSize; return stream.str() + units[i] + L"\t(" + std::to_wstring(size) + L" bytes )"; }; - size_t capacity, available, required; - ptr->getSizes(capacity, available, required); graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); - graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(capacity)).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); - graphics.DrawString((L" Available:\t\t\t" + readableFileSize(available)).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); - graphics.DrawString((L" Required:\t\t\t" + readableFileSize(required)).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); + graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); + graphics.DrawString((L" Available:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); + graphics.DrawString((L" Required:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); EndPaint(hWnd, &ps); @@ -242,29 +222,25 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l const auto notification = HIWORD(wParam); auto controlHandle = HWND(lParam); if (notification == BN_CLICKED) { - auto dirFrame = (DirectoryFrame*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); - if (dirFrame) { - std::string directory(""); - if (SUCCEEDED(OpenFileDialog(directory))) { - if (directory != "" && directory.length() > 2ull) { - dirFrame->setDirectory(directory); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; - } + std::string directory(""); + if (SUCCEEDED(OpenFileDialog(directory))) { + if (directory != "" && directory.length() > 2ull) { + directory = directory_and_package(directory, ptr->m_installer->getPackageName()); + ptr->m_installer->setDirectory(directory); + SetWindowTextA(ptr->m_directoryField, directory.c_str()); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; } } } else if (notification == EN_CHANGE) { - auto dirPtr = (std::string*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); - if (dirPtr) { - std::vector data(GetWindowTextLength(controlHandle) + 1ull); - GetWindowTextA(controlHandle, &data[0], (int)data.size()); - *dirPtr = std::string(data.data()); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; - } + std::vector data(GetWindowTextLength(controlHandle) + 1ull); + GetWindowTextA(controlHandle, &data[0], (int)data.size()); + ptr->m_installer->setDirectory(std::string(data.data())); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; } } return DefWindowProc(hWnd, message, wParam, lParam); diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index 4ea0f34..0905d93 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -6,28 +6,17 @@ #include +class Installer; + /** Custom frame class, representing the installer 'directory choosing' screen. */ class DirectoryFrame : public Frame { public: // Public (de)Constructors ~DirectoryFrame(); - DirectoryFrame(std::string * directory, const std::string & packageName, const size_t & requiredSize, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Methods - /** Set the directory to be used by this UI element. - @param dir the directory path. */ - void setDirectory(const std::string & dir); - /** Retrieve the drive capacity, availabilty, and required size for this package. - @param capacity reference updated with the size in bytes of the drive found. - @param available reference updated with the size in bytes remaining on the drive found. - @param required reference updated with the size in bytes of the package to be installed. */ - void getSizes(size_t & capacity, size_t & available, size_t & required) const; + DirectoryFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); -private: - // Private Attributes - std::string * m_directory = nullptr, m_packageName = ""; - size_t m_requiredSize = 0ull; + // Public Attributes + Installer * m_installer = nullptr; HWND m_directoryField = nullptr, m_browseButton = nullptr; }; diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp index b189b4e..a25b034 100644 --- a/src/nStaller/Frames/FailFrame.cpp +++ b/src/nStaller/Frames/FailFrame.cpp @@ -1,5 +1,6 @@ #include "FailFrame.h" #include "TaskLogger.h" +#include "../Installer.h" #include #include @@ -14,9 +15,10 @@ FailFrame::~FailFrame() TaskLogger::RemoveCallback_TextAdded(m_logIndex); } -FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) +FailFrame::FailFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class + m_installer = installer; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -32,6 +34,7 @@ FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); // Create error log m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); @@ -44,6 +47,7 @@ FailFrame::FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (FailFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); diff --git a/src/nStaller/Frames/FailFrame.h b/src/nStaller/Frames/FailFrame.h index 08fdbd3..d02cfff 100644 --- a/src/nStaller/Frames/FailFrame.h +++ b/src/nStaller/Frames/FailFrame.h @@ -5,16 +5,18 @@ #include "Frame.h" +class Installer; + /** Custom frame class, representing the installer 'failure' screen. */ class FailFrame : public Frame { public: // Public (de)Constructors ~FailFrame(); - FailFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); + FailFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); -private: - // Private Attributes + // Public Attributes + Installer * m_installer = nullptr; HWND m_hwndLog = nullptr; size_t m_logIndex = 0ull; }; diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index 08d4bdb..f8b8a4b 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -1,4 +1,5 @@ #include "FinishFrame.h" +#include "../Installer.h" #include #include @@ -12,10 +13,10 @@ FinishFrame::~FinishFrame() DestroyWindow(m_checkbox); } -FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const HWND parent, const RECT & rc) +FinishFrame::FinishFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class - m_openDirOnClose = openDirOnClose; + m_installer = installer; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -31,16 +32,17 @@ FinishFrame::FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); // Create checkbox m_checkbox = CreateWindow("Button", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_BORDER | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 15, 15, m_hwnd, (HMENU)1, hInstance, NULL); - SetWindowLongPtr(m_checkbox, GWLP_USERDATA, (LONG_PTR)m_openDirOnClose); - CheckDlgButton(m_hwnd, 1, *openDirOnClose ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(m_hwnd, 1, BST_CHECKED); setVisible(false); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (FinishFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); @@ -73,12 +75,9 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l const auto notification = HIWORD(wParam); auto controlHandle = HWND(lParam); if (notification == BN_CLICKED) { - auto oOCPtr = (bool*)GetWindowLongPtr(controlHandle, GWLP_USERDATA); - if (oOCPtr) { - BOOL checked = IsDlgButtonChecked(hWnd, 1); - *oOCPtr = checked; - return S_OK; - } + BOOL checked = IsDlgButtonChecked(hWnd, 1); + ptr->m_installer->showDirectoryOnClose(checked); + return S_OK; } } return DefWindowProc(hWnd, message, wParam, lParam); diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h index 47eba31..24b0449 100644 --- a/src/nStaller/Frames/FinishFrame.h +++ b/src/nStaller/Frames/FinishFrame.h @@ -5,17 +5,18 @@ #include "Frame.h" +class Installer; + /** Custom frame class, representing the installer 'finish' screen. */ class FinishFrame : public Frame { public: // Public (de)Constructors ~FinishFrame(); - FinishFrame(bool * openDirOnClose, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + FinishFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); -private: - // Private Attributes - bool * m_openDirOnClose = nullptr; + // Public Attributes + Installer * m_installer = nullptr; HWND m_checkbox = nullptr; }; diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp index 9a2608a..06ff6ba 100644 --- a/src/nStaller/Frames/InstallFrame.cpp +++ b/src/nStaller/Frames/InstallFrame.cpp @@ -1,5 +1,6 @@ #include "InstallFrame.h" #include "TaskLogger.h" +#include "../Installer.h" #include @@ -15,9 +16,10 @@ InstallFrame::~InstallFrame() TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); } -InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) +InstallFrame::InstallFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class + m_installer = installer; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -55,10 +57,10 @@ InstallFrame::InstallFrame(const HINSTANCE hInstance, const HWND parent, const R static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); - auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); // Draw Background LinearGradientBrush backgroundGradient( diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h index ed858a1..12fc485 100644 --- a/src/nStaller/Frames/InstallFrame.h +++ b/src/nStaller/Frames/InstallFrame.h @@ -6,22 +6,21 @@ #include +class Installer; + /** Custom frame class, representing the installer 'install' screen. */ class InstallFrame : public Frame { public: // Public (de)Constructors ~InstallFrame(); - InstallFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); + InstallFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Attributes - std::wstring m_progress = L"0%"; - - -private: - // Private Attributes + Installer * m_installer = nullptr; HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; size_t m_logIndex = 0ull, m_taskIndex = 0ull; + std::wstring m_progress = L"0%"; }; #endif // INSTALLFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index cc5dc3b..d29cad4 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -1,11 +1,9 @@ #include "WelcomeFrame.h" #include "Resource.h" +#include "../Installer.h" static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -static std::wstring name = Resource::WString(IDS_PRODUCT_NAME); -static std::wstring version = Resource::WString(IDS_PRODUCT_VERSION); -static std::wstring description = Resource::WString(IDS_PRODUCT_DESCRIPTION); WelcomeFrame::~WelcomeFrame() { @@ -13,9 +11,10 @@ WelcomeFrame::~WelcomeFrame() DestroyWindow(m_hwnd); } -WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc) +WelcomeFrame::WelcomeFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) { // Create window class + m_installer = installer; m_hinstance = hInstance; m_wcex.cbSize = sizeof(WNDCLASSEX); m_wcex.style = CS_HREDRAW | CS_VREDRAW; @@ -31,11 +30,13 @@ WelcomeFrame::WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const R m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + auto ptr = (WelcomeFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); @@ -59,10 +60,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - auto nameVer = name + L" " + version; - if (name.empty()) nameVer = L"it's contents"; + auto nameVer = ptr->m_installer->m_name + L" " + ptr->m_installer->m_version; + if (ptr->m_installer->m_name.empty()) nameVer = L"it's contents"; graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(description.c_str(), -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString(ptr->m_installer->m_description.c_str(), -1, ®Font, PointF{ 10, 115 }, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Frames/WelcomeFrame.h b/src/nStaller/Frames/WelcomeFrame.h index f84054d..cf8fcc1 100644 --- a/src/nStaller/Frames/WelcomeFrame.h +++ b/src/nStaller/Frames/WelcomeFrame.h @@ -5,12 +5,18 @@ #include "Frame.h" +class Installer; + /** Custom frame class, representing the installer 'welcome' screen. */ class WelcomeFrame : public Frame { public: // Public (de)Constructors ~WelcomeFrame(); - WelcomeFrame(const HINSTANCE hInstance, const HWND parent, const RECT & rc); + WelcomeFrame(Installer * installer, HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Attributes + Installer * m_installer = nullptr; }; #endif // WELCOMEFRAME_H \ No newline at end of file diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 8ed7f96..fae9b75 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -1,7 +1,9 @@ #include "Installer.h" #include "Common.h" #include "TaskLogger.h" +#include #include +#include // Starting State #include "States/WelcomeState.h" @@ -17,14 +19,38 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -Installer::Installer(const HINSTANCE hInstance) - : m_archive(IDR_ARCHIVE, "ARCHIVE") +Installer::Installer() + : m_archive(IDR_ARCHIVE, "ARCHIVE"), m_manifest(IDR_MANIFEST, "MANIFEST") +{ + // Process manifest + if (m_manifest.exists()) { + struct compare_string { bool operator()(const wchar_t * a, const wchar_t * b) const { return wcscmp(a, b) < 0; } }; + std::map valueMap = { + // Add all manifest string values to keep track of here + { L"-name", &m_name }, + { L"-version", &m_version }, + { L"-description", &m_description } + }; + + // Create a string stream of the manifest file + std::wstringstream ss; + ss << reinterpret_cast(m_manifest.getPtr()); + + // Cycle through every line, matching the attribute name with the attribute value + std::wstring attrib, value; + while (ss >> attrib && ss >> std::quoted(value)) + if (valueMap.find(attrib.c_str()) != valueMap.end()) + *(valueMap.at(attrib.c_str())) = value; // update the value found in the map + } +} + +Installer::Installer(const HINSTANCE hInstance) : Installer() { bool success = true; // Get user's program files directory TCHAR pf[MAX_PATH]; SHGetSpecialFolderPath(0, pf, CSIDL_PROGRAM_FILES, FALSE); - m_directory = std::string(pf); + setDirectory(std::string(pf)); // Check archive integrity if (!m_archive.exists()) { @@ -57,8 +83,8 @@ Installer::Installer(const HINSTANCE hInstance) success = false; } else { - m_window = CreateWindow( - "nStaller", (Resource::String(IDS_PRODUCT_NAME) + " Installer").c_str(), + m_window = CreateWindowW( + L"nStaller", (m_name + L" Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, @@ -81,11 +107,11 @@ Installer::Installer(const HINSTANCE hInstance) SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); // The portions of the screen that change based on input - m_frames[WELCOME_FRAME] = new WelcomeFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[DIRECTORY_FRAME] = new DirectoryFrame(&m_directory, m_packageName, m_maxSize, hInstance, m_window, { 170,0,800,450 }); - m_frames[INSTALL_FRAME] = new InstallFrame(hInstance, m_window, { 170,0,800,450 }); - m_frames[FINISH_FRAME] = new FinishFrame(&m_showDirectoryOnClose, hInstance, m_window, { 170,0,800,450 }); - m_frames[FAIL_FRAME] = new FailFrame(hInstance, m_window, { 170,0,800,450 }); + m_frames[WELCOME_FRAME] = new WelcomeFrame(this, hInstance, m_window, { 170,0,800,450 }); + m_frames[DIRECTORY_FRAME] = new DirectoryFrame(this, hInstance, m_window, { 170,0,800,450 }); + m_frames[INSTALL_FRAME] = new InstallFrame(this, hInstance, m_window, { 170,0,800,450 }); + m_frames[FINISH_FRAME] = new FinishFrame(this, hInstance, m_window, { 170,0,800,450 }); + m_frames[FAIL_FRAME] = new FailFrame(this, hInstance, m_window, { 170,0,800,450 }); setState(new WelcomeState(this)); } @@ -139,21 +165,61 @@ std::string Installer::getDirectory() const return m_directory; } +void Installer::setDirectory(const std::string & directory) +{ + m_directory = directory; + + try { + const auto spaceInfo = std::filesystem::space(std::filesystem::path(getDirectory()).root_path()); + m_capacity = spaceInfo.capacity; + m_available = spaceInfo.available; + } + catch (std::filesystem::filesystem_error &) { + m_capacity = 0ull; + m_available = 0ull; + } +} + bool Installer::shouldShowDirectory() const { return m_showDirectoryOnClose && m_valid && m_finished; } +void Installer::showDirectoryOnClose(const bool & show) +{ + m_showDirectoryOnClose = show; +} + char * Installer::getPackagePointer() const { return m_packagePtr; } -size_t Installer::getPackageSize() const +size_t Installer::getCompressedPackageSize() const { return m_packageSize; } +size_t Installer::getDirectorySizeCapacity() const +{ + return m_capacity; +} + +size_t Installer::getDirectorySizeAvailable() const +{ + return m_available; +} + +size_t Installer::getDirectorySizeRequired() const +{ + return m_maxSize; +} + +std::string Installer::getPackageName() const +{ + return m_packageName; +} + void Installer::updateButtons(const WORD btnHandle) { if (btnHandle == LOWORD(m_prevBtn)) diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 460f59e..e8fbfc4 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -42,15 +42,33 @@ class Installer { /** Retrieves the current directory chosen for installation. @return active installation directory. */ std::string getDirectory() const; - /** Retrieve if the chosen directory should open when the installer closes. + /** Sets a new installation directory. + @param directory new installation directory. */ + void setDirectory(const std::string & directory); + /** Retrieve if the chosen directory should show when the installer closes. @return true if should show, false otherwise. */ bool shouldShowDirectory() const; + /** Set if the chosen directory should show when the installer closes. + @param show whether or not the directory should show. */ + void showDirectoryOnClose(const bool & show); /** Retrieves the pointer to the compressed packaged contents. @return the package pointer (offset of folder name data). */ char * getPackagePointer() const; /** Retrieves the size of the compressed package. @return the package size (minus the folder name data). */ - size_t getPackageSize() const; + size_t getCompressedPackageSize() const; + /** Retrieves the size of the drive used in the current directory. + @return the drive capacity. */ + size_t getDirectorySizeCapacity() const; + /** Retrieves the remaining size of the drive used in the current directory. + @return the available size. */ + size_t getDirectorySizeAvailable() const; + /** Retrieves the required size of the uncompressed package to be installed. + @return the (uncompressed) package size. */ + size_t getDirectorySizeRequired() const; + /** Retrieves the package name. + @return the package name. */ + std::string getPackageName() const; /** Check which button has been active, and perform it's state operation. @param btnHandle handle to the currently active button. */ void updateButtons(const WORD btnHandle); @@ -66,13 +84,24 @@ class Installer { void enableButtons(const bool & prev, const bool & next, const bool & close); + // Public manifest strings + std::wstring + m_name = L"", + m_version = L"", + m_description = L""; + + private: + // Private Constructors + Installer(); + + // Private Attributes - Resource m_archive; + Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; char * m_packagePtr = nullptr; - size_t m_packageSize = 0ull, m_maxSize = 0ull; + size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; FrameEnums m_currentIndex = WELCOME_FRAME; Frame * m_frames[FRAME_COUNT]; State * m_state = nullptr; diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp index 42a9c99..36f3904 100644 --- a/src/nStaller/States/InstallState.cpp +++ b/src/nStaller/States/InstallState.cpp @@ -20,7 +20,7 @@ void InstallState::enact() size_t byteCount(0ull), fileCount(0ull); auto directory = m_installer->getDirectory(); sanitize_path(directory); - if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getPackageSize(), byteCount, fileCount)) + if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getCompressedPackageSize(), byteCount, fileCount)) m_installer->invalidate(); else m_installer->enableButtons(false, true, false); diff --git a/src/nStaller/archive.dat b/src/nStaller/archive.npack similarity index 100% rename from src/nStaller/archive.dat rename to src/nStaller/archive.npack diff --git a/src/nStaller/manifest.nman b/src/nStaller/manifest.nman new file mode 100644 index 0000000..e69de29 diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc index 77b4d804e6d4a5242ff962d313520c5a3b344ce2..a53f56f6e7c0315bda8dfb16570ad1d7f677a32c 100644 GIT binary patch delta 133 zcmbOtIY(lH2~%tyLjgk~Lo!1)gAxNT0~Z*3GPp1VF~l?YGB`5$F?ce#F}N}WGlVd3 zVi8ed$OY=pW5{GkV@PEvW+-9M1M(nZN}HvbcQXkpFa$6ZG2}C30Tm7gAxNT0~Z(vGlVb%F?cfgF}MR+jtou=J`Ap487Cm^4weC_ z Date: Sat, 13 Apr 2019 15:13:37 -0500 Subject: [PATCH 13/44] Modified installer error log The error log now has a timestamp and appends its errors to the end of the log --- src/nStaller/Frames/FinishFrame.cpp | 1 - src/nStaller/States/FailState.cpp | 23 +++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp index f8b8a4b..f331497 100644 --- a/src/nStaller/Frames/FinishFrame.cpp +++ b/src/nStaller/Frames/FinishFrame.cpp @@ -73,7 +73,6 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l } else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); - auto controlHandle = HWND(lParam); if (notification == BN_CLICKED) { BOOL checked = IsDlgButtonChecked(hWnd, 1); ptr->m_installer->showDirectoryOnClose(checked); diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp index b22821c..fe74211 100644 --- a/src/nStaller/States/FailState.cpp +++ b/src/nStaller/States/FailState.cpp @@ -2,6 +2,7 @@ #include "Common.h" #include "TaskLogger.h" #include "../Installer.h" +#include #include @@ -16,16 +17,26 @@ void FailState::enact() // Dump error log to disk const auto dir = get_current_directory() + "\\error_log.txt"; - const auto data = "Installer error log:\r\n" + TaskLogger::PullText(); + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Installer error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); - std::ofstream file(dir, std::ios::binary | std::ios::out); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); if (!file.is_open()) TaskLogger::PushText("Cannot dump error log to disk...\r\n"); - else { - file.write(data.c_str(), (std::streamsize)data.size()); - file.close(); - } + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); } void FailState::pressPrevious() From 73f31065f310a36bd0e52e22e11f009dbb53bcfc Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 13 Apr 2019 15:32:38 -0500 Subject: [PATCH 14/44] Fallback on package name If no installer name is found in the manifest, fallsback on package name again. --- src/BufferTools.cpp | 6 +++--- src/Common.h | 31 +++++++++++++++++++++++++++++++ src/nStaller/Installer.cpp | 6 +++++- src/resource.h | 20 -------------------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/BufferTools.cpp b/src/BufferTools.cpp index 97d9ab3..e01e64c 100644 --- a/src/BufferTools.cpp +++ b/src/BufferTools.cpp @@ -54,7 +54,7 @@ bool BFT::DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_new, const size_t & size_new, char ** buffer_diff, size_t & size_diff, size_t * instructionCount) { std::vector instructions; - instructions.reserve(std::max(size_old, size_new) / 8ull); + instructions.reserve(std::max(size_old, size_new) / 8ull); std::mutex instructionMutex; Threader threader; constexpr size_t amount(4096); @@ -192,7 +192,7 @@ bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_ // We only care about repeats larger than 36 bytes. if (inst->newData.size() > 36ull) { // Upper limit (mx and my) reduced by 36, since we only care about matches that exceed 36 bytes - size_t max = std::min(inst->newData.size(), inst->newData.size() - 37ull); + size_t max = std::min(inst->newData.size(), inst->newData.size() - 37ull); for (size_t x = 0ull; x < max; ++x) { const auto & value_at_x = inst->newData[x]; if (inst->newData[x + 36ull] != value_at_x) @@ -233,7 +233,7 @@ bool BFT::DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_ writeGuard.release(); x = ULLONG_MAX; // require overflow, because we want next itteration for x == 0 - max = std::min(inst->newData.size(), inst->newData.size() - 37ull); + max = std::min(inst->newData.size(), inst->newData.size() - 37ull); break; } x = y - 1; diff --git a/src/Common.h b/src/Common.h index 2dc8581..1b200c7 100644 --- a/src/Common.h +++ b/src/Common.h @@ -9,6 +9,7 @@ #include #include #include +#include /** Changes an input string to lower case, and returns it. @@ -21,6 +22,36 @@ inline static std::string string_to_lower(const std::string & string) return input; } +/** Converts the input string into a wide string. +@param str the input string. +@return wide string version of the string. */ +inline static std::wstring to_wideString(const std::string & str) +{ + if (!str.empty()) { + if (const auto size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0)) { + std::wstring wstr(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], size_needed); + return wstr; + } + } + return std::wstring(); +} + +/** Converts the input wide string into a string. +@param str the input wide string. +@return string version of the wide string. */ +inline static std::string from_wideString(const std::wstring & wstr) +{ + if (!wstr.empty()) { + if (const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL)) { + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &str[0], size_needed, NULL, NULL); + return str; + } + } + return std::string(); +} + /** Increment a pointer's address by the offset provided. @param ptr the pointer to increment by the offset amount. @param offset the offset amount to apply to the pointer's address. diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index fae9b75..0fe9ab8 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -63,6 +63,10 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); m_maxSize = *reinterpret_cast(m_packagePtr); + + // If no name is found, use the package name (if available) + if (m_name.empty() && !m_packageName.empty()) + m_name = to_wideString(m_packageName); } // Create window class WNDCLASSEX wcex; @@ -84,7 +88,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() } else { m_window = CreateWindowW( - L"nStaller", (m_name + L" Installer").c_str(), + L"nStaller",(m_name + L" Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, diff --git a/src/resource.h b/src/resource.h index dbd42d9..6d32a7e 100644 --- a/src/resource.h +++ b/src/resource.h @@ -56,26 +56,6 @@ class Resource { inline bool exists() const { return (m_hResource && m_hMemory && m_ptr) && (m_size > 0ull); } - /** Retrieve a wide-string resource from a string table.*/ - inline static std::wstring WString(const int & resourceID, const HMODULE & moduleHandle = nullptr) { - if (MAKEINTRESOURCE(resourceID)) { - wchar_t * buffer = nullptr; - if (auto length = LoadStringW(moduleHandle, resourceID, (LPWSTR)(&buffer), 0)) - return std::wstring(buffer, length); - } - return std::wstring(); - } - /** Retrieve a string resource from a string table.*/ - inline static std::string String(const int & resourceID, const HMODULE & moduleHandle = nullptr) { - const auto wstr = WString(resourceID, moduleHandle); - if (!wstr.empty()) - if (const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL)) { - std::string strTo(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); - return strTo; - } - return std::string(); - } private: From 40e0e9a8392170f72a2b1e343d66221471a20589 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 13 Apr 2019 15:42:38 -0500 Subject: [PATCH 15/44] Updated description text rendering Updated description text rendering, so it can support long text. --- src/nStaller/Frames/WelcomeFrame.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index d29cad4..74a89ad 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -55,15 +55,16 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + StringFormat format = StringFormat::GenericTypographic(); // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); auto nameVer = ptr->m_installer->m_name + L" " + ptr->m_installer->m_version; if (ptr->m_installer->m_name.empty()) nameVer = L"it's contents"; - graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(ptr->m_installer->m_description.c_str(), -1, ®Font, PointF{ 10, 115 }, &blackBrush); + graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); + graphics.DrawString(ptr->m_installer->m_description.c_str(), -1, ®Font, RectF(10, 150, 630, 300), &format, &blackBrush); EndPaint(hWnd, &ps); return S_OK; From b5395025ff8be67b2a84a8115c9a3fd7b9f2d600 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 13 Apr 2019 23:55:14 -0500 Subject: [PATCH 16/44] Updated manifest to use a map Manifest values stored in a map for easy access, and easier maintenance. Won't need to manually declare all future strings --- src/nStaller/Frames/WelcomeFrame.cpp | 6 +++--- src/nStaller/Installer.cpp | 27 +++++++++++---------------- src/nStaller/Installer.h | 13 ++++++++----- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp index 74a89ad..9818656 100644 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ b/src/nStaller/Frames/WelcomeFrame.cpp @@ -61,10 +61,10 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - auto nameVer = ptr->m_installer->m_name + L" " + ptr->m_installer->m_version; - if (ptr->m_installer->m_name.empty()) nameVer = L"it's contents"; + auto nameVer = ptr->m_installer->m_mfStrings[L"name"] + L" " + ptr->m_installer->m_mfStrings[L"version"]; + if (ptr->m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); - graphics.DrawString(ptr->m_installer->m_description.c_str(), -1, ®Font, RectF(10, 150, 630, 300), &format, &blackBrush); + graphics.DrawString(ptr->m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, 630, 300), &format, &blackBrush); EndPaint(hWnd, &ps); return S_OK; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 0fe9ab8..0ad4079 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -1,7 +1,6 @@ #include "Installer.h" #include "Common.h" #include "TaskLogger.h" -#include #include #include @@ -24,23 +23,19 @@ Installer::Installer() { // Process manifest if (m_manifest.exists()) { - struct compare_string { bool operator()(const wchar_t * a, const wchar_t * b) const { return wcscmp(a, b) < 0; } }; - std::map valueMap = { - // Add all manifest string values to keep track of here - { L"-name", &m_name }, - { L"-version", &m_version }, - { L"-description", &m_description } - }; - // Create a string stream of the manifest file std::wstringstream ss; ss << reinterpret_cast(m_manifest.getPtr()); - // Cycle through every line, matching the attribute name with the attribute value + // Cycle through every line, inserting attributes into the manifest map std::wstring attrib, value; - while (ss >> attrib && ss >> std::quoted(value)) - if (valueMap.find(attrib.c_str()) != valueMap.end()) - *(valueMap.at(attrib.c_str())) = value; // update the value found in the map + while (ss >> attrib && ss >> std::quoted(value)) { + // Yes, this will leak + // still doesn't work + wchar_t * k = new wchar_t[attrib.length() + 1]; + wcscpy_s(k, attrib.length() + 1, attrib.data()); + m_mfStrings[k] = value; + } } } @@ -65,8 +60,8 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() m_maxSize = *reinterpret_cast(m_packagePtr); // If no name is found, use the package name (if available) - if (m_name.empty() && !m_packageName.empty()) - m_name = to_wideString(m_packageName); + if (m_mfStrings[L"name"].empty() && !m_packageName.empty()) + m_mfStrings[L"name"] = to_wideString(m_packageName); } // Create window class WNDCLASSEX wcex; @@ -88,7 +83,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() } else { m_window = CreateWindowW( - L"nStaller",(m_name + L" Installer").c_str(), + L"nStaller",(m_mfStrings[L"name"] + L" Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index e8fbfc4..b49d03a 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -3,6 +3,7 @@ #define INSTALLER_H #include "Resource.h" +#include #include #include @@ -85,10 +86,12 @@ class Installer { // Public manifest strings - std::wstring - m_name = L"", - m_version = L"", - m_description = L""; + struct compare_string { + bool operator()(const wchar_t * a, const wchar_t * b) const { + return wcscmp(a, b) < 0; + } + }; + std::map m_mfStrings; private: @@ -96,7 +99,7 @@ class Installer { Installer(); - // Private Attributes + // Private Attributes Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; From 2331bc62d20e87c35e4384f6a7df01d68850358b Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 15 Apr 2019 11:13:30 -0500 Subject: [PATCH 17/44] Adding eula screen Adding eula screen --- src/nStaller/Frames/AgreementFrame.cpp | 75 ++++++++++++++++++++++++++ src/nStaller/Frames/AgreementFrame.h | 24 +++++++++ src/nStaller/Frames/DirectoryFrame.cpp | 4 +- src/nStaller/Frames/DirectoryFrame.h | 1 + src/nStaller/Installer.cpp | 10 ++-- src/nStaller/Installer.h | 2 +- src/nStaller/States/AgreementState.cpp | 40 ++++++++++++++ src/nStaller/States/AgreementState.h | 23 ++++++++ src/nStaller/States/DirectoryState.cpp | 4 +- src/nStaller/States/WelcomeState.cpp | 4 +- 10 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 src/nStaller/Frames/AgreementFrame.cpp create mode 100644 src/nStaller/Frames/AgreementFrame.h create mode 100644 src/nStaller/States/AgreementState.cpp create mode 100644 src/nStaller/States/AgreementState.h diff --git a/src/nStaller/Frames/AgreementFrame.cpp b/src/nStaller/Frames/AgreementFrame.cpp new file mode 100644 index 0000000..0a2258a --- /dev/null +++ b/src/nStaller/Frames/AgreementFrame.cpp @@ -0,0 +1,75 @@ +#include "AgreementFrame.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +AgreementFrame::~AgreementFrame() +{ + UnregisterClass("AGREEMENT_FRAME", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkNo); + DestroyWindow(m_checkYes); +} + +AgreementFrame::AgreementFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) +{ + // Create window class + m_installer = installer; + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "AGREEMENT_FRAME"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("AGREEMENT_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (AgreementFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Please read the following license agreement.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_COMMAND) { + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Frames/AgreementFrame.h b/src/nStaller/Frames/AgreementFrame.h new file mode 100644 index 0000000..86255c1 --- /dev/null +++ b/src/nStaller/Frames/AgreementFrame.h @@ -0,0 +1,24 @@ +#pragma once +#ifndef AGREEMENTFRAME_H +#define AGREEMENTFRAME_H + +#include "Frame.h" +#include + + +class Installer; + +/** Custom frame class, representing the installer 'accept agreement' screen. */ +class AgreementFrame : public Frame { +public: + // Public (de)Constructors + ~AgreementFrame(); + AgreementFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Attributes + Installer * m_installer = nullptr; + HWND m_log = nullptr, m_checkNo = nullptr, m_checkYes = nullptr; +}; + +#endif // AGREEMENTFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp index dbaf075..20dec47 100644 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ b/src/nStaller/Frames/DirectoryFrame.cpp @@ -172,9 +172,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l Graphics graphics(BeginPaint(hWnd, &ps)); // Draw Background - SolidBrush solidWhiteBackground( - Color(255, 255, 255, 255) - ); + SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 450); LinearGradientBrush backgroundGradient( Point(0, 0), diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h index 0905d93..2e496fc 100644 --- a/src/nStaller/Frames/DirectoryFrame.h +++ b/src/nStaller/Frames/DirectoryFrame.h @@ -15,6 +15,7 @@ class DirectoryFrame : public Frame { ~DirectoryFrame(); DirectoryFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + // Public Attributes Installer * m_installer = nullptr; HWND m_directoryField = nullptr, m_browseButton = nullptr; diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 0ad4079..73d86d4 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -10,6 +10,7 @@ // Frames used in this GUI application #include "Frames/WelcomeFrame.h" +#include "Frames/AgreementFrame.h" #include "Frames/DirectoryFrame.h" #include "Frames/InstallFrame.h" #include "Frames/FinishFrame.h" @@ -107,6 +108,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() // The portions of the screen that change based on input m_frames[WELCOME_FRAME] = new WelcomeFrame(this, hInstance, m_window, { 170,0,800,450 }); + m_frames[AGREEMENT_FRAME] = new AgreementFrame(this, hInstance, m_window, { 170,0,800,450 }); m_frames[DIRECTORY_FRAME] = new DirectoryFrame(this, hInstance, m_window, { 170,0,800,450 }); m_frames[INSTALL_FRAME] = new InstallFrame(this, hInstance, m_window, { 170,0,800,450 }); m_frames[FINISH_FRAME] = new FinishFrame(this, hInstance, m_window, { 170,0,800,450 }); @@ -267,15 +269,15 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Steps const SolidBrush lineBrush(Color(255,100,100,100)); graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); - constexpr static wchar_t* step_labels[] = { L"Welcome", L"Directory", L"Install", L"Finish" }; + constexpr static wchar_t* step_labels[] = { L"Welcome", L"EULA", L"Directory", L"Install", L"Finish" }; FontFamily fontFamily(L"Segoe UI"); Font font(&fontFamily, 15, FontStyleBold, UnitPixel); REAL vertical_offset = 15; const auto frameIndex = (int)ptr->getCurrentIndex(); - for (int x = 0; x < 4; ++x) { + for (int x = 0; x < 5; ++x) { // Draw Circle auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); - if (x == 3 && frameIndex == 4) + if (x == 4 && frameIndex == 5) color = Color(255, 225, 25, 75); const SolidBrush brush(color); Pen pen(color); @@ -286,7 +288,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Draw Text graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); - if (x == 2) + if (x == 3) vertical_offset = 460; else vertical_offset += 50; diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index b49d03a..993d54d 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -21,7 +21,7 @@ class Installer { // Public Enumerations const enum FrameEnums { - WELCOME_FRAME, DIRECTORY_FRAME, INSTALL_FRAME, FINISH_FRAME, FAIL_FRAME, + WELCOME_FRAME, AGREEMENT_FRAME, DIRECTORY_FRAME, INSTALL_FRAME, FINISH_FRAME, FAIL_FRAME, FRAME_COUNT }; diff --git a/src/nStaller/States/AgreementState.cpp b/src/nStaller/States/AgreementState.cpp new file mode 100644 index 0000000..8f334fb --- /dev/null +++ b/src/nStaller/States/AgreementState.cpp @@ -0,0 +1,40 @@ +#include "AgreementState.h" +#include "WelcomeState.h" +#include "DirectoryState.h" +#include "../Installer.h" + + +AgreementState::AgreementState(Installer * installer) + : State(installer) {} + +void AgreementState::enact() +{ + m_installer->showFrame(Installer::FrameEnums::AGREEMENT_FRAME); + m_installer->showButtons(true, true, true); +} + +void AgreementState::pressPrevious() +{ + m_installer->setState(new WelcomeState(m_installer)); +} + +void AgreementState::pressNext() +{ + auto directory = m_installer->getDirectory(); + + if (directory == "" || directory == " " || directory.length() < 3) + MessageBox( + NULL, + "Please enter a valid directory before proceeding.", + "Invalid path!", + MB_OK | MB_ICONERROR | MB_TASKMODAL + ); + else + m_installer->setState(new DirectoryState(m_installer)); +} + +void AgreementState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} \ No newline at end of file diff --git a/src/nStaller/States/AgreementState.h b/src/nStaller/States/AgreementState.h new file mode 100644 index 0000000..349eb4e --- /dev/null +++ b/src/nStaller/States/AgreementState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef AGREEMENTSTATE_H +#define AGREEMENTSTATE_H + +#include "State.h" + + +/** This state encapuslates the "Accept the license agreement" - Screen" state. */ +class AgreementState : public State { +public: + // Public (de)Constructors + ~AgreementState() = default; + AgreementState(Installer * installer); + + + // Public Interface Implementations + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // AGREEMENTSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp index 8082d99..15e3998 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/States/DirectoryState.cpp @@ -1,5 +1,5 @@ #include "DirectoryState.h" -#include "WelcomeState.h" +#include "AgreementState.h" #include "InstallState.h" #include "../Installer.h" @@ -15,7 +15,7 @@ void DirectoryState::enact() void DirectoryState::pressPrevious() { - m_installer->setState(new WelcomeState(m_installer)); + m_installer->setState(new AgreementState(m_installer)); } void DirectoryState::pressNext() diff --git a/src/nStaller/States/WelcomeState.cpp b/src/nStaller/States/WelcomeState.cpp index 64c5877..caf7b68 100644 --- a/src/nStaller/States/WelcomeState.cpp +++ b/src/nStaller/States/WelcomeState.cpp @@ -1,5 +1,5 @@ #include "WelcomeState.h" -#include "DirectoryState.h" +#include "AgreementState.h" #include "../Installer.h" @@ -19,7 +19,7 @@ void WelcomeState::pressPrevious() void WelcomeState::pressNext() { - m_installer->setState(new DirectoryState(m_installer)); + m_installer->setState(new AgreementState(m_installer)); } void WelcomeState::pressClose() From edd63c24a79610840cb2f7ed13f2db3fc4a8a63a Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 15 Apr 2019 13:11:50 -0500 Subject: [PATCH 18/44] Finished eula screen, improved frames/states Merged frames and states together, simplifying the logic for frames and states. Overall, easier to maintain and add new screens. Eula screen finished. --- src/nStaller/Frames/AgreementFrame.cpp | 75 -------- src/nStaller/Frames/AgreementFrame.h | 24 --- src/nStaller/Frames/DirectoryFrame.cpp | 245 ------------------------ src/nStaller/Frames/DirectoryFrame.h | 24 --- src/nStaller/Frames/FailFrame.cpp | 78 -------- src/nStaller/Frames/FailFrame.h | 24 --- src/nStaller/Frames/FinishFrame.cpp | 83 -------- src/nStaller/Frames/FinishFrame.h | 23 --- src/nStaller/Frames/Frame.h | 33 ---- src/nStaller/Frames/InstallFrame.cpp | 90 --------- src/nStaller/Frames/InstallFrame.h | 26 --- src/nStaller/Frames/WelcomeFrame.cpp | 73 ------- src/nStaller/Frames/WelcomeFrame.h | 22 --- src/nStaller/Installer.cpp | 59 +++--- src/nStaller/Installer.h | 25 +-- src/nStaller/States/AgreementState.cpp | 122 ++++++++++-- src/nStaller/States/AgreementState.h | 11 +- src/nStaller/States/DirectoryState.cpp | 251 ++++++++++++++++++++++++- src/nStaller/States/DirectoryState.h | 10 +- src/nStaller/States/FailState.cpp | 81 +++++++- src/nStaller/States/FailState.h | 11 +- src/nStaller/States/FinishState.cpp | 87 ++++++++- src/nStaller/States/FinishState.h | 10 +- src/nStaller/States/InstallState.cpp | 95 +++++++++- src/nStaller/States/InstallState.h | 12 +- src/nStaller/States/State.h | 29 ++- src/nStaller/States/WelcomeState.cpp | 77 +++++++- src/nStaller/States/WelcomeState.h | 6 +- src/nStaller/manifest.nman | 11 ++ 29 files changed, 785 insertions(+), 932 deletions(-) delete mode 100644 src/nStaller/Frames/AgreementFrame.cpp delete mode 100644 src/nStaller/Frames/AgreementFrame.h delete mode 100644 src/nStaller/Frames/DirectoryFrame.cpp delete mode 100644 src/nStaller/Frames/DirectoryFrame.h delete mode 100644 src/nStaller/Frames/FailFrame.cpp delete mode 100644 src/nStaller/Frames/FailFrame.h delete mode 100644 src/nStaller/Frames/FinishFrame.cpp delete mode 100644 src/nStaller/Frames/FinishFrame.h delete mode 100644 src/nStaller/Frames/Frame.h delete mode 100644 src/nStaller/Frames/InstallFrame.cpp delete mode 100644 src/nStaller/Frames/InstallFrame.h delete mode 100644 src/nStaller/Frames/WelcomeFrame.cpp delete mode 100644 src/nStaller/Frames/WelcomeFrame.h diff --git a/src/nStaller/Frames/AgreementFrame.cpp b/src/nStaller/Frames/AgreementFrame.cpp deleted file mode 100644 index 0a2258a..0000000 --- a/src/nStaller/Frames/AgreementFrame.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "AgreementFrame.h" -#include "../Installer.h" - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -AgreementFrame::~AgreementFrame() -{ - UnregisterClass("AGREEMENT_FRAME", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_checkNo); - DestroyWindow(m_checkYes); -} - -AgreementFrame::AgreementFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "AGREEMENT_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("AGREEMENT_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (AgreementFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Please read the following license agreement.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); - - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_COMMAND) { - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/AgreementFrame.h b/src/nStaller/Frames/AgreementFrame.h deleted file mode 100644 index 86255c1..0000000 --- a/src/nStaller/Frames/AgreementFrame.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#ifndef AGREEMENTFRAME_H -#define AGREEMENTFRAME_H - -#include "Frame.h" -#include - - -class Installer; - -/** Custom frame class, representing the installer 'accept agreement' screen. */ -class AgreementFrame : public Frame { -public: - // Public (de)Constructors - ~AgreementFrame(); - AgreementFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; - HWND m_log = nullptr, m_checkNo = nullptr, m_checkYes = nullptr; -}; - -#endif // AGREEMENTFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.cpp b/src/nStaller/Frames/DirectoryFrame.cpp deleted file mode 100644 index 20dec47..0000000 --- a/src/nStaller/Frames/DirectoryFrame.cpp +++ /dev/null @@ -1,245 +0,0 @@ -#include "DirectoryFrame.h" -#include "../Installer.h" -#include -#include -#include -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -static std::string directory_and_package(const std::string & dir, const std::string & pack) -{ - std::string directory = dir; - // Ensure last character is a backslash - if (dir.size() && dir[dir.size() - 1ull] != '\\') - directory += '\\' + pack; - else - directory += pack; - return directory; -} - -DirectoryFrame::~DirectoryFrame() -{ - UnregisterClass("DIRECTORY_FRAME", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_directoryField); - DestroyWindow(m_browseButton); -} - -DirectoryFrame::DirectoryFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "DIRECTORY_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("DIRECTORY_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - - // Create directory lookup fields - m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory_and_package(m_installer->getDirectory(), m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); - m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); - setVisible(false); -} - -static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) -{ - /** File Dialog Event Handler */ - class DialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents { - private: - ~DialogEventHandler() { }; - long _cRef; - - - public: - // Constructor - DialogEventHandler() : _cRef(1) { }; - - - // IUnknown methods - IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) { - static const QITAB qit[] = { - QITABENT(DialogEventHandler, IFileDialogEvents), - QITABENT(DialogEventHandler, IFileDialogControlEvents), - { 0 }, - }; - return QISearch(this, qit, riid, ppv); - } - IFACEMETHODIMP_(ULONG) AddRef() { - return InterlockedIncrement(&_cRef); - } - IFACEMETHODIMP_(ULONG) Release() { - long cRef = InterlockedDecrement(&_cRef); - if (!cRef) - delete this; - return cRef; - } - - - // IFileDialogEvents methods - IFACEMETHODIMP OnFileOk(IFileDialog *) { return S_OK; }; - IFACEMETHODIMP OnFolderChange(IFileDialog *) { return S_OK; }; - IFACEMETHODIMP OnFolderChanging(IFileDialog *, IShellItem *) { return S_OK; }; - IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; }; - IFACEMETHODIMP OnSelectionChange(IFileDialog *) { return S_OK; }; - IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; - IFACEMETHODIMP OnTypeChange(IFileDialog *) { return S_OK; }; - IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; - - - // IFileDialogControlEvents methods - IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *, DWORD, DWORD ) { return S_OK; }; - IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; - IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize *, DWORD, BOOL) { return S_OK; }; - IFACEMETHODIMP OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; - }; - - *ppv = NULL; - DialogEventHandler *pDialogEventHandler = new (std::nothrow) DialogEventHandler(); - HRESULT hr = pDialogEventHandler ? S_OK : E_OUTOFMEMORY; - if (SUCCEEDED(hr)) { - hr = pDialogEventHandler->QueryInterface(riid, ppv); - pDialogEventHandler->Release(); - } - return hr; -} - -HRESULT OpenFileDialog(std::string & directory) -{ - // CoCreate the File Open Dialog object. - IFileDialog *pfd = NULL; - IFileDialogEvents *pfde = NULL; - DWORD dwCookie, dwFlags; - HRESULT hr = S_FALSE; - if ( - SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))) && - SUCCEEDED(CreateDialogEventHandler(IID_PPV_ARGS(&pfde))) && - SUCCEEDED(pfd->Advise(pfde, &dwCookie)) && - SUCCEEDED(pfd->GetOptions(&dwFlags)) && - SUCCEEDED(pfd->SetOptions(dwFlags | FOS_PICKFOLDERS | FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT)) && - SUCCEEDED(pfd->Show(NULL)) - ) - { - // The result is an IShellItem object. - IShellItem *psiResult; - PWSTR pszFilePath = NULL; - if (SUCCEEDED(pfd->GetResult(&psiResult)) && SUCCEEDED(psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath))) { - std::wstringstream ss; - ss << pszFilePath; - auto ws = ss.str(); - typedef std::codecvt converter_type; - const std::locale locale(""); - const converter_type& converter = std::use_facet(locale); - std::vector to(ws.length() * converter.max_length()); - std::mbstate_t state; - const wchar_t* from_next; - char* to_next; - const converter_type::result result = converter.out(state, ws.data(), ws.data() + ws.length(), from_next, &to[0], &to[0] + to.size(), to_next); - if (result == converter_type::ok || result == converter_type::noconv) { - directory = std::string(&to[0], to_next); - hr = S_OK; - } - CoTaskMemFree(pszFilePath); - psiResult->Release(); - } - - // Unhook the event handler. - pfd->Unadvise(dwCookie); - pfde->Release(); - pfd->Release(); - } - return hr; -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (DirectoryFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); - graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 450); - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); - - constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { - auto remainingSize = (double)size; - constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB" }; - int i = 0; - while (remainingSize > 1024.00) { - remainingSize /= 1024.00; - ++i; - } - std::wstringstream stream; - stream << std::fixed << std::setprecision(2) << remainingSize; - return stream.str() + units[i] + L"\t(" + std::to_wstring(size) + L" bytes )"; - }; - graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); - graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); - graphics.DrawString((L" Available:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); - graphics.DrawString((L" Required:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); - - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_COMMAND) { - const auto notification = HIWORD(wParam); - auto controlHandle = HWND(lParam); - if (notification == BN_CLICKED) { - std::string directory(""); - if (SUCCEEDED(OpenFileDialog(directory))) { - if (directory != "" && directory.length() > 2ull) { - directory = directory_and_package(directory, ptr->m_installer->getPackageName()); - ptr->m_installer->setDirectory(directory); - SetWindowTextA(ptr->m_directoryField, directory.c_str()); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; - } - } - } - else if (notification == EN_CHANGE) { - std::vector data(GetWindowTextLength(controlHandle) + 1ull); - GetWindowTextA(controlHandle, &data[0], (int)data.size()); - ptr->m_installer->setDirectory(std::string(data.data())); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; - } - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/DirectoryFrame.h b/src/nStaller/Frames/DirectoryFrame.h deleted file mode 100644 index 2e496fc..0000000 --- a/src/nStaller/Frames/DirectoryFrame.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#ifndef DIRECTORYFRAME_H -#define DIRECTORYFRAME_H - -#include "Frame.h" -#include - - -class Installer; - -/** Custom frame class, representing the installer 'directory choosing' screen. */ -class DirectoryFrame : public Frame { -public: - // Public (de)Constructors - ~DirectoryFrame(); - DirectoryFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; - HWND m_directoryField = nullptr, m_browseButton = nullptr; -}; - -#endif // DIRECTORYFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/FailFrame.cpp b/src/nStaller/Frames/FailFrame.cpp deleted file mode 100644 index a25b034..0000000 --- a/src/nStaller/Frames/FailFrame.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "FailFrame.h" -#include "TaskLogger.h" -#include "../Installer.h" -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -FailFrame::~FailFrame() -{ - UnregisterClass("FAIL_FRAME", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); -} - -FailFrame::FailFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FAIL_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FAIL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - - // Create error log - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); - setVisible(false); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (FailFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 225, 25, 75), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/FailFrame.h b/src/nStaller/Frames/FailFrame.h deleted file mode 100644 index d02cfff..0000000 --- a/src/nStaller/Frames/FailFrame.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#ifndef FAILFRAME_H -#define FAILFRAME_H - -#include "Frame.h" - - -class Installer; - -/** Custom frame class, representing the installer 'failure' screen. */ -class FailFrame : public Frame { -public: - // Public (de)Constructors - ~FailFrame(); - FailFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; - HWND m_hwndLog = nullptr; - size_t m_logIndex = 0ull; -}; - -#endif // FAILFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.cpp b/src/nStaller/Frames/FinishFrame.cpp deleted file mode 100644 index f331497..0000000 --- a/src/nStaller/Frames/FinishFrame.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "FinishFrame.h" -#include "../Installer.h" -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -FinishFrame::~FinishFrame() -{ - UnregisterClass("FINISH_FRAME", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_checkbox); -} - -FinishFrame::FinishFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FINISH_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FINISH_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - - // Create checkbox - m_checkbox = CreateWindow("Button", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_BORDER | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 15, 15, m_hwnd, (HMENU)1, hInstance, NULL); - CheckDlgButton(m_hwnd, 1, BST_CHECKED); - setVisible(false); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (FinishFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 255, 125), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Show installation directory on close.", -1, ®Font, PointF{ 30, 147 }, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_COMMAND) { - const auto notification = HIWORD(wParam); - if (notification == BN_CLICKED) { - BOOL checked = IsDlgButtonChecked(hWnd, 1); - ptr->m_installer->showDirectoryOnClose(checked); - return S_OK; - } - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/FinishFrame.h b/src/nStaller/Frames/FinishFrame.h deleted file mode 100644 index 24b0449..0000000 --- a/src/nStaller/Frames/FinishFrame.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#ifndef FINISHFRAME_H -#define FINISHFRAME_H - -#include "Frame.h" - - -class Installer; - -/** Custom frame class, representing the installer 'finish' screen. */ -class FinishFrame : public Frame { -public: - // Public (de)Constructors - ~FinishFrame(); - FinishFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; - HWND m_checkbox = nullptr; -}; - -#endif // FINISHFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/Frame.h b/src/nStaller/Frames/Frame.h deleted file mode 100644 index 9c7309b..0000000 --- a/src/nStaller/Frames/Frame.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifndef FRAME_H -#define FRAME_H - -#include -#pragma warning(push) -#pragma warning(disable:4458) -#include -#pragma warning(pop) - - -using namespace Gdiplus; - -/** Encapsulation of a windows GDI 'window' object.*/ -class Frame { -public: - // Public Methods - /** Sets the visibility & enable state of this window. - @param state whether or not to show and activate this window. */ - void setVisible(const bool & state) { - ShowWindow(m_hwnd, state); - EnableWindow(m_hwnd, state); - } - - -protected: - // Private Attributes - WNDCLASSEX m_wcex; - HWND m_hwnd = nullptr; - HINSTANCE m_hinstance; -}; - -#endif // FRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.cpp b/src/nStaller/Frames/InstallFrame.cpp deleted file mode 100644 index 06ff6ba..0000000 --- a/src/nStaller/Frames/InstallFrame.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "InstallFrame.h" -#include "TaskLogger.h" -#include "../Installer.h" -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -InstallFrame::~InstallFrame() -{ - UnregisterClass("INSTALL_FRAME", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - DestroyWindow(m_hwndPrgsBar); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); - TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); -} - -InstallFrame::InstallFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "INSTALL_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("INSTALL_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - - // Create log box and progress bar - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left)-20, (rc.bottom - rc.top)-100, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); - m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { - SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); - SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); - m_progress = std::to_wstring( position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f)))+ L"%"; - RECT rc = { 580, 410, 800, 450 }; - RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); - }); - - setVisible(false); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (InstallFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/InstallFrame.h b/src/nStaller/Frames/InstallFrame.h deleted file mode 100644 index 12fc485..0000000 --- a/src/nStaller/Frames/InstallFrame.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once -#ifndef INSTALLFRAME_H -#define INSTALLFRAME_H - -#include "Frame.h" -#include - - -class Installer; - -/** Custom frame class, representing the installer 'install' screen. */ -class InstallFrame : public Frame { -public: - // Public (de)Constructors - ~InstallFrame(); - InstallFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; - HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; - size_t m_logIndex = 0ull, m_taskIndex = 0ull; - std::wstring m_progress = L"0%"; -}; - -#endif // INSTALLFRAME_H \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.cpp b/src/nStaller/Frames/WelcomeFrame.cpp deleted file mode 100644 index 9818656..0000000 --- a/src/nStaller/Frames/WelcomeFrame.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "WelcomeFrame.h" -#include "Resource.h" -#include "../Installer.h" - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -WelcomeFrame::~WelcomeFrame() -{ - UnregisterClass("WELCOME_FRAME", m_hinstance); - DestroyWindow(m_hwnd); -} - -WelcomeFrame::WelcomeFrame(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) -{ - // Create window class - m_installer = installer; - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "WELCOME_FRAME"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_FRAME", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (WelcomeFrame*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - StringFormat format = StringFormat::GenericTypographic(); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - auto nameVer = ptr->m_installer->m_mfStrings[L"name"] + L" " + ptr->m_installer->m_mfStrings[L"version"]; - if (ptr->m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; - graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); - graphics.DrawString(ptr->m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, 630, 300), &format, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/Frames/WelcomeFrame.h b/src/nStaller/Frames/WelcomeFrame.h deleted file mode 100644 index cf8fcc1..0000000 --- a/src/nStaller/Frames/WelcomeFrame.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#ifndef WELCOMEFRAME_H -#define WELCOMEFRAME_H - -#include "Frame.h" - - -class Installer; - -/** Custom frame class, representing the installer 'welcome' screen. */ -class WelcomeFrame : public Frame { -public: - // Public (de)Constructors - ~WelcomeFrame(); - WelcomeFrame(Installer * installer, HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Attributes - Installer * m_installer = nullptr; -}; - -#endif // WELCOMEFRAME_H \ No newline at end of file diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 73d86d4..f9f8312 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -9,12 +9,12 @@ #include "States/FailState.h" // Frames used in this GUI application -#include "Frames/WelcomeFrame.h" -#include "Frames/AgreementFrame.h" -#include "Frames/DirectoryFrame.h" -#include "Frames/InstallFrame.h" -#include "Frames/FinishFrame.h" -#include "Frames/FailFrame.h" +#include "States/WelcomeState.h" +#include "States/AgreementState.h" +#include "States/DirectoryState.h" +#include "States/InstallState.h" +#include "States/FinishState.h" +#include "States/FailState.h" static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); @@ -107,13 +107,13 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); // The portions of the screen that change based on input - m_frames[WELCOME_FRAME] = new WelcomeFrame(this, hInstance, m_window, { 170,0,800,450 }); - m_frames[AGREEMENT_FRAME] = new AgreementFrame(this, hInstance, m_window, { 170,0,800,450 }); - m_frames[DIRECTORY_FRAME] = new DirectoryFrame(this, hInstance, m_window, { 170,0,800,450 }); - m_frames[INSTALL_FRAME] = new InstallFrame(this, hInstance, m_window, { 170,0,800,450 }); - m_frames[FINISH_FRAME] = new FinishFrame(this, hInstance, m_window, { 170,0,800,450 }); - m_frames[FAIL_FRAME] = new FailFrame(this, hInstance, m_window, { 170,0,800,450 }); - setState(new WelcomeState(this)); + m_states[WELCOME_STATE] = new WelcomeState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[AGREEMENT_STATE] = new AgreementState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[DIRECTORY_STATE] = new DirectoryState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[INSTALL_STATE] = new InstallState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[FINISH_STATE] = new FinishState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[FAIL_STATE] = new FailState(this, hInstance, m_window, { 170,0,800,450 }); + setState(WELCOME_STATE); } #ifndef DEBUG @@ -124,7 +124,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() void Installer::invalidate() { - setState(new FailState(this)); + setState(FAIL_STATE); showButtons(false, false, true); enableButtons(false, false, true); m_valid = false; @@ -135,28 +135,19 @@ void Installer::finish() m_finished = true; } -void Installer::showFrame(const FrameEnums & newIndex) -{ - m_frames[m_currentIndex]->setVisible(false); - m_frames[newIndex]->setVisible(true); - m_currentIndex = newIndex; -} - -void Installer::setState(State * state) -{ - if (!m_valid) - delete state; // refuse new states - else { - if (m_state != nullptr) - delete m_state; - m_state = state; - m_state->enact(); +void Installer::setState(const StateEnums & stateIndex) +{ + if (m_valid) { + m_states[m_currentIndex]->setVisible(false); + m_states[stateIndex]->enact(); + m_states[stateIndex]->setVisible(true); + m_currentIndex = stateIndex; RECT rc = { 0, 0, 160, 500 }; RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); } } -Installer::FrameEnums Installer::getCurrentIndex() const +Installer::StateEnums Installer::getCurrentIndex() const { return m_currentIndex; } @@ -224,11 +215,11 @@ std::string Installer::getPackageName() const void Installer::updateButtons(const WORD btnHandle) { if (btnHandle == LOWORD(m_prevBtn)) - m_state->pressPrevious(); + m_states[m_currentIndex]->pressPrevious(); else if (btnHandle == LOWORD(m_nextBtn)) - m_state->pressNext(); + m_states[m_currentIndex]->pressNext(); else if (btnHandle == LOWORD(m_exitBtn)) - m_state->pressClose(); + m_states[m_currentIndex]->pressClose(); RECT rc = { 0, 0, 160, 500 }; RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); } diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 993d54d..a7a333a 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -8,8 +8,7 @@ #include -class Frame; -class State; +class FrameState; /** Encapsulates the logical features of the installer. */ class Installer { @@ -20,9 +19,9 @@ class Installer { // Public Enumerations - const enum FrameEnums { - WELCOME_FRAME, AGREEMENT_FRAME, DIRECTORY_FRAME, INSTALL_FRAME, FINISH_FRAME, FAIL_FRAME, - FRAME_COUNT + const enum StateEnums { + WELCOME_STATE, AGREEMENT_STATE, DIRECTORY_STATE, INSTALL_STATE, FINISH_STATE, FAIL_STATE, + STATE_COUNT }; @@ -31,15 +30,12 @@ class Installer { void invalidate(); /** Flags the installation as complete. */ void finish(); - /** Displays the screen matching the supplied enumeration. - @param newIndex the screen to make visible (makes current screen invisible). */ - void showFrame(const FrameEnums & newIndex); - /** Override the current state, making the supplied state active. - @param state the new state to use. */ - void setState(State * state); + /** Make the state identified by the supplied enum as active, deactivating the previous state. + @param stateIndex the new state to use. */ + void setState(const StateEnums & stateIndex); /** Retrieves the current frame's enumeration. @return the current frame's index, as an enumeration. */ - FrameEnums getCurrentIndex() const; + StateEnums getCurrentIndex() const; /** Retrieves the current directory chosen for installation. @return active installation directory. */ std::string getDirectory() const; @@ -105,9 +101,8 @@ class Installer { bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; - FrameEnums m_currentIndex = WELCOME_FRAME; - Frame * m_frames[FRAME_COUNT]; - State * m_state = nullptr; + StateEnums m_currentIndex = WELCOME_STATE; + FrameState * m_states[STATE_COUNT]; HWND m_window = nullptr, m_prevBtn = nullptr, diff --git a/src/nStaller/States/AgreementState.cpp b/src/nStaller/States/AgreementState.cpp index 8f334fb..af489bb 100644 --- a/src/nStaller/States/AgreementState.cpp +++ b/src/nStaller/States/AgreementState.cpp @@ -1,40 +1,126 @@ #include "AgreementState.h" -#include "WelcomeState.h" -#include "DirectoryState.h" #include "../Installer.h" -AgreementState::AgreementState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +AgreementState::~AgreementState() +{ + UnregisterClass("AGREEMENT_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkNo); + DestroyWindow(m_checkYes); +} + +AgreementState::AgreementState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "AGREEMENT_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("AGREEMENT_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create eula + m_log = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 125, m_hwnd, NULL, hInstance, NULL); + SetWindowTextW(m_log, m_installer->m_mfStrings[L"eula"].c_str()); + + // Create checkboxes + m_checkYes = CreateWindow("Button", "I accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, (rc.bottom - rc.top) - 40, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); + m_checkNo = CreateWindow("Button", "I do not accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, (rc.bottom - rc.top) - 20, 610, 15, m_hwnd, (HMENU)2, hInstance, NULL); + CheckDlgButton(m_hwnd, 2, BST_CHECKED); +} void AgreementState::enact() { - m_installer->showFrame(Installer::FrameEnums::AGREEMENT_FRAME); m_installer->showButtons(true, true, true); + m_installer->enableButtons(true, m_agrees, true); } void AgreementState::pressPrevious() { - m_installer->setState(new WelcomeState(m_installer)); + m_installer->setState(Installer::WELCOME_STATE); } void AgreementState::pressNext() -{ - auto directory = m_installer->getDirectory(); - - if (directory == "" || directory == " " || directory.length() < 3) - MessageBox( - NULL, - "Please enter a valid directory before proceeding.", - "Invalid path!", - MB_OK | MB_ICONERROR | MB_TASKMODAL - ); - else - m_installer->setState(new DirectoryState(m_installer)); +{ + m_installer->setState(Installer::DIRECTORY_STATE); } void AgreementState::pressClose() { // No new screen PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (AgreementState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Please read the following license agreement:", -1, ®Font, PointF{ 10, 50 }, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make checkbox text background color transparent + if (HWND(lParam) == ptr->m_checkYes || HWND(lParam) == ptr->m_checkNo) { + SetBkMode(HDC(wParam), TRANSPARENT); + return (LRESULT)GetStockObject(NULL_BRUSH); + } + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + auto controlHandle = HWND(lParam); + if (controlHandle == ptr->m_checkYes && IsDlgButtonChecked(hWnd, 1)) { + CheckDlgButton(ptr->m_hwnd, 2, BST_UNCHECKED); + ptr->m_installer->enableButtons(true, true, true); + ptr->m_agrees = true; + } + else if (controlHandle == ptr->m_checkNo && IsDlgButtonChecked(hWnd, 2)) { + CheckDlgButton(ptr->m_hwnd, 1, BST_UNCHECKED); + ptr->m_installer->enableButtons(true, false, true); + ptr->m_agrees = false; + } + } + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/AgreementState.h b/src/nStaller/States/AgreementState.h index 349eb4e..29ea376 100644 --- a/src/nStaller/States/AgreementState.h +++ b/src/nStaller/States/AgreementState.h @@ -6,11 +6,11 @@ /** This state encapuslates the "Accept the license agreement" - Screen" state. */ -class AgreementState : public State { +class AgreementState : public FrameState { public: // Public (de)Constructors - ~AgreementState() = default; - AgreementState(Installer * installer); + ~AgreementState(); + AgreementState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations @@ -18,6 +18,11 @@ class AgreementState : public State { virtual void pressPrevious(); virtual void pressNext(); virtual void pressClose(); + + + // Public Attributes + HWND m_log = nullptr, m_checkNo = nullptr, m_checkYes = nullptr; + bool m_agrees = false; }; #endif // AGREEMENTSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/States/DirectoryState.cpp index 15e3998..2d8e8c9 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/States/DirectoryState.cpp @@ -1,21 +1,69 @@ #include "DirectoryState.h" -#include "AgreementState.h" -#include "InstallState.h" #include "../Installer.h" +#include +#include +#include +#include +#include -DirectoryState::DirectoryState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +static std::string directory_and_package(const std::string & dir, const std::string & pack) +{ + std::string directory = dir; + // Ensure last character is a backslash + if (dir.size() && dir[dir.size() - 1ull] != '\\') + directory += '\\' + pack; + else + directory += pack; + return directory; +} + +DirectoryState::~DirectoryState() +{ + UnregisterClass("DIRECTORY_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_directoryField); + DestroyWindow(m_browseButton); +} + +DirectoryState::DirectoryState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "DIRECTORY_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("DIRECTORY_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create directory lookup fields + m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory_and_package(m_installer->getDirectory(), m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); +} void DirectoryState::enact() { - m_installer->showFrame(Installer::FrameEnums::DIRECTORY_FRAME); m_installer->showButtons(true, true, true); + m_installer->enableButtons(true, true, true); } void DirectoryState::pressPrevious() { - m_installer->setState(new AgreementState(m_installer)); + m_installer->setState(Installer::AGREEMENT_STATE); } void DirectoryState::pressNext() @@ -30,11 +78,200 @@ void DirectoryState::pressNext() MB_OK | MB_ICONERROR | MB_TASKMODAL ); else - m_installer->setState(new InstallState(m_installer)); + m_installer->setState(Installer::INSTALL_STATE); } void DirectoryState::pressClose() { // No new screen PostQuitMessage(0); +} + +static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) +{ + /** File Dialog Event Handler */ + class DialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents { + private: + ~DialogEventHandler() { }; + long _cRef; + + + public: + // Constructor + DialogEventHandler() : _cRef(1) { }; + + + // IUnknown methods + IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) { + static const QITAB qit[] = { + QITABENT(DialogEventHandler, IFileDialogEvents), + QITABENT(DialogEventHandler, IFileDialogControlEvents), + { 0 }, + }; + return QISearch(this, qit, riid, ppv); + } + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement(&_cRef); + } + IFACEMETHODIMP_(ULONG) Release() { + long cRef = InterlockedDecrement(&_cRef); + if (!cRef) + delete this; + return cRef; + } + + + // IFileDialogEvents methods + IFACEMETHODIMP OnFileOk(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnFolderChanging(IFileDialog *, IShellItem *) { return S_OK; }; + IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnSelectionChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; + IFACEMETHODIMP OnTypeChange(IFileDialog *) { return S_OK; }; + IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; + + + // IFileDialogControlEvents methods + IFACEMETHODIMP OnItemSelected(IFileDialogCustomize *, DWORD, DWORD) { return S_OK; }; + IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; + IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize *, DWORD, BOOL) { return S_OK; }; + IFACEMETHODIMP OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; + }; + + *ppv = NULL; + DialogEventHandler *pDialogEventHandler = new (std::nothrow) DialogEventHandler(); + HRESULT hr = pDialogEventHandler ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) { + hr = pDialogEventHandler->QueryInterface(riid, ppv); + pDialogEventHandler->Release(); + } + return hr; +} + +HRESULT OpenFileDialog(std::string & directory) +{ + // CoCreate the File Open Dialog object. + IFileDialog *pfd = NULL; + IFileDialogEvents *pfde = NULL; + DWORD dwCookie, dwFlags; + HRESULT hr = S_FALSE; + if ( + SUCCEEDED(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))) && + SUCCEEDED(CreateDialogEventHandler(IID_PPV_ARGS(&pfde))) && + SUCCEEDED(pfd->Advise(pfde, &dwCookie)) && + SUCCEEDED(pfd->GetOptions(&dwFlags)) && + SUCCEEDED(pfd->SetOptions(dwFlags | FOS_PICKFOLDERS | FOS_OVERWRITEPROMPT | FOS_CREATEPROMPT)) && + SUCCEEDED(pfd->Show(NULL)) + ) + { + // The result is an IShellItem object. + IShellItem *psiResult; + PWSTR pszFilePath = NULL; + if (SUCCEEDED(pfd->GetResult(&psiResult)) && SUCCEEDED(psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath))) { + std::wstringstream ss; + ss << pszFilePath; + auto ws = ss.str(); + typedef std::codecvt converter_type; + const std::locale locale(""); + const converter_type& converter = std::use_facet(locale); + std::vector to(ws.length() * converter.max_length()); + std::mbstate_t state; + const wchar_t* from_next; + char* to_next; + const converter_type::result result = converter.out(state, ws.data(), ws.data() + ws.length(), from_next, &to[0], &to[0] + to.size(), to_next); + if (result == converter_type::ok || result == converter_type::noconv) { + directory = std::string(&to[0], to_next); + hr = S_OK; + } + CoTaskMemFree(pszFilePath); + psiResult->Release(); + } + + // Unhook the event handler. + pfd->Unadvise(dwCookie); + pfde->Release(); + pfd->Release(); + } + return hr; +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (DirectoryState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); + graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 450); + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); + + constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { + auto remainingSize = (double)size; + constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB" }; + int i = 0; + while (remainingSize > 1024.00) { + remainingSize /= 1024.00; + ++i; + } + std::wstringstream stream; + stream << std::fixed << std::setprecision(2) << remainingSize; + return stream.str() + units[i] + L"\t(" + std::to_wstring(size) + L" bytes )"; + }; + graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); + graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); + graphics.DrawString((L" Available:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); + graphics.DrawString((L" Required:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); + + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + auto controlHandle = HWND(lParam); + if (notification == BN_CLICKED) { + std::string directory(""); + if (SUCCEEDED(OpenFileDialog(directory))) { + if (directory != "" && directory.length() > 2ull) { + directory = directory_and_package(directory, ptr->m_installer->getPackageName()); + ptr->m_installer->setDirectory(directory); + SetWindowTextA(ptr->m_directoryField, directory.c_str()); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; + } + } + } + else if (notification == EN_CHANGE) { + std::vector data(GetWindowTextLength(controlHandle) + 1ull); + GetWindowTextA(controlHandle, &data[0], (int)data.size()); + ptr->m_installer->setDirectory(std::string(data.data())); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; + } + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.h b/src/nStaller/States/DirectoryState.h index 08bde84..d65957a 100644 --- a/src/nStaller/States/DirectoryState.h +++ b/src/nStaller/States/DirectoryState.h @@ -6,11 +6,11 @@ /** This state encapuslates the "Choose a directory - Screen" state. */ -class DirectoryState : public State { +class DirectoryState : public FrameState { public: // Public (de)Constructors - ~DirectoryState() = default; - DirectoryState(Installer * installer); + ~DirectoryState(); + DirectoryState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations @@ -18,6 +18,10 @@ class DirectoryState : public State { virtual void pressPrevious(); virtual void pressNext(); virtual void pressClose(); + + + // Public Attributes + HWND m_directoryField = nullptr, m_browseButton = nullptr; }; #endif // DIRECTORYSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp index fe74211..b35df1a 100644 --- a/src/nStaller/States/FailState.cpp +++ b/src/nStaller/States/FailState.cpp @@ -4,14 +4,52 @@ #include "../Installer.h" #include #include +#include +#include -FailState::FailState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FailState::~FailState() +{ + UnregisterClass("FAIL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +FailState::FailState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); +} void FailState::enact() { - m_installer->showFrame(Installer::FrameEnums::FAIL_FRAME); m_installer->showButtons(false, false, true); m_installer->enableButtons(false, false, true); @@ -53,4 +91,41 @@ void FailState::pressClose() { // No new screen PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/FailState.h b/src/nStaller/States/FailState.h index b3ae483..711e7a5 100644 --- a/src/nStaller/States/FailState.h +++ b/src/nStaller/States/FailState.h @@ -6,11 +6,11 @@ /** This state encapuslates the "Failure - Screen" state. */ -class FailState: public State { +class FailState: public FrameState { public: // Public (de)Constructors - ~FailState() = default; - FailState(Installer * installer); + ~FailState(); + FailState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations @@ -18,6 +18,11 @@ class FailState: public State { virtual void pressPrevious(); virtual void pressNext(); virtual void pressClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr; + size_t m_logIndex = 0ull; }; #endif // FAILSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp index c0cb182..87c1333 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/States/FinishState.cpp @@ -1,13 +1,47 @@ #include "FinishState.h" #include "../Installer.h" +#include +#include -FinishState::FinishState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FinishState::~FinishState() +{ + UnregisterClass("FINISH_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkbox); +} + +FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FINISH_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create checkbox + m_checkbox = CreateWindow("Button", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_BORDER | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 15, 15, m_hwnd, (HMENU)1, hInstance, NULL); + CheckDlgButton(m_hwnd, 1, BST_CHECKED); +} void FinishState::enact() { - m_installer->showFrame(Installer::FrameEnums::FINISH_FRAME); m_installer->showButtons(false, false, true); m_installer->enableButtons(false, false, true); m_installer->finish(); @@ -27,4 +61,51 @@ void FinishState::pressClose() { // No new screen PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (FinishState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Show installation directory on close.", -1, ®Font, PointF{ 30, 147 }, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + BOOL checked = IsDlgButtonChecked(hWnd, 1); + ptr->m_installer->showDirectoryOnClose(checked); + return S_OK; + } + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/FinishState.h b/src/nStaller/States/FinishState.h index 9db1ea4..dac6f09 100644 --- a/src/nStaller/States/FinishState.h +++ b/src/nStaller/States/FinishState.h @@ -6,11 +6,11 @@ /** This state encapuslates the "Finished - Screen" state. */ -class FinishState: public State { +class FinishState: public FrameState { public: // Public (de)Constructors - ~FinishState() = default; - FinishState(Installer * installer); + ~FinishState(); + FinishState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations @@ -18,6 +18,10 @@ class FinishState: public State { virtual void pressPrevious(); virtual void pressNext(); virtual void pressClose(); + + + // Public Attributes + HWND m_checkbox = nullptr; }; #endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp index 36f3904..7183223 100644 --- a/src/nStaller/States/InstallState.cpp +++ b/src/nStaller/States/InstallState.cpp @@ -1,17 +1,63 @@ #include "InstallState.h" -#include "FinishState.h" #include "Common.h" #include "BufferTools.h" #include "DirectoryTools.h" #include "../Installer.h" +#include -InstallState::InstallState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +InstallState::~InstallState() +{ + UnregisterClass("INSTALL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +InstallState::InstallState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "INSTALL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("INSTALL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + m_progress = std::to_wstring(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + L"%"; + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + }); +} void InstallState::enact() { - m_installer->showFrame(Installer::FrameEnums::INSTALL_FRAME); m_installer->showButtons(true, true, true); m_installer->enableButtons(false, false, false); @@ -35,10 +81,49 @@ void InstallState::pressPrevious() void InstallState::pressNext() { - m_installer->setState(new FinishState(m_installer)); + m_installer->setState(Installer::FINISH_STATE); } void InstallState::pressClose() { // Should never happen +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (InstallState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/InstallState.h b/src/nStaller/States/InstallState.h index f764819..4c3f10d 100644 --- a/src/nStaller/States/InstallState.h +++ b/src/nStaller/States/InstallState.h @@ -7,11 +7,11 @@ /** This state encapuslates the "Installing - Screen" state. */ -class InstallState : public State { +class InstallState : public FrameState { public: // Public (de)Constructors - ~InstallState() = default; - InstallState(Installer * installer); + ~InstallState(); + InstallState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations @@ -21,6 +21,12 @@ class InstallState : public State { virtual void pressClose(); + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; + std::wstring m_progress = L"0%"; + + private: // Private Attributes std::thread m_thread; diff --git a/src/nStaller/States/State.h b/src/nStaller/States/State.h index e8a7691..7823921 100644 --- a/src/nStaller/States/State.h +++ b/src/nStaller/States/State.h @@ -2,14 +2,30 @@ #ifndef STATE_H #define STATE_H +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + +using namespace Gdiplus; class Installer; -/***/ -class State { +/**Encapsulation of a windows GDI 'window' object, for a particular state of the application. */ +class FrameState { public: // Public (de)Constructors - State(Installer * installer) : m_installer(installer) {} + FrameState(Installer * installer) : m_installer(installer) {} + + + // Public Methods + /** Sets the visibility & enable state of this window. + @param state whether or not to show and activate this window. */ + void setVisible(const bool & state) { + ShowWindow(m_hwnd, state); + EnableWindow(m_hwnd, state); + } // Public Interface Declarations @@ -25,6 +41,13 @@ class State { // Public Attributes Installer * m_installer = nullptr; + HWND m_hwnd = nullptr; + + +protected: + // Private Attributes + WNDCLASSEX m_wcex; + HINSTANCE m_hinstance; }; #endif // STATE_H \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.cpp b/src/nStaller/States/WelcomeState.cpp index caf7b68..4b6c45b 100644 --- a/src/nStaller/States/WelcomeState.cpp +++ b/src/nStaller/States/WelcomeState.cpp @@ -1,15 +1,42 @@ #include "WelcomeState.h" -#include "AgreementState.h" #include "../Installer.h" -WelcomeState::WelcomeState(Installer * installer) - : State(installer) {} +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +WelcomeState::~WelcomeState() +{ + UnregisterClass("WELCOME_STATE", m_hinstance); + DestroyWindow(m_hwnd); +} + +WelcomeState::WelcomeState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(installer) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); +} void WelcomeState::enact() { - m_installer->showFrame(Installer::FrameEnums::WELCOME_FRAME); - m_installer->showButtons(false, true, true); + m_installer->showButtons(false, true, true); + m_installer->enableButtons(false, true, true); } void WelcomeState::pressPrevious() @@ -19,11 +46,49 @@ void WelcomeState::pressPrevious() void WelcomeState::pressNext() { - m_installer->setState(new AgreementState(m_installer)); + m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); } void WelcomeState::pressClose() { // No new screen PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (WelcomeState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = ptr->m_installer->m_mfStrings[L"name"] + L" " + ptr->m_installer->m_mfStrings[L"version"]; + if (ptr->m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); + graphics.DrawString(ptr->m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, 620, 300), &format, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.h b/src/nStaller/States/WelcomeState.h index 17a03f3..bc15117 100644 --- a/src/nStaller/States/WelcomeState.h +++ b/src/nStaller/States/WelcomeState.h @@ -6,11 +6,11 @@ /** This state encapuslates the "Welcome - Screen" state. */ -class WelcomeState : public State { +class WelcomeState : public FrameState { public: // Public (de)Constructors - ~WelcomeState() = default; - WelcomeState(Installer * installer); + ~WelcomeState(); + WelcomeState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); // Public Interface Implementations diff --git a/src/nStaller/manifest.nman b/src/nStaller/manifest.nman index e69de29..ac1382f 100644 --- a/src/nStaller/manifest.nman +++ b/src/nStaller/manifest.nman @@ -0,0 +1,11 @@ +name "Empty" + +version "1.2.3.4" + +description +"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + +eula +"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers. + +This software is provided as-is, use it at your own risk." \ No newline at end of file From 74d385300dfa41e81a50cbe59188adbeb5d2ecc0 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 15 Apr 2019 13:35:31 -0500 Subject: [PATCH 19/44] Bug fixes - Fixed checkbox appearance on finish screen - Fixed manifest overwrite bug (must keep an empty stock-manifest for now) --- src/nStaller/Installer.cpp | 2 -- src/nStaller/States/AgreementState.cpp | 8 ++++++-- src/nStaller/States/FinishState.cpp | 12 +++++++----- src/nStaller/manifest.nman | 11 ----------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index f9f8312..17b5720 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -31,8 +31,6 @@ Installer::Installer() // Cycle through every line, inserting attributes into the manifest map std::wstring attrib, value; while (ss >> attrib && ss >> std::quoted(value)) { - // Yes, this will leak - // still doesn't work wchar_t * k = new wchar_t[attrib.length() + 1]; wcscpy_s(k, attrib.length() + 1, attrib.data()); m_mfStrings[k] = value; diff --git a/src/nStaller/States/AgreementState.cpp b/src/nStaller/States/AgreementState.cpp index af489bb..4183524 100644 --- a/src/nStaller/States/AgreementState.cpp +++ b/src/nStaller/States/AgreementState.cpp @@ -35,8 +35,12 @@ AgreementState::AgreementState(Installer * installer, const HINSTANCE hInstance, setVisible(false); // Create eula - m_log = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 125, m_hwnd, NULL, hInstance, NULL); - SetWindowTextW(m_log, m_installer->m_mfStrings[L"eula"].c_str()); + m_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"edit", m_installer->m_mfStrings[L"eula"].c_str(), WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 125, m_hwnd, NULL, hInstance, NULL); + if (m_installer->m_mfStrings[L"eula"].empty()) + SetWindowTextW(m_log, + L"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers.\r\n" + L"This software is provided as - is, use it at your own risk." + ); // Create checkboxes m_checkYes = CreateWindow("Button", "I accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, (rc.bottom - rc.top) - 40, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp index 87c1333..827616c 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/States/FinishState.cpp @@ -36,7 +36,7 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const setVisible(false); // Create checkbox - m_checkbox = CreateWindow("Button", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | WS_BORDER | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 15, 15, m_hwnd, (HMENU)1, hInstance, NULL); + m_checkbox = CreateWindow("Button", "Show installation directory on close", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); CheckDlgButton(m_hwnd, 1, BST_CHECKED); } @@ -82,19 +82,21 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l // Preparing Fonts FontFamily fontFamily(L"Segoe UI"); Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); // Draw Text graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Show installation directory on close.", -1, ®Font, PointF{ 30, 147 }, &blackBrush); + graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); EndPaint(hWnd, &ps); return S_OK; } else if (message == WM_CTLCOLORSTATIC) { + // Make checkbox text background color transparent + if (HWND(lParam) == ptr->m_checkbox) { + SetBkMode(HDC(wParam), TRANSPARENT); + return (LRESULT)GetStockObject(NULL_BRUSH); + } // Make log color white SetBkColor(HDC(wParam), RGB(255, 255, 255)); return (LRESULT)GetStockObject(WHITE_BRUSH); diff --git a/src/nStaller/manifest.nman b/src/nStaller/manifest.nman index ac1382f..e69de29 100644 --- a/src/nStaller/manifest.nman +++ b/src/nStaller/manifest.nman @@ -1,11 +0,0 @@ -name "Empty" - -version "1.2.3.4" - -description -"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - -eula -"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers. - -This software is provided as-is, use it at your own risk." \ No newline at end of file From 3c826237041304b5c90f1287ad52d9583c4973c4 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 15 Apr 2019 16:13:41 -0500 Subject: [PATCH 20/44] Adding packaging exclusion criteria CompressDirectory now can skip files matching a file path or extension. --- src/DirectoryTools.cpp | 42 ++++++++++++++++++------ src/DirectoryTools.h | 4 ++- src/nSuite/Commands/InstallerCommand.cpp | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 993340c..24098db 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -10,7 +10,7 @@ #include -bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount) +bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount, const std::vector & exclusions) { // Variables Threader threader; @@ -35,17 +35,39 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer // and make a list containing all relevant files and their attributes size_t archiveSize(0ull); for each (const auto & entry in directoryArray) { + const auto extension = std::filesystem::path(entry).extension(); auto path = entry.path().string(); path = path.substr(absolute_path_length, path.size() - absolute_path_length); - const auto pathSize = path.size(); - - const size_t unitSize = - size_t(sizeof(size_t)) + // size of path size variable in bytes - pathSize + // the actual path data - size_t(sizeof(size_t)) + // size of the file size variable in bytes - entry.file_size(); // the actual file data - archiveSize += unitSize; - files.push_back({ entry.path().string(), path, entry.file_size(), unitSize }); + bool useEntry = true; + for each (const auto & excl in exclusions) { + if (excl.empty()) + continue; + if (excl[0] == '\\') { + // Compare Paths + if (path == excl) { + useEntry = false; + break; + } + } + else if (excl[0] == '.') { + // Compare Extensions + if (extension == excl) { + useEntry = false; + break; + } + } + } + if (useEntry) { + const auto pathSize = path.size(); + + const size_t unitSize = + size_t(sizeof(size_t)) + // size of path size variable in bytes + pathSize + // the actual path data + size_t(sizeof(size_t)) + // size of the file size variable in bytes + entry.file_size(); // the actual file data + archiveSize += unitSize; + files.push_back({ entry.path().string(), path, entry.file_size(), unitSize }); + } } fileCount = files.size(); diff --git a/src/DirectoryTools.h b/src/DirectoryTools.h index 23075ab..3c3c776 100644 --- a/src/DirectoryTools.h +++ b/src/DirectoryTools.h @@ -3,6 +3,7 @@ #define DIRECTORY_TOOLS_H #include +#include /** Namespace to keep directory-related operations grouped together. */ @@ -13,8 +14,9 @@ namespace DRT { @param packBuffer pointer to the destination buffer, which will hold compressed contents. @param packSize reference updated with the size in bytes of the compressed destinationBuffer. @param fileCount reference updated with the number of files compressed into the package. + @param exclusions optional list of filenames/types to avoid packaging. Strings starting with "\\" match an entire path, "." match extension. @return true if compression success, false otherwise. */ - bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount); + bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount, const std::vector & exclusions = std::vector()); /** Decompresses a packaged buffer into a destination directory. @param dstDirectory the absolute path to the directory to compress. @param packBuffer the buffer containing the compressed contents. diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index baad1d4..cc39ed0 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -46,7 +46,7 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount, {"\\manifest.nman"})) exit_program("Cannot create installer from the directory specified, aborting...\r\n"); // Acquire installer resource From 01d79ebedbe18192f5f285e6c62f43c8fccf2898 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 15 Apr 2019 20:36:54 -0500 Subject: [PATCH 21/44] Added desktop/startmenu shortcuts Added desktop/startmenu shortcuts to installer/manifest --- src/Common.h | 56 ++++++++++++++- src/nStaller/Installer.cpp | 10 --- src/nStaller/Installer.h | 8 +-- src/nStaller/States/FinishState.cpp | 108 ++++++++++++++++++++++++++-- src/nStaller/States/FinishState.h | 4 ++ src/nStaller/nStaller.cpp | 8 +-- 6 files changed, 166 insertions(+), 28 deletions(-) diff --git a/src/Common.h b/src/Common.h index 1b200c7..afaeae3 100644 --- a/src/Common.h +++ b/src/Common.h @@ -7,11 +7,12 @@ #include #include #include +#include #include +#include #include #include - /** Changes an input string to lower case, and returns it. @param string the input string. @return lower case version of the string. */ @@ -52,6 +53,37 @@ inline static std::string from_wideString(const std::wstring & wstr) return std::string(); } +/** Creates a shortcut file for the paths chosen. +@param srcPath path to the target file that the shortcut will link to. +@param wrkPath path to the working directory for the shortcut. +@param dstPath path to where the shortcut should be placed. */ +inline static void create_shortcut(const std::string & srcPath, const std::string & wrkPath, const std::string & dstPath) +{ + IShellLink* psl; + if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl))) { + IPersistFile* ppf; + + // Set the path to the shortcut target and add the description. + psl->SetPath(srcPath.c_str()); + psl->SetWorkingDirectory(wrkPath.c_str()); + psl->SetIconLocation(srcPath.c_str(), 0); + + // Query IShellLink for the IPersistFile interface, used for saving the + // shortcut in persistent storage. + if (SUCCEEDED(psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf))) { + WCHAR wsz[MAX_PATH]; + + // Ensure that the string is Unicode. + MultiByteToWideChar(CP_ACP, 0, (dstPath + ".lnk").c_str(), -1, wsz, MAX_PATH); + + // Save the link by calling IPersistFile::Save. + ppf->Save(wsz, TRUE); + ppf->Release(); + } + psl->Release(); + } +} + /** Increment a pointer's address by the offset provided. @param ptr the pointer to increment by the offset amount. @param offset the offset amount to apply to the pointer's address. @@ -83,6 +115,28 @@ inline static auto get_file_paths(const std::string & directory) return paths; } +/** Retrieve the start menu directory. +@return path to the user's start menu. */ +inline static std::string get_users_startmenu() +{ + // Get the running directory + char cPath[FILENAME_MAX]; + if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, cPath))) + return std::string(cPath); + return std::string(); +} + +/** Retrieve the desktop directory. +@return path to the user's desktop. */ +inline static std::string get_users_desktop() +{ + // Get the running directory + char cPath[FILENAME_MAX]; + if (SHGetSpecialFolderPathA(HWND_DESKTOP, cPath, CSIDL_DESKTOP, FALSE)) + return std::string(cPath); + return std::string(); +} + /** Retrieve the directory this executable is running out-of. @return the current directory of this program. */ inline static std::string get_current_directory() diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 17b5720..5362c73 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -170,16 +170,6 @@ void Installer::setDirectory(const std::string & directory) } } -bool Installer::shouldShowDirectory() const -{ - return m_showDirectoryOnClose && m_valid && m_finished; -} - -void Installer::showDirectoryOnClose(const bool & show) -{ - m_showDirectoryOnClose = show; -} - char * Installer::getPackagePointer() const { return m_packagePtr; diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index a7a333a..35239e8 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -42,12 +42,6 @@ class Installer { /** Sets a new installation directory. @param directory new installation directory. */ void setDirectory(const std::string & directory); - /** Retrieve if the chosen directory should show when the installer closes. - @return true if should show, false otherwise. */ - bool shouldShowDirectory() const; - /** Set if the chosen directory should show when the installer closes. - @param show whether or not the directory should show. */ - void showDirectoryOnClose(const bool & show); /** Retrieves the pointer to the compressed packaged contents. @return the package pointer (offset of folder name data). */ char * getPackagePointer() const; @@ -98,7 +92,7 @@ class Installer { // Private Attributes Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; - bool m_valid = true, m_showDirectoryOnClose = true, m_finished = false; + bool m_valid = true, m_finished = false; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; StateEnums m_currentIndex = WELCOME_STATE; diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp index 827616c..698d41d 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/States/FinishState.cpp @@ -1,5 +1,8 @@ #include "FinishState.h" +#include "Common.h" #include "../Installer.h" +#include +#include #include #include @@ -11,6 +14,8 @@ FinishState::~FinishState() UnregisterClass("FINISH_STATE", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_checkbox); + for each (auto checkboxHandle in m_shortcutCheckboxes) + DestroyWindow(checkboxHandle); } FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) @@ -35,9 +40,62 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); - // Create checkbox + // Create checkboxes m_checkbox = CreateWindow("Button", "Show installation directory on close", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); CheckDlgButton(m_hwnd, 1, BST_CHECKED); + + // Shortcuts + auto desktopStrings = m_installer->m_mfStrings[L"shortcut"], startmenuStrings = m_installer->m_mfStrings[L"startmenu"]; + size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; + m_shortcutCheckboxes.reserve(numD + numS); + m_shortcuts_d.reserve(numD + numS); + size_t last = 0; + if (!desktopStrings.empty()) + for (size_t x = 0; x < numD; ++x) { + // Find end of shortcut + auto nextComma = desktopStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = desktopStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + m_shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) + last++; + } + last = 0; + if (!startmenuStrings.empty()) + for (size_t x = 0; x < numS; ++x) { + // Find end of shortcut + auto nextComma = startmenuStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = startmenuStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + m_shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) + last++; + } + int vertical = 170, checkIndex = 2; + for each (const auto & shortcut in m_shortcuts_d) { + const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the Desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); + vertical += 20; + checkIndex++; + } + for each (const auto & shortcut in m_shortcuts_s) { + const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); + vertical += 20; + checkIndex++; + } } void FinishState::enact() @@ -59,7 +117,41 @@ void FinishState::pressNext() void FinishState::pressClose() { - // No new screen + // Open the installation directory + if user wants it to + if (IsDlgButtonChecked(m_hwnd, 1)) + ShellExecute(NULL, "open", m_installer->getDirectory().c_str(), NULL, NULL, SW_SHOWDEFAULT); + + // Create Shortcuts + int x = 2; + for each (const auto & shortcut in m_shortcuts_d) { + if (IsDlgButtonChecked(m_hwnd, x)) { + std::error_code ec; + const auto nonwideShortcut = from_wideString(shortcut); + auto instDir = m_installer->getDirectory(); + auto srcPath = instDir; + if (srcPath.back() == '\\') + srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); + srcPath += nonwideShortcut; + const auto dstPath = get_users_desktop() + "\\" + std::filesystem::path(srcPath).filename().string(); + create_shortcut(srcPath, instDir, dstPath); + } + x++; + } + for each (const auto & shortcut in m_shortcuts_s) { + if (IsDlgButtonChecked(m_hwnd, x)) { + std::error_code ec; + const auto nonwideShortcut = from_wideString(shortcut); + auto instDir = m_installer->getDirectory(); + auto srcPath = instDir; + if (srcPath.back() == '\\') + srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); + srcPath += nonwideShortcut; + const auto dstPath = get_users_startmenu() + "\\" + std::filesystem::path(srcPath).filename().string(); + create_shortcut(srcPath, instDir, dstPath); + } + x++; + } + PostQuitMessage(0); } @@ -93,7 +185,14 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l } else if (message == WM_CTLCOLORSTATIC) { // Make checkbox text background color transparent - if (HWND(lParam) == ptr->m_checkbox) { + const auto handle = HWND(lParam); + bool isCheckbox = handle == ptr->m_checkbox; + for each (auto chkHandle in ptr->m_shortcutCheckboxes) + if (handle == chkHandle) { + isCheckbox = true; + break; + } + if (isCheckbox) { SetBkMode(HDC(wParam), TRANSPARENT); return (LRESULT)GetStockObject(NULL_BRUSH); } @@ -104,8 +203,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); if (notification == BN_CLICKED) { - BOOL checked = IsDlgButtonChecked(hWnd, 1); - ptr->m_installer->showDirectoryOnClose(checked); + ptr->m_showDirectory = IsDlgButtonChecked(hWnd, 1); return S_OK; } } diff --git a/src/nStaller/States/FinishState.h b/src/nStaller/States/FinishState.h index dac6f09..3906e8f 100644 --- a/src/nStaller/States/FinishState.h +++ b/src/nStaller/States/FinishState.h @@ -3,6 +3,7 @@ #define FINISHSTATE_H #include "State.h" +#include /** This state encapuslates the "Finished - Screen" state. */ @@ -22,6 +23,9 @@ class FinishState: public FrameState { // Public Attributes HWND m_checkbox = nullptr; + bool m_showDirectory = true; + std::vector m_shortcutCheckboxes; + std::vector m_shortcuts_d, m_shortcuts_s; }; #endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp index 25f0a06..773bebd 100644 --- a/src/nStaller/nStaller.cpp +++ b/src/nStaller/nStaller.cpp @@ -7,6 +7,7 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) { + CoInitialize(NULL); Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); @@ -18,11 +19,8 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ TranslateMessage(&msg); DispatchMessage(&msg); } - - // Open the installation directory if finished successfully + if user wants it to - if (installer.shouldShowDirectory()) - ShellExecute(NULL, "open", installer.getDirectory().c_str(), NULL, NULL, SW_SHOWDEFAULT); - + // Close + CoUninitialize(); return (int)msg.wParam; } \ No newline at end of file From 9107959ade8a7626eaf3c1cee7ed1f4cc6b8e472 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Tue, 16 Apr 2019 16:43:48 -0500 Subject: [PATCH 22/44] Added an uninstaller Added an uninstaller. Currently able to delete everything but the root folder it seems Later I'll add the registry to link the uninstaller to the program. --- CMakeLists.txt | 1 + src/Common.h | 7 +- src/nStaller/CMakeLists.txt | 3 + src/nStaller/Installer.cpp | 8 +- src/nStaller/States/FailState.cpp | 2 +- src/nStaller/States/FinishState.cpp | 7 +- src/nStaller/States/InstallState.cpp | 53 +++- src/nStaller/nStaller.rc | Bin 3100 -> 3412 bytes src/resource.h | 5 +- src/unStaller/CMakeLists.txt | 58 ++++ src/unStaller/States/FailState.cpp | 131 ++++++++ src/unStaller/States/FailState.h | 28 ++ src/unStaller/States/FinishState.cpp | 92 ++++++ src/unStaller/States/FinishState.h | 23 ++ src/unStaller/States/State.h | 53 ++++ src/unStaller/States/UninstallState.cpp | 199 ++++++++++++ src/unStaller/States/UninstallState.h | 36 +++ src/unStaller/States/WelcomeState.cpp | 94 ++++++ src/unStaller/States/WelcomeState.h | 23 ++ src/unStaller/Uninstaller.cpp | 282 ++++++++++++++++++ src/unStaller/Uninstaller.h | 88 ++++++ src/unStaller/Uninstaller.rc | Bin 0 -> 3038 bytes src/unStaller/icon.ico | Bin 0 -> 32888 bytes src/unStaller/manifest.nman | 0 .../unStaller.dir/Release/Uninstaller.res | Bin 0 -> 33960 bytes 25 files changed, 1170 insertions(+), 23 deletions(-) create mode 100644 src/unStaller/CMakeLists.txt create mode 100644 src/unStaller/States/FailState.cpp create mode 100644 src/unStaller/States/FailState.h create mode 100644 src/unStaller/States/FinishState.cpp create mode 100644 src/unStaller/States/FinishState.h create mode 100644 src/unStaller/States/State.h create mode 100644 src/unStaller/States/UninstallState.cpp create mode 100644 src/unStaller/States/UninstallState.h create mode 100644 src/unStaller/States/WelcomeState.cpp create mode 100644 src/unStaller/States/WelcomeState.h create mode 100644 src/unStaller/Uninstaller.cpp create mode 100644 src/unStaller/Uninstaller.h create mode 100644 src/unStaller/Uninstaller.rc create mode 100644 src/unStaller/icon.ico create mode 100644 src/unStaller/manifest.nman create mode 100644 src/unStaller/unStaller.dir/Release/Uninstaller.res diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ac6389..66f0e6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ link_libraries( ) # add all sub-projects and plugins here +add_subdirectory( "src/unStaller" ) add_subdirectory( "src/nStaller" ) add_subdirectory( "src/nUpdater" ) add_subdirectory( "src/nSuite" ) diff --git a/src/Common.h b/src/Common.h index afaeae3..0fa2e41 100644 --- a/src/Common.h +++ b/src/Common.h @@ -109,9 +109,10 @@ inline static void sanitize_path(std::string & path) inline static auto get_file_paths(const std::string & directory) { std::vector paths; - for (const auto & entry : std::filesystem::recursive_directory_iterator(directory)) - if (entry.is_regular_file()) - paths.emplace_back(entry); + if (std::filesystem::is_directory(directory)) + for (const auto & entry : std::filesystem::recursive_directory_iterator(directory)) + if (entry.is_regular_file()) + paths.emplace_back(entry); return paths; } diff --git a/src/nStaller/CMakeLists.txt b/src/nStaller/CMakeLists.txt index 1a6440e..3c1447a 100644 --- a/src/nStaller/CMakeLists.txt +++ b/src/nStaller/CMakeLists.txt @@ -34,6 +34,9 @@ target_link_libraries(${Module} "Shlwapi.lib" ) +# This module requires the uninstaller to be built first +add_dependencies(nStaller unStaller) + # Set windows/visual studio settings set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") target_compile_Definitions(${Module} PRIVATE $<$:DEBUG>) diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 5362c73..5a1e5ec 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -4,11 +4,7 @@ #include #include -// Starting State -#include "States/WelcomeState.h" -#include "States/FailState.h" - -// Frames used in this GUI application +// States used in this GUI application #include "States/WelcomeState.h" #include "States/AgreementState.h" #include "States/DirectoryState.h" @@ -278,7 +274,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); SolidBrush greyBrush(Color(255, 127, 127, 127)); SolidBrush blueishBrush(Color(255, 100, 125, 175)); - graphics.DrawString(L"This custom installer was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 180, 475 }, &blueishBrush); EndPaint(hWnd, &ps); diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp index b35df1a..0f0b6ef 100644 --- a/src/nStaller/States/FailState.cpp +++ b/src/nStaller/States/FailState.cpp @@ -95,7 +95,7 @@ void FailState::pressClose() static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + //auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); if (message == WM_PAINT) { PAINTSTRUCT ps; Graphics graphics(BeginPaint(hWnd, &ps)); diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/States/FinishState.cpp index 698d41d..052e2a8 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/States/FinishState.cpp @@ -45,10 +45,11 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const CheckDlgButton(m_hwnd, 1, BST_CHECKED); // Shortcuts - auto desktopStrings = m_installer->m_mfStrings[L"shortcut"], startmenuStrings = m_installer->m_mfStrings[L"startmenu"]; + const auto desktopStrings = m_installer->m_mfStrings[L"shortcut"], startmenuStrings = m_installer->m_mfStrings[L"startmenu"]; size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; m_shortcutCheckboxes.reserve(numD + numS); m_shortcuts_d.reserve(numD + numS); + m_shortcuts_s.reserve(numD + numS); size_t last = 0; if (!desktopStrings.empty()) for (size_t x = 0; x < numD; ++x) { @@ -84,14 +85,14 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const int vertical = 170, checkIndex = 2; for each (const auto & shortcut in m_shortcuts_d) { const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); - m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the Desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); vertical += 20; checkIndex++; } for each (const auto & shortcut in m_shortcuts_s) { const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); - m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start-menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); vertical += 20; checkIndex++; diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp index 7183223..4890dda 100644 --- a/src/nStaller/States/InstallState.cpp +++ b/src/nStaller/States/InstallState.cpp @@ -2,8 +2,11 @@ #include "Common.h" #include "BufferTools.h" #include "DirectoryTools.h" +#include "Resource.h" #include "../Installer.h" #include +#include +#include static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); @@ -62,14 +65,48 @@ void InstallState::enact() m_installer->enableButtons(false, false, false); m_thread = std::thread([&]() { - // Unpackage using the rest of the resource file - size_t byteCount(0ull), fileCount(0ull); - auto directory = m_installer->getDirectory(); - sanitize_path(directory); - if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getCompressedPackageSize(), byteCount, fileCount)) - m_installer->invalidate(); - else - m_installer->enableButtons(false, true, false); + // Acquire the uninstaller resource + Resource uninstaller(IDR_UNINSTALLER, "UNINSTALLER"), manifest(IDR_MANIFEST, "MANIFEST"); + if (!uninstaller.exists()) { + TaskLogger::PushText("Cannot access installer resource, aborting...\r\n"); + m_installer->setState(Installer::FAIL_STATE); + } + else { + // Unpackage using the rest of the resource file + size_t byteCount(0ull), fileCount(0ull); + auto directory = m_installer->getDirectory(); + sanitize_path(directory); + if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getCompressedPackageSize(), byteCount, fileCount)) + m_installer->invalidate(); + else { + // Write uninstaller to disk + const auto uninstallerPath = m_installer->getDirectory() + "\\uninstaller.exe"; + std::filesystem::create_directories(std::filesystem::path(uninstallerPath).parent_path()); + std::ofstream file(uninstallerPath, std::ios::binary | std::ios::out); + if (!file.is_open()) { + TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); + m_installer->invalidate(); + } + TaskLogger::PushText("Writing Uninstaller:\"" + uninstallerPath + "\"\r\n"); + file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); + file.close(); + + // Update uninstaller's resources + std::string newDir = std::regex_replace(directory, std::regex("\\\\"), "\\\\"); + const std::string newManifest( + std::string(reinterpret_cast(manifest.getPtr()), manifest.getSize()) + + "\r\ndirectory \"" + newDir + "\"" + ); + auto handle = BeginUpdateResource(uninstallerPath.c_str(), false); + if (!(bool)UpdateResource(handle, "MANIFEST", MAKEINTRESOURCE(IDR_MANIFEST), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)newManifest.c_str(), (DWORD)newManifest.size())) { + TaskLogger::PushText("Cannot write manifest contents to the uninstaller, aborting...\r\n"); + m_installer->invalidate(); + } + EndUpdateResource(handle, FALSE); + + m_installer->enableButtons(false, true, false); + } + } }); m_thread.detach(); } diff --git a/src/nStaller/nStaller.rc b/src/nStaller/nStaller.rc index a53f56f6e7c0315bda8dfb16570ad1d7f677a32c..8cd2c661b975d92499b71555a2b8ea4efe79a566 100644 GIT binary patch delta 284 zcmbOuaYbswET+j4TzvI@3@!|=3{DK84DJlP3|tJJKz:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" + LINK_FLAGS "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\"" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/unStaller/States/FailState.cpp b/src/unStaller/States/FailState.cpp new file mode 100644 index 0000000..9b2ca21 --- /dev/null +++ b/src/unStaller/States/FailState.cpp @@ -0,0 +1,131 @@ +#include "FailState.h" +#include "Common.h" +#include "TaskLogger.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FailState::~FailState() +{ + UnregisterClass("FAIL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +FailState::FailState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(uninstaller) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); +} + +void FailState::enact() +{ + m_uninstaller->showButtons(false, false, true); + m_uninstaller->enableButtons(false, false, true); + + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Uninstaller error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); +} + +void FailState::pressPrevious() +{ + // Should never happen +} + +void FailState::pressNext() +{ + // Should never happen +} + +void FailState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + //auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/States/FailState.h b/src/unStaller/States/FailState.h new file mode 100644 index 0000000..98bca6f --- /dev/null +++ b/src/unStaller/States/FailState.h @@ -0,0 +1,28 @@ +#pragma once +#ifndef FAILSTATE_H +#define FAILSTATE_H + +#include "State.h" + + +/** This state encapuslates the "Failure - Screen" state. */ +class FailState: public FrameState { +public: + // Public (de)Constructors + ~FailState(); + FailState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Interface Implementations + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/FinishState.cpp b/src/unStaller/States/FinishState.cpp new file mode 100644 index 0000000..41c99f1 --- /dev/null +++ b/src/unStaller/States/FinishState.cpp @@ -0,0 +1,92 @@ +#include "FinishState.h" +#include "Common.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +FinishState::~FinishState() +{ + UnregisterClass("FINISH_STATE", m_hinstance); + DestroyWindow(m_hwnd); +} + +FinishState::FinishState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(uninstaller) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FINISH_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); +} + +void FinishState::enact() +{ + m_uninstaller->showButtons(false, false, true); + m_uninstaller->enableButtons(false, false, true); + m_uninstaller->finish(); +} + +void FinishState::pressPrevious() +{ + // Should never happen +} + +void FinishState::pressNext() +{ + // Should never happen +} + +void FinishState::pressClose() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (FinishState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/States/FinishState.h b/src/unStaller/States/FinishState.h new file mode 100644 index 0000000..bdb8a1a --- /dev/null +++ b/src/unStaller/States/FinishState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef FINISHSTATE_H +#define FINISHSTATE_H + +#include "State.h" + + +/** This state encapuslates the "Finished - Screen" state. */ +class FinishState: public FrameState { +public: + // Public (de)Constructors + ~FinishState(); + FinishState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Interface Implementations + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/State.h b/src/unStaller/States/State.h new file mode 100644 index 0000000..bf90427 --- /dev/null +++ b/src/unStaller/States/State.h @@ -0,0 +1,53 @@ +#pragma once +#ifndef STATE_H +#define STATE_H + +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + + +using namespace Gdiplus; +class Uninstaller; + +/**Encapsulation of a windows GDI 'window' object, for a particular state of the application. */ +class FrameState { +public: + // Public (de)Constructors + FrameState(Uninstaller * uninstaller) : m_uninstaller(uninstaller) {} + + + // Public Methods + /** Sets the visibility & enable state of this window. + @param state whether or not to show and activate this window. */ + void setVisible(const bool & state) { + ShowWindow(m_hwnd, state); + EnableWindow(m_hwnd, state); + } + + + // Public Interface Declarations + /** Trigger this state to perform its screen action. */ + virtual void enact() = 0; + /** Cause this state to process the "previous" action. */ + virtual void pressPrevious() = 0; + /** Cause this state to process the "next" action. */ + virtual void pressNext() = 0; + /** Cause this state to process the "close" action. */ + virtual void pressClose() = 0; + + + // Public Attributes + Uninstaller * m_uninstaller = nullptr; + HWND m_hwnd = nullptr; + + +protected: + // Private Attributes + WNDCLASSEX m_wcex; + HINSTANCE m_hinstance; +}; + +#endif // STATE_H \ No newline at end of file diff --git a/src/unStaller/States/UninstallState.cpp b/src/unStaller/States/UninstallState.cpp new file mode 100644 index 0000000..d50878b --- /dev/null +++ b/src/unStaller/States/UninstallState.cpp @@ -0,0 +1,199 @@ +#include "UninstallState.h" +#include "Common.h" +#include "BufferTools.h" +#include "DirectoryTools.h" +#include "../Uninstaller.h" +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +UninstallState::~UninstallState() +{ + UnregisterClass("UNINSTALL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +UninstallState::UninstallState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(uninstaller) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "UNINSTALL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("UNINSTALL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + m_progress = std::to_wstring(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + L"%"; + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + }); +} + +void UninstallState::enact() +{ + m_uninstaller->showButtons(false, true, true); + m_uninstaller->enableButtons(false, false, false); + + m_thread = std::thread([&]() { + const auto directory = from_wideString(m_uninstaller->getDirectory()); + + // Find all installed files + const auto entries = get_file_paths(directory); + + // Find all shortcuts + const auto desktopStrings = m_uninstaller->m_mfStrings[L"shortcut"], startmenuStrings = m_uninstaller->m_mfStrings[L"startmenu"]; + size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; + std::vector shortcuts_d, shortcuts_s; + shortcuts_d.reserve(numD + numS); + shortcuts_s.reserve(numD + numS); + size_t last = 0; + if (!desktopStrings.empty()) + for (size_t x = 0; x < numD; ++x) { + // Find end of shortcut + auto nextComma = desktopStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = desktopStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) + last++; + } + last = 0; + if (!startmenuStrings.empty()) + for (size_t x = 0; x < numS; ++x) { + // Find end of shortcut + auto nextComma = startmenuStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = startmenuStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) + last++; + } + + // Set progress bar range to include all files + shortcuts + 1 (cleanup step) + TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 1); + size_t progress = 0ull; + + // Remove all files in the installation folder, list them + std::error_code er; + if (!entries.size()) + TaskLogger::PushText("Already uninstalled / no files found.\r\n"); + else { + for each (const auto & entry in entries) { + TaskLogger::PushText("Deleting file: \"" + entry.path().string() + "\"\r\n"); + std::filesystem::remove(entry, er); + TaskLogger::SetProgress(++progress); + } + } + + // Remove all shortcuts + for each (const auto & shortcut in shortcuts_d) { + const auto path = get_users_desktop() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting desktop shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + for each (const auto & shortcut in shortcuts_s) { + const auto path = get_users_startmenu() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting start-menu shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + + // Clean up whatever's left (empty folders) + std::filesystem::remove_all(directory, er); + TaskLogger::SetProgress(++progress); + + m_uninstaller->enableButtons(false, true, false); + }); + m_thread.detach(); +} + +void UninstallState::pressPrevious() +{ + // Should never happen +} + +void UninstallState::pressNext() +{ + m_uninstaller->setState(Uninstaller::FINISH_STATE); +} + +void UninstallState::pressClose() +{ + // Should never happen +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (UninstallState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstalling", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/States/UninstallState.h b/src/unStaller/States/UninstallState.h new file mode 100644 index 0000000..1c61a7a --- /dev/null +++ b/src/unStaller/States/UninstallState.h @@ -0,0 +1,36 @@ +#pragma once +#ifndef UNINSTALLSTATE_H +#define UNINSTALLSTATE_H + +#include "State.h" +#include +#include + + +/** This state encapuslates the "Uninstalling - Screen" state. */ +class UninstallState : public FrameState { +public: + // Public (de)Constructors + ~UninstallState(); + UninstallState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Interface Implementations + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; + std::wstring m_progress = L"0%"; + + +private: + // Private Attributes + std::thread m_thread; +}; + +#endif // UNINSTALLSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/WelcomeState.cpp b/src/unStaller/States/WelcomeState.cpp new file mode 100644 index 0000000..0d0bac3 --- /dev/null +++ b/src/unStaller/States/WelcomeState.cpp @@ -0,0 +1,94 @@ +#include "WelcomeState.h" +#include "../Uninstaller.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +WelcomeState::~WelcomeState() +{ + UnregisterClass("WELCOME_STATE", m_hinstance); + DestroyWindow(m_hwnd); +} + +WelcomeState::WelcomeState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) + : FrameState(uninstaller) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); +} + +void WelcomeState::enact() +{ + m_uninstaller->showButtons(false, true, true); + m_uninstaller->enableButtons(false, true, true); +} + +void WelcomeState::pressPrevious() +{ + // Should never happen +} + +void WelcomeState::pressNext() +{ + m_uninstaller->setState(Uninstaller::StateEnums::UNINSTALL_STATE); +} + +void WelcomeState::pressClose() +{ + // No new screen + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (WelcomeState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, 450), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Uninstallation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = ptr->m_uninstaller->m_mfStrings[L"name"] + L" " + ptr->m_uninstaller->m_mfStrings[L"version"]; + if (ptr->m_uninstaller->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will remove " + nameVer + L" from your computer.").c_str(), -1, ®Font, PointF{ 10, 75 }, &format, &blackBrush); + graphics.DrawString(L"Note: the installation directory for this software will be deleted.\r\nIf there are any files that you wish to preserve, move them before continuing.", -1, ®Font, RectF(10, 400, 620, 300), &format, &blackBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/States/WelcomeState.h b/src/unStaller/States/WelcomeState.h new file mode 100644 index 0000000..1df156d --- /dev/null +++ b/src/unStaller/States/WelcomeState.h @@ -0,0 +1,23 @@ +#pragma once +#ifndef WELCOMESTATE_H +#define WELCOMESTATE_H + +#include "State.h" + + +/** This state encapuslates the "Welcome - Screen" state. */ +class WelcomeState : public FrameState { +public: + // Public (de)Constructors + ~WelcomeState(); + WelcomeState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + + + // Public Interface Implementations + virtual void enact(); + virtual void pressPrevious(); + virtual void pressNext(); + virtual void pressClose(); +}; + +#endif // WELCOMESTATE_H \ No newline at end of file diff --git a/src/unStaller/Uninstaller.cpp b/src/unStaller/Uninstaller.cpp new file mode 100644 index 0000000..075fe92 --- /dev/null +++ b/src/unStaller/Uninstaller.cpp @@ -0,0 +1,282 @@ +#include "Uninstaller.h" +#include "Common.h" +#include "TaskLogger.h" +#include +#include +#include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) + +// States used in this GUI application +#include "States/WelcomeState.h" +#include "States/UninstallState.h" +#include "States/FinishState.h" +#include "States/FailState.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) +{ + CoInitialize(NULL); + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + Uninstaller uninstaller(hInstance); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + +#ifdef NDEBUG + // Delete scraps of the installation directory + if (uninstaller.isValid()) { + std::wstring cmd(L"cmd.exe /C ping 1.1.1.1 -n 1 -w 5000 > Nul & rmdir /q/s \"" + uninstaller.getDirectory()); + cmd.erase(std::find(cmd.begin(), cmd.end(), L'\0'), cmd.end()); + cmd += L"\""; + STARTUPINFOW si = { 0 }; + PROCESS_INFORMATION pi = { 0 }; + + CreateProcessW(NULL, (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + std::error_code er; + std::filesystem::remove_all(uninstaller.getDirectory(), er); + } +#endif + + // Close + CoUninitialize(); + return (int)msg.wParam; +} + +Uninstaller::Uninstaller() + : m_manifest(IDR_MANIFEST, "MANIFEST") +{ + // Process manifest + if (m_manifest.exists()) { + // Create a string stream of the manifest file + std::wstringstream ss; + ss << reinterpret_cast(m_manifest.getPtr()); + + // Cycle through every line, inserting attributes into the manifest map + std::wstring attrib, value; + while (ss >> attrib && ss >> std::quoted(value)) { + wchar_t * k = new wchar_t[attrib.length() + 1]; + wcscpy_s(k, attrib.length() + 1, attrib.data()); + m_mfStrings[k] = value; + } + } +} + +Uninstaller::Uninstaller(const HINSTANCE hInstance) : Uninstaller() +{ + // Ensure that a manifest exists + bool success = true; + if (!m_manifest.exists()) { + TaskLogger::PushText("Critical failure: uninstaller manifest doesn't exist!\r\n"); + success = false; + } + + // Acquire the installation directory + m_directory = m_mfStrings[L"directory"]; + if (m_directory.empty()) + m_directory = to_wideString(get_current_directory()); + + // Create window class + WNDCLASSEX wcex; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, (LPCSTR)IDI_ICON1); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = "Uninstaller"; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + if (!RegisterClassEx(&wcex)) { + TaskLogger::PushText("Critical failure: could not create main window.\r\n"); + success = false; + } + else { + m_window = CreateWindowW( + L"Uninstaller",(m_mfStrings[L"name"] + L" Uninstaller").c_str(), + WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, + 800, 500, + NULL, NULL, hInstance, NULL + ); + + // Create + SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); + m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); + + auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); + RECT rc = { 0, 0, 800, 500 }; + ShowWindow(m_window, true); + UpdateWindow(m_window); + AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); + SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + + // The portions of the screen that change based on input + m_states[WELCOME_STATE] = new WelcomeState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[UNINSTALL_STATE] = new UninstallState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[FINISH_STATE] = new FinishState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[FAIL_STATE] = new FailState(this, hInstance, m_window, { 170,0,800,450 }); + setState(WELCOME_STATE); + } + +#ifndef DEBUG + if (!success) + invalidate(); +#endif +} + +void Uninstaller::invalidate() +{ + setState(FAIL_STATE); + showButtons(false, false, true); + enableButtons(false, false, true); + m_valid = false; +} + +bool Uninstaller::isValid() const +{ + return m_valid; +} + +void Uninstaller::finish() +{ + m_finished = true; +} + +void Uninstaller::setState(const StateEnums & stateIndex) +{ + if (m_valid) { + m_states[m_currentIndex]->setVisible(false); + m_states[stateIndex]->enact(); + m_states[stateIndex]->setVisible(true); + m_currentIndex = stateIndex; + RECT rc = { 0, 0, 160, 500 }; + RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + } +} + +Uninstaller::StateEnums Uninstaller::getCurrentIndex() const +{ + return m_currentIndex; +} + +std::wstring Uninstaller::getDirectory() const +{ + return m_directory; +} + +void Uninstaller::updateButtons(const WORD btnHandle) +{ + if (btnHandle == LOWORD(m_prevBtn)) + m_states[m_currentIndex]->pressPrevious(); + else if (btnHandle == LOWORD(m_nextBtn)) + m_states[m_currentIndex]->pressNext(); + else if (btnHandle == LOWORD(m_exitBtn)) + m_states[m_currentIndex]->pressClose(); + RECT rc = { 0, 0, 160, 500 }; + RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); +} + +void Uninstaller::showButtons(const bool & prev, const bool & next, const bool & close) +{ + if (m_valid) { + ShowWindow(m_prevBtn, prev); + ShowWindow(m_nextBtn, next); + ShowWindow(m_exitBtn, close); + } +} + +void Uninstaller::enableButtons(const bool & prev, const bool & next, const bool & close) +{ + if (m_valid) { + EnableWindow(m_prevBtn, prev); + EnableWindow(m_nextBtn, next); + EnableWindow(m_exitBtn, close); + } +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + auto ptr = (Uninstaller*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) { + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(hWnd, &ps)); + // Draw Background + const LinearGradientBrush backgroundGradient1( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); + + // Draw Steps + const SolidBrush lineBrush(Color(255,100,100,100)); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"Uninstall", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; + const auto frameIndex = (int)ptr->getCurrentIndex(); + for (int x = 0; x < 3; ++x) { + // Draw Circle + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + if (x == 2 && frameIndex == 3) + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20 ); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20 ); + + // Draw Text + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); + + if (x == 1) + vertical_offset = 460; + else + vertical_offset += 50; + } + + // Draw -watermark- + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 180, 475 }, &blueishBrush); + + EndPaint(hWnd, &ps); + return S_OK; + } + else if (message == WM_DESTROY) + PostQuitMessage(0); + else if (message == WM_COMMAND) { + if (HIWORD(wParam) == BN_CLICKED) { + ptr->updateButtons(LOWORD(lParam)); + return S_OK; + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/Uninstaller.h b/src/unStaller/Uninstaller.h new file mode 100644 index 0000000..0f6d9ea --- /dev/null +++ b/src/unStaller/Uninstaller.h @@ -0,0 +1,88 @@ +#pragma once +#ifndef INSTALLER_H +#define INSTALLER_H + +#include "Resource.h" +#include +#include +#include + + +class FrameState; + +/** Encapsulates the logical features of the uninstaller. */ +class Uninstaller { +public: + // Public (de)Constructors + ~Uninstaller() = default; + Uninstaller(const HINSTANCE hInstance); + + + // Public Enumerations + const enum StateEnums { + WELCOME_STATE, UNINSTALL_STATE, FINISH_STATE, FAIL_STATE, + STATE_COUNT + }; + + + // Public Methods + /** When called, invalidates the uninstaller, halting it from progressing. */ + void invalidate(); + /** Returns whether or not the uninstaller has been invalidated. + @return true if valid, false otherwise. */ + bool isValid() const; + /** Flags the installation as complete. */ + void finish(); + /** Make the state identified by the supplied enum as active, deactivating the previous state. + @param stateIndex the new state to use. */ + void setState(const StateEnums & stateIndex); + /** Retrieves the current frame's enumeration. + @return the current frame's index, as an enumeration. */ + StateEnums getCurrentIndex() const; + /** Retrieves the current directory chosen for installation. + @return active installation directory. */ + std::wstring getDirectory() const; + /** Check which button has been active, and perform it's state operation. + @param btnHandle handle to the currently active button. */ + void updateButtons(const WORD btnHandle); + /** Sets the visibility state of all 3 buttons. + @param prev make the 'previous' button visible. + @param next make the 'next' button visible. + @param close make the 'close' button visible. */ + void showButtons(const bool & prev, const bool & next, const bool & close); + /** Sets the enable state of all 3 buttons. + @param prev make the 'previous' button enabled. + @param next make the 'next' button enabled. + @param close make the 'close' button enabled. */ + void enableButtons(const bool & prev, const bool & next, const bool & close); + + + // Public manifest strings + struct compare_string { + bool operator()(const wchar_t * a, const wchar_t * b) const { + return wcscmp(a, b) < 0; + } + }; + std::map m_mfStrings; + + +private: + // Private Constructors + Uninstaller(); + + + // Private Attributes + Resource m_manifest; + std::wstring m_directory = L""; + bool m_valid = true, m_finished = false; + StateEnums m_currentIndex = WELCOME_STATE; + FrameState * m_states[STATE_COUNT]; + HWND + m_window = nullptr, + m_prevBtn = nullptr, + m_nextBtn = nullptr, + m_exitBtn = nullptr; +}; + + +#endif // INSTALLER_H \ No newline at end of file diff --git a/src/unStaller/Uninstaller.rc b/src/unStaller/Uninstaller.rc new file mode 100644 index 0000000000000000000000000000000000000000..c59850fced2e936302cafea88ec2d6182bb1c476 GIT binary patch literal 3038 zcmchZT~8W86o$`joAf_ymzy?e45&A~R6t5xg}_2=VnPtn(zIXzQ%(BY+dl6M%d)VT zN=>tw-Ptqe^F3$I%=3dLD%^yzZ)LBmLdG32Wly5gXTTHA< z!`n;@*Y*y7PwZmhdmBGHL{TP=9&toG?~(e+6Ks`?(b2LR5>?{vAR&tp)N`mEzBN{Y zCB^gA_^*S>C;Px!8@}VM96eS&deU5Xc=!009nvLx#gqJL9)jn99FEDJpZN?X(k*;F zbNr|95$LNb=I9$QXsO07@;&RKr^QT_aL#|7_X+c0Z6%@0mjI4B zQCHV^0xN-vM$rw=+eCkZ*gM1>|CL0d{lp<#!#l;;wo@z@?ZFO+D_U0N&9csv`zrb9 zkh!d%sV1+Xw3HvRi)NtoG=Fi0Uv({Hvl0#b^STf5QP1H*CPgb|S2gi4>qENkt0~Ks zSUyIs=Cb4M251Rf?KQwv$b@(>XZLHL-jbmakqwa;&u?c{pChr|QpIhG>$Z=DV0@Q(Zl&0Ef# zIeL8lHdg}rcG-32eWEK-Q_r#4SlAHuiZ02g{#NVe<)ZY}c1^}rZR==eYvkXnB5Q5; z+R$lMS#&2~Cz}+VS?Lb%g5Y$C=Pn&1yRT!k>HL|ICGFwW9j5;6`-!TgWU7SX7H?C( z)$5t9L%XDA{1~rw1+C?Kb+DsXyV%OFzHJrx M)ZKS8i>tc-0N~Dwr2qf` literal 0 HcmV?d00001 diff --git a/src/unStaller/icon.ico b/src/unStaller/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..24e5ebbf3aa6312a3144ab637e7305ce87e54763 GIT binary patch literal 32888 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 Op!Z_(Qq)NYtN1_W`7y`< literal 0 HcmV?d00001 diff --git a/src/unStaller/manifest.nman b/src/unStaller/manifest.nman new file mode 100644 index 0000000..e69de29 diff --git a/src/unStaller/unStaller.dir/Release/Uninstaller.res b/src/unStaller/unStaller.dir/Release/Uninstaller.res new file mode 100644 index 0000000000000000000000000000000000000000..b2f51aa75b6e013ec53a9b46dc91fa03345d89dc GIT binary patch literal 33960 zcmcG$1zc6n);E66p;NlMJEU8>Lr_3T6$AvNyHi5Cqy;HOKvB9yKtxJJqz@n^U2@3t z-y7xk`27Cwz0ZB#d*AtdzRc{IS!>psS+geg*+3u=2m=5l@)v;Qe+%~ z|E7BLhkc_>$#QE*hYSQW5tWgsi1Vu2t+yZ2toy2sxNoOd6dUbE8qTD>#gBYf;OuO- zk@Z#CjM0RgH4nx~#BwJY4kdudn+ideT5TFjw;K0+PgD&L+z;KYJ#LOA-Av1Gf7-FP=CV^=5#i5 zZpv>~!ia|sR(kVBvKK{=R#65wEVT$eeLL@jRqtqfPpuu(^>c0|_O*nawe~ice*0cK z83RVndm#jGFy3RB1!NQAY521t%H}ll2t6 zwCA>?Zp(2wrnFDDYb{zl{~UjoR%@8~o;cU;Va#A4d_5*$|D#{2Q8QW0&3vZD=Q=Xq zp{H)}D?7L?qdYePOSW+DLp6c7s0BVf;`F6mztAm};CQv9k{T5DA)5~tMNOp2`7!X8Ej6@y zqkmvkpMmws&we+OVN{Kfjmp4&*N5l45XKHO!QN8!kcCRwF@Ml{lvKfgsIVS_{h%+gP7J1K2B@gvaiBHls$6c?#v%mK)k%x48aZiF~Ou%e$ zV>y`bj&y|^vptHXgh2Y225F0KmyYW(8=081-poCFHf|&pS%M1uDD%bU40#aw(|iz0^2watSujyDgRk0*Bh z4A+fhMn})SXSJ1|V7A|!S=5ds|p``5sHbUdTP>_53n=JpcLINtrLh*e1)3r@LK9F}j~UX7S%J=^+lus~Pq~(tfFcZewlFXX>mdAq8eb3{Hr}e=6Bl6u9M+avDXR|85pGzp5 z^Mtb|s|)QWl3!jXqOV{qOU)BZk$cc`_O7My^zw z==)TxCN77Y;!WNDGS6DV3DZJ~Y#t7#g<0}`hr)>wIgi2_y^$@<#+8Lu4Y)lbGrj_g zcv{DnQZZvC8XRNP2U|uCMoH23Qw>7qPGdci!+D9_D~wQMp}RFc?#-;Nlx?|z$Gj%a zv`!zj;2s#8lPOjwdPSQU&92Yeza(obtyw@JgA3zAWY#9#hdC7OgI;CojMPP5s84s_ zM4*H?K{pR3=V6pJ_e{iME}-QZMPW^SK!IS}pq#e^V4hOOr4GlTIQ> z;gGy(wy@(hmJQ{1ucYqgc|WS{ z`gXeq=M_yCcSfw0rj8VbK$9-({kgnpbN6|TS+7xX@?Dg+%T|B6;HzES;|wM3k1modSW}j@D}O@(PvmV*QY2HmmDG z!t#ttmHnOvN?-{YZ@dK`QG#v28X6{)b&{2K#QVGR$HS@d$`dE8Ntp-Ze3dXpv_`(@ z8|SN8*9-@qo5*;pnXK?aznwCk6YytmHm;A~-CjxB@1F8NJF4y+?nP}9(9uK@IJ;l%P%80sf4XR?#u@lEO3mHuX>WG&W+S|QM)<`BhepXRnSR9Eh#8{b zemvB3HuIP&4@w=|<~7?$B_%%ozKTbS9JN;Nbl*&Zcq|KS_fC&sCDWU%cxEa-$R;PR z&tj58H&4U$*G)w|$Ed%*p^30=>TARBVbqozGJrwKjXFfyQM`L)?5mxmyDP7wya`FV zk7f&dI7!ER2T`ZN>HSkHFZ1Q-*gdF1``wPk?OKXQb)$4R&Zae!Z%65k@TMzy+&oQd znyCbf&i3;lH%Ok!^TS>IU5~G@3)4;HJA6Qt*MHPRb)C&4!RidUCSc+zE@P&WQdrdl zt6fNrQJ;A$chil&I5EC_X0egWz$}^OvyZPHgo+OH6xX~X)?>H`u6>fSP9tbo-1Pu; zI*YHi+V@gjbgI!xYVPKn{_8M3Cm7Sh69pjA$ni#aK57M{ec>A1^(PKpdT2|e$ynJ{js$+#qCy_kko z&C`7QK~4UO7@?~j)SIY1oC15*krpyIH?lQW1aI*wrPO(#eQ-8rZy?R&^Ib2delF-X zZLzq-&0TciN#ULfoR0F0*wb;Z``*Vl;<@+Oda6jg7krbBLc{F1D|xeW@^^MmLo#^1 zMQxY~l|1Bmm^f}$#98bNq#=Zu`q$%5epHe8 z*YmP77V*hszOeIQd+J#Nx9Fihs_MO`Dzr`^C{S_G8IbKbW+p|PlPf9#h%#NY zr!p?{0HH!78k@Kswud$_#o^+L-RdT(%C*E>#%W@X+i9!OImD7hLZUr}(-Vh1-&kbn zqpofs7?;YJF{XseF8uj=T=k;9fEk-A?%hUq5^K%ZtQF7H`NazDIBTyci%V02yB#;N zk2Q@nLh&@55Zh$1EZ*MI$mMDw&8!y!%2}Ed1?UZr;o@ z`RM^))MVY9q3sz`>O+0*&}5n?a<+Tmm5Tn}aS^Uw^BwWA&8rIGXR~OLRzvyd>sL=> zlci=ZHWagu&FmP^xut9Hn5d@~7&DuPc_*Ii^-f+>CPp*ecH*3BfKEjH_50I$M6(l=l z-)nm*4wkq|G91jCZ?uebggt9$EH&QtmC{vo93!=Oq_f}tH)#RT^vnG1%)=Xc%^3T`l}~zqNtN?l@v9e+~z<@j9JBmju(2- zS+gQOymiXs=2T*4{qBf9g(#>)yCzxb`_AbRZ!e~kTr!*a^Ouu$rf7|94K_x}Te$=J z`-1Dh;bv{DZ>mKqnq=tln?lVFx8(;{&byeXyw(R^-PkYr!PC>_+Uy&dc3jRw@^Vq3 zAd>HcQ7DOq^&|HwZf)~wnwa!X+t2M?+!H^zy1veFqO3>%=;> zE-!*n7VAlC-{;?1VT1yC(L2X!tVGx2qEc~LJ!a~&6g8q$g!Ke6YB+9_W$i0u@T$5v zz7AI+5E3>uatV}7AP99=DAlX=*>FuyzUjte1x4InuxV&pv4$c<&7bx(szDQ`lkUGv zo~$xy`I@}wYGneP=`xt65&&Oknkz=BZi|N#Orm#{bd1cthfexcQ0kMwwRbrWSnrn6 zE+<%yrk1p4KQvYM>3{odvHpoNf6Z-SCQcK4>puAufppPy&DIRMTq-Ha?xw0Kp%Kl|c1=a|goTYPCe;#RW^hCoYn#DO8TgA? z{IF|P_`&|El1GcPZ{>PtUUSrr$Sh#+i{BS7aH$F+HG`zb;LpmX^f)kG9U4N-Sn4DB zSfh%(@=!A=ewB6upSSe*_D;bl&$UrON>%w@_YX!)e(d&p?QMsXcU-^0JI*-Y z9%K1U>UcPw<_U%+S)HM`xHo)o|H`yv;jl**PVfz1BrK>FU)jeJDbS!FyAW>@Rkm~T zBES%r#qjX?eZm)o_S7Jl@ov!jYD3s|i2S~qE|QP3%K0u6tUe(tBebc| zR~92ZAcyvO`YiZjr7GCNe;CK#Er5SL9Y|qzNlugXmt)?Re!RBVYA=|Fq2pen+)CLQDDZNC_{lO=M^;XI0&*~ko4xHnql1RziT>6}IQ*0WcL9@8KyEt(VcT1)= z*A*l5X`{AMMou=DKsN8mM*7~QxY~X)OqH@`PDZK2ZoZBv8kc@r4c-E%3F$29{(C z6GSQ`;bh-kIjso)~L?KRCy{=3(y5H}Bv@<`HmcF;|3@?w>w`Z38<3GCCm$IVK!2-@w(Vsi31?;%pBJZY3W!pm{i znve^t98xNG4@%x2heokATs3)bYB21BM~)>EY#qu6@q!`0Iz0+7DQrIbsb!GlHr?(* z`a0@6y!KcG&BjfX-;{^Jh-JxCQ<~|hBf?)hjwp~^?7IN!K)s?MvN8|Z#@%gFX;XB10Z7$b4`@VP|_p+ukL=$>c z-FJXyKh2&;@XATL;em%m8x!g$avoQ-S#b<%?-z>NKd?gmZB7+`Sj7e$(o%ZHt9(ey z$$L?^TtD-4G5aTbw1ee~Ee!G)60O7Z*TKe0u0K_xGuMyLko5%yZ*%f?;zh&s&2c{* z>hvQveoA(ULYeM_T>7*u|@Pbvh=lvCgtRrxW`eDGh@Gi;)d&&DGh`MCU@>bqy^ilb${G&-Bo@9uvrQ;~>bFtZFEawf*B(KEYjPZo2#k&={;P<^};sCOZeWC8zvmLTSjFbpfk3=HBHB^weYgRLadPmSV=u> zPU~6!#)^}L;mp5n5-0_p6~avJ@jhQNh}AM(80A(j$>7oISuEB>Yu%Y3%J+Sk&K}S2 z64-6^pqGtoSWRisl{H7@wV5(kRO;`2UqPh|bzd6^J_4D=VWGfCr zd3!5)`2?m2%L2>YQ@h#KGrP7pYRg4~mTC7NZm>nD15{QyE3Dlp~8zdBplb$NadEgl8P(<5DJvUfZcR+#%L}1+71`((I`33>v38DiT(n zyj;W(4=jEk5>3(blSN#@>~4YO7_QHZAlY*JR~EOU0~>;4EF(@=RzHaYW4u7(tw1L$ z39Gsjoj&DP6UvEnSCE8N6Mc;wa2YS_54FPxG;_=0*|ui2*5+1wGm%ZrH*EZ-ANIab zOURxtu1Wj&N52vKXfE6G@#!6pyjdr+&=(|ZGPPpzlAH9j#xGuyr0NU2v3jldZ2J+( zcI`70W1fisr&Bm9wS)(+(cldhYO(WpnGmsSCX<9DSK!y-oqtfmb_9z_i zF9%oID;=au1D}7!2wK!g;>IEK>yB}tj(1)j5U;IJQ(=u{bK)GS`9V?JnN;EOkjE$Q z5&g2w$DI1x@Gi}o$@&16fy!aj2M}~*I}WgMPV1~V3#fu29Z?=(|G=`(W+y;}s?8mo z*!wawtLGk0$vieS$gWJoY zXN-G7-TIE+LCgd`%e~MhPqrV}?fSiQy~MXFdB1G((M&EYBk?p4TZ@@YW90s0$g!p} zd6Cn!x5}uY43M7sn#Uto^)czbWm_>Vpq;)&3CYk!8Vn9xf0?V~+Ib@-sMYs*dwB-7 zR*?s;nX|iUa_%$9>)O>p(i*kvmXg@o-tR;uUcT}B1mA7G<>o2RS%1~(t9$5tWH)Wo zRkhxajrjqZA|l@!>@4O^bE&EhL+7fB7MTET>+$_*5$(Rz9@^dk!^`B;mK|J*S#jzS z=C}4mUi$sSea%X|uo#Q;iN_~aJ8QJ)!S+COxpJQP>-R~H*FABm*Xcy+-Bfd8->|tY zQw>g+s&};Rpk7yGriV`jRVYAr%DTnO4|3<4VltrKi<7>&>Cx8+P$A8+?n$Gg7DhWC ziML~9>4m&B ze-8gP3=KGHQs_Lu?aFTAsVw*2OL`q55C}1==INF-?b3vPz213v@`_BR)r1KqPY4M{ zhKo@=x#hIbmk|TpLB#8H_oh#$=O~%I%cLf&LVW*>f*9MH(N#)W(I*rU{=9>kFfG&Z z++Jc}Hjmw;`e=gNAeHw~=F3$vs*S!H<=E$}<#r)WPP3St%=!k84QB)$^DQ?ak(Dk# zd_zXY0t`+AV(=+~nCXYo65FUy)gkAo-a^}hUnj+LYrKCtA@00Fq{P?RD0ur@OPcn0QHOa~9U+XO$3Aq_p zKJfl}S7@fc!;$kdR+Vw&LSwzgbz!r@MkS39KD379I2ci6WI0Lt#{3J74qfQSmdxl^ zRMsnvVnJ0geZH{jpEZj_o3d3EO@op>=CSQ-bbQ~}d?BsRzl$nz->`O+PiLEH<Wf(&Nbyi}ghdgpov& zB^$}y65{cF1-&G(J}Kio-!(+NHt62HHPh-Twvv2&rAx2!ou`?rGKMy>7A|$F)}zAJ zyAphv#;9h*sg95Cjx!eaSts?TIhq;~j%?t7+;F+|Z zRLiMn>?PxMW!2SlUtMH-V!m5Q@hF8bl9=!zPZkO9qi78i1DCk+J=Xci$O83R{I2>| zJr>H?L;;q@k|f?+fI(}$yN20?JjkeWwJSNJY?ZWT)sAW}aer10vsC)-%BaElLKkz# zw)K19bBFRTzFU;qKfre|Cx>iL2Bq+pVM8Jv{xeI1ZDMOpCGZW^o+EOn*#(oKH*}Ty zkBe~KIU9rx+TWT12d{-L9JlY91Fo|-ho%ecDu^s^IzBSTx>L!=T>16q8^hGS7!?*m z?PTupzDYbYMNZ{fE8^ki!rWEkaED$dlego^qEVE2&a=imev6&OcbSOq6Lf_sRb+KXR>tq7`U$zoAPocWx?y+ zTXt*h=0`i-f99t~A<62tSKyvjRrjNWSCY@KCuN-?&aJK78~J)8r#XG`G!9l;xhNiJ zN2r*AlGM{i$kWrp+BiI@mn}HYfQserD*d#auD$X#N6ChMEon)`E83gAsfzCs=XaV{ zWev+cdOX>^*KN@i$vspD(n4=&xmwT6^XKKUt`X~>e#<33J#G3alr1uurP}73B(jRf zhF{8LTsuGEPuO$wbbCtLrgYCL*0aT~GLeiBb)o7rykacd3}Uv2UgCM{9UP5`8>m9u^BbXYmWO)x7Y&KtJ56&!S00cvSbO6@%JjQ`=2G&P|dWl(#6bT(qmV>H<6O$PE=0Cmi;}u ztE)sKw@%W(OjL#zJn62}y&rILmRUdWh;Bxn&3rO;g=ADPX;9iN(|)gJ^5sebwGXpd zMI4nJ@LC+#><}W_PBfD|H~DpPrQV!$I{IyVDi8Z-;MA64z?vtqk$-1-_}K!@wBURF z*G;2tX`xRTeSW;ho%N~k^q?NzC}X6jk_iYzpNyItJIhI@eU8M`*VmJijnP$K9;+gk z5He$W(YF}uc#BWBxmvB)hVSfyRaTJmp<(snX2yUhQ>f6GtN+{)cWS`I6u;g1jF*F7 zzhl9US3Gbi9-OZrGtFqo>G#rI6cBEpN%uYV9Tw~V>CE&lLFQF%eSt@SdP#Y+NM!!e zIqB@dM&xF`NIku6+RmY-*z@WA7|4@$r!NyLT=l|Eu66}lO2iySJG)(X`%8-}vFMuY zLnpn8nr61VOuN3@<_yXPL@#8J+i$X?rf!_;?2c(HTp6cm5kS`~Ms1EVl35{34J98S z_1<^R8W~~b>;D>G-geI>vW}j%bWPoj^l>q6Uf{h*q|0gKR;9exnb?AI1YgkM3M&+X z7;9hfz8U^-Zr>eCF>GN>L!@;7vtaJwY+J6oR;gtT+Z(+%n2hzWt#)TyJvziTXSYgz z+6U>y-LXc>`Ty|BFkgk;@IgoTSHub3$}ZJedU9}!6>;xSw>ro^CUo3v0Ygnmr1khS zi9u}5$=NVfS{NF2Uly^I!orqp3mZgFs>jc4b5iIX3Wn+{hq|?A=UD`wmOuI<-8VSp zwiQB+stG$Snt}-IB6gfSpXxfmE!sw6VcSgRv>}lcn$*q9trbOgeaB8lD%VEe1f53M zw@N_1VmYki+_pZA`~2ADu!&&55CS=+Ip~w&eWu=FlHf4+!K!i51C6T7;|@#lUjJAL zU@uzz)DjP;a&{-zwZCmr*N>ooULkdeAUrI`+{-gA%wN~lxBmk z4Dj@iQ=D>aWVaTM%g?=JsEe~B+kSq_eQJp>Jh7j|#`dTX-O>@UmI_-m-xNIq`-Eao6gGggYXr(OUPX zZq5oXv>1LZpGL1sS|g(h+}p2)oDyw&w$W}*ihVkdCOOjg%0E}YWYXX5t;*jq%OP=1 ziTin|*f~oBh%FT|yFWQsOG_z>*w*2x+nIn?C}GN9WjmTI<#%x@@jTiQIU+F8sB6hr z!OB0*eP)CP$7yejP=u)HqtpA^VkqvY^=T@K#1v?nH{~q!@tYq`SB-P2o~aCeva|8rK_T&>O*>24qk%y1Lw?KHePmnFR}&Hl zExRQ`0Y|x_Q*FaEIw}!_2D~jMue3Va-!pRdM`vu5ksWqyymPundER#Pj9m2e+|#03 zXSNQwYfF|ahtNw3zYD|Y;qa1x16QqgJLccawtak=ur`Z3>aky;^>mn5((yFl`2Fm@ z-49ZzH&*^m%$!pA;k1~#Qpz_ot-WWd1mL@kHI0Jw78&zHFPQ z@ME?QqO7H3pg6tSKyjMrsGalnilbNS5zdp7=}BQf9q|*pwg{nBO>*e9z;>U7b?fx}C$=Ij=P+*$t>SKK&MOt+W_+WrL;A zL(HO;1ZGBjI&Q@0Ah}*Ys(Z$Od2c9APVEf&kWdMVu-I`q6X8TWuiLz<$rdx-WgNuV zY>Z^L$eXX>&d%P&Y3!TD-4H%Els+&Sk6VTDx>1CRqB|2H>Syt<&{HiIC^Dz=)|Hbh zV|Qe(`LrV5iA-sS??-gcQd9^8qJ0qMYujWE{SKvQx7m(9(CuONUzs!e-ZyQw$PQfj z*zRO*S+F%lbGE5Gmg6`>`rbF_LxjP1!^UJ4iK8GgCykuS9TGU(TLA5tNXKBaW1SIa zX>39u+PBKSI^8Mw_fdzD9=qj~Hj`5JmF_F%W7;Du^ygvAK(F%+>9=6UU~EG?uAWXc z*hiiGjC;zpS^LcRyv2Znp;FVl@$H9Jf96)$5^eRB^!vND~bL z>0m5)RT;Ai4q7a*h^LEHT5~?Z&ti+Bysc{VMcJd*Vc`3YWVFl~h48}cr-c`^2L6$J zi2ad_+ksQ}SAVK0)yV{tY(Mf23u?{DA8@-PIar=SHZG9`?+9b2naEn&Y@w{=PX+co z3tXlXZu-TjBc6!QIAan)&l+so0tEuQe(X(Zc2}u;AH8Fm4Y*gk*t2=Fkr(1MpE6_5 zi_MWvt7F#Z`r~#wiiv@P&p15SOXrmb$AF%xDyDp;Rh=|p8A)jAOnt`)yG=IL!R+H5 zv8#}8SdYr4eNHCsoL27!Bc|C0-kyG=Em|C`eTdT@>m7i!K>I0nyd-2GF`(6fV?$Pd z&nOb;wO7Dqzr`;SxTE?OA3GWP8pa4;zmZpUx-Dtlv_lyc=)EhZ0Do+ONfh~gAtAre zR@C$CxB+=(nmm|jTfJE_2uZ=erW=Xg!^YFI*tLh#>>U`=vY_57lLzmA)boTQUNB2Q zuPl<^pR|}n0lRhooyU@E96JItE@PUtvc@k1d*i+Pvm#fh z7@)?zDD8u=MI!grjk_U?95=eLzv4UGjJ8eoA746b^Nkz} zSUNp?%r0K*L^1O4tieGTQf6W#6xCgY6|&fPdOkL=`=hI?Bz|}cHnhQajTh+4YzR^N z*4!v`N}c$w^X~0*w-JffvtwMZ4XUNNPQYC9?u;4d0Ple+qZikkN8j*3BIciuJ#^mn ztz3Gonu1=p^}$kVdW$!*JBZjw{u6=!K?L=g<*6fc8BEW*ad)pJEgwne&2FyU9y#&w zChq>D^B$}IX(i?3nY8ak%8zpwG@j=7(CXgg2Fr%I?!v+G-@!1N+@(BI&})JOYQZbWfgf)kTHZ66 zd-{roJcNOA1bBs5xkic-N9}qfq>&y=LJ(^!Uir?2H0bcOdwOgLAp>kbs#>JGECQD2 zUF)hSVOFQwt=9KY18^;$uG$?fqZVOB?( zybfeWkuae4mc4f+sVoP6d6v$I(x?BV76q7L@rm$8Y*n9mKsd_@zS?rVkF2x_!u~{| zI!$pF8KJR4Xmmm66D5!3EZ=%Gzp_BJWF-$tfTVc8Mx{ri>!6mh#bO;PaFFCMzKJl1 z8Rh(FWgWBga~9?eHV)=*+}DE%hY=$eyDxq?qu6Q& z-!&Pwo#xQGg+&_7cU&LRKR++0&>FK4AQ?MaE9Kus>}BibT;^Z`-Nx`X zizw;A4pm~WvJ^5}Z#$n7oX8B}E>!0mBs*FuO{n|s9mWC)IAnR*=>tcD2f`WQawr*1 z<4?^CGo^NrXXJ-h(?tc`$H_XA)AR;(rCna&scuO+1}WfYK_EBSHwIxyDf7f(9;S)%udy$XUpUi*{^Wl4Rj0tsUTC zRJbbn2YrRJZ(+3$u~wOyes4QHv*Z7HH^704Vx5lVkX`{=wrkX`TT#bIIeL^yKi*kT z!3k{i3}RWy_Q+J;XOGDR+C_7_u>zMzzzg%tm+G~)gu^U~Sh)_pz;6UN(j5Vzihy4P zWycCBvt7LoT#(|RrDU=aD<015{V0wbqr^Oh&uR#B4i&-YI`3)4cYE*Gy0~k@8)q70 z(?awEIT*;2fEQH=#6szl-gJ|p#_XPl1*#;4vHCjH~x!j4Scgj^Zlgt}RSe9gKn}iUuZ$Hz}tXXIT52lLtN`@tdHirf$hA0E zb`eyD7n=A^y87n#((L)~i?C+)d#kRarRPt zgSnZ@UutOTwmu0+P%X$DYRz+Hcq?_@eBkps4F(LXkU>LSxH=MV+s5%>;Goco83YA! zb>_u86R{u3vqudll2nEhwjCcm>t054?<3(6cdGn!)~@GrdapU&qnXN@ukFQU<_t6K zRTyJo9hpnf&09>vX|+Gqg&`rfWmay%(!ueR-B0}>Y%}u(9ul=d;r`Bmp-{ z(^iQF%$vZ*v|Tp=Zm!!fk~3hf|>x07pUKrrOx> z$!aMjmzgH9IP|od;4b@iX}j)1#pkI$^sTn!MLlz8;O8%+lLt59+MHO!s|n`UybavwJE*z6<;>knwjO^$ zf9V57MCrenOKdm48Dl8$VI;U|=RV}CHS#OT_}w*b+?p8k7P~b4TKxwqoCoyVN^sR+ z-31@){!!Dt!ZTTX|5A|(2U=)LKsu$>m22}ONV!_CC*gDbbC2F)6o2u7HxtZDd7F12 zahp*FxoyMSP|nNsKSmCnIfcvrganp;jT}yu=c>ez4Ic(B+E>Zj?s2gYI zsK=YGLq)}65T&gk729w;pYBc~nx#nv-s+QqT&YLt_Gsbm_{Cpt!QXhV&LXK04|3~H zU+Di(8p$79rxv?5G#x}S#n;}ny^5s&mcFa;$m5lK94-XCmw$YHq^(=f5iYqcq!5MG ztfhmD$~FtTvA9eREiXCs{dTs69mW9}Z4J`iTJ8vYM)IyTXtE4_#eoI;CZ`DAX&k|P zwzm)UIji~hSJ?LJ`zmKB8b&mc@+6R8i^>{C!cjG44|mRE&Nu6ktMARqQWY>=G&$>L7uTf3vV($ZZnR!c|7OzQ(`uw)1QjzMQw! zz5J6oOdJ!zA&U}BoJrBytEOrL2_=Di>DzZkw%Ox%GGMnmdY40-81L9lwb(>8Ro(2SZ$$PB+QPOmWoIRb$0#-2MPaXIkC6vTz;{fR z3WfqeQB|4XUQ+zpRI8ApM0;OQ*carXj3FMglhs63?*2!gb<{9nyeIE$j-1{NNxbD% zAI7bOphh(w`n0eIv>lxIrOEF?h!Fe8tqB>W*6;f{p@&QrqUW!Wv(mtVx2AJ;b6hM5 z*mzy{?!6Wc2Y$qG+wKMRbg4$$0kTQ{**)xnleR4RAFxwO5mYB(Yv<)n+|~N{MJ}2# z)K zUfoe!ZVj-A{fTqHm4 z+3Y^osD%CDJ+qajDC?v=(>Iay_P4Yp{+NhKod9DgOg(bT5%LPs!>JfQn@$XzAqj_F-U=JNJcYJz8|V&^+k|6Y zsP7PW1W*~!Xf{2WLp8)S3?0Aj*)OfqSIdSmKoa0@PQ_TP-k>hI%%B3#lH1beKPl>V zt1LdN;nDQ_UMVPlB^!1X6VseDi`5dx-0lTRJIS`;!${2RYq(F^iF+bG+Bb0rEpNvd zIdtIgxN--rr5*NuJQppu$A5C|d9{pk7^>~|3v2S1<0lXMWkzCAf(A6Oi?l_My ze~YxKx8V-5*?v6R#VxTh&q^|$VDxBHdS-vV#ry6KkF!`_(kyzjp2Sb*MdPuZ-Rb;E z+Xa~Wg6L8Df!$K8{Z|C_OoO}v)b?j*7$anw!Qa&}&s_SEc586}?mLQWYHE6zsXK}p z%+Rn#mbd+R>H=-FXVzm6cV5Dw$IIN~6F{$$|Ij%K}87zBhv3cmE0j_BFrNI|;%?{cpPv)H1g9G~x{58iIQ6#cgeL z1sDR5eFW@jAT`9Hd`HZ8M*)h7WhYa!E}++ar2z6ah0cc|!XccjUO7Z45a>RFaE;+Z z)XxY;8q{^a(60sb0u!?Rb-ioc3Rf7cSY^t|q5Fj}r&9u7$OcpJ;XU2Y<;$-(M*M-B z6U-h+qfXV#9OFho3dnk2L}qR93P-Le$o4y1fw_CqLkc^37P^(q6c_YX1ms8={ctgr>H zaQDYgDFJ>2MAeKF`cItjz?PaI6)XZ*4qhqJ(}?;SxOXBL_aTi6mZ|TUzr^Bw{@`KH zGU`y7urHGN_{44OK##=QXF^?yD5CsLQB2qe2g2(j0w|eE6kB#V$_d3`)097!C zTwu>g=l~58$@13IX}R_x3ch*fdauc34@{0d6u}-9l6O5CBl3ib>b5r@Cq5-m$_U~G z2<0a#NakQq;X^ne9|EG!`nMe_9LZOQP`=R=oXnbwIzZ%|ZG8JdITaK{wfU(6(}qx1 z2%u}oL)K|=j1~v5tHU(P$g_*^*k5sv0p^TPc-z#5;O*Gt5Ax0`ph<;Lolf-6_^}Us zFlYKZJ<|YHj*|^<8D`_Q`L0wFdqU9@u1$I4eNMHpI4g{6ecjK_p|=cLZ>tTn!VqIQ zE-=x6zHjL)={an)4G0lB<(i{tU&36EsF1I!@#1ayc7Cl+f+V<~cm=09)ZETS#&^;0 zt;^{v4`48pz7QTU-zmj>nK$!ScXC%WvqXG0}KNTA%A`Y%mF+L0I&oA1wQu$Loh(| z9}eIb#RUHVnEzs1pzuimjsO5(JYQfOi2VTouP6VX1$=<>2!P8r{~k{QWn}+D>){~pe-~2{C@-Cr3_a8Lwkn5c;Ex|8vvXG*ac7k zfD^!9ee)Vf|EI@->KA`d{w)CD*Y05ZU>ptLqHVBm|5-=_3W2(Q04{0& z3)B_=XUzUD$_9V6|LS-9pbQ|o09?pF`#leRr zfahiZfpN`cJH0^pKMI~e0Z{M$-)MJ$XH1YD>>C~cpf3iVX#h+Dhy=?(faCD*h6sQG z!0R4>KWzXU4-~MCLjd4f@qY?RKqgQ}=x?+j9~O`ngfIYb4)y$c2K55x+kZ^{YrlB0 z13dqf3;6Le&E@_>A6N0u{2jDUvjJ4B1o*!&7k=0hh+6;v?SVe$ zPaBxIlmYZ1|7br4@&f9C{^d_vFin0*FACK8cY^}J;F2E@Ly-_D6bXf)BB3bY9s(=_ zeg0+tU&d6w%OJq-RsN5GfC8YN#ouT*foH5sdWwHU{J+TeL_xZ8#sT!@k>SGQU0|~1NC_SMhnt&0l4r1 zTK^ZqgKG!qqrr9X1e6_yhKan?0gs9UiAT+e#HZmxqGEx454HoYonRkc>HwyHt*1~J z3LwAF?{WL31CMdU1;1akzzZ+P_}^$1^yu^{EThNKYG{KE%< zd&kSZzl?K$ylDWx)`LHFxby);EZ~_7lppj3mvmqZ`hbi5G!Xy8b@1;xDF7K~bTX#D z?ey1JU-}$gljy(f1%`$J=pctA=T`;B1ss16^8hYo0Q=tz05gDo0Ga?Gz}k(1WVmMe z7k_qLuZ#R&`-T%B4bp?OVEorQ==yg-|H*S2*uOl+;TQAma;>_InN{pA?C4S!0>-Ns za~XVwK%V|?`_gt2foCuu9GfWs2EU&nNRSTf53p~*_I}kH1L8|sFn)4b|DUac0O@}V z;GSU`515;m{sL^{Pmpk4yX1Qj!%)G!JSe9lfXn^|P_L!vMgw4je1Uh1}$P z$`|wO7d^Ol_>=y>t%E?7e>s5rO(dWl2t@FY z_AhPVuR95#4D?mi06qh_3INpWPk{RlQ2t9=a1Ma&T-0F$;(s-OXAi)?VIz5OKLT_} z`!B!oI}Ofn2ox3IZ4ZFycN^ed^w04J)4==yc)?K7f7d|);!psG0IC3}1E2>0(t`8n z@AP2#h5Q2lqVS;oEMw8|B2jTkkOT|@NWqJJ+kdwqa2&v~2ac%#W&pGSSOM_o_=9O* z01zZL2_0Z3mwJNoNC21v@b~(Ef7ZRA2YCFe@_R!7A3>&Ik0cVmeKA)-fAQyBxU?m~ zJIR1t^ZeBOvdWt%K_wxNd{bzW~}(HUQ90z<$4!{m--(kpHjsKNJlM z*lR$SvFQYnl$zcbz+n`*SSNXIMPAqu*ze%}pH0gR@SQ41biB(wG|&doDjK`AlPhGfwh_ zj)HenfpG%?&Y%BUORidFAVFOK9^l>%++zc2P#4F5_1>uupMi~l7*h2}3E2n2MHBr9zZS-Jl|Wt=qrkzMLZ$_I7bz`!)YB^BEPaK>0wKFYkImJ;3v3 zDS)cqNC09w0RNld0FEV*m@y#duQ~8*F8tEtGX4Lxcdjv3RaYE8ckawwW*FwlJd}aC z0R}|qFu;Vj-a&@9LMxA=R$x#BQDMrAR$608eV{?5jkTsRbkdrZT4S&=lr)V6;|EJ< zN{wkuV+~l^plvM}OTY?q`uneQ_RN{fysp~d2RHe%&)JW)_g;ISz1LoAT_|Y?&Qdql zK~-aY$#%Wpih|=k&f?VqOz9ta!;^kv~I$+Q!?c%wl)h! z&#*aCy2h%#r>cIT$!8<3x`}^kYNda9!%~yCOKYcbmQ3OfHlBM>t)E|BLkLUW8^IN**<(6?4@SRIH6hkJ{9BS{%DXDhfqVAE;gLd#V zkY2DaJtNtlDm<$1awvO^TR+8iZ7&cb!pnm$LI=+eu>W z)$&`=x%eoMElYRP79d?8XP=#+n}}uaTmhta`yzb7fzJ&G{0?Mob_M^u*WsnYxF(E? zh&xD6SbV=1593%EGYW1=_iHf3wze&9wD*+PtawMt=pOQF(qnQw|F~DPL+u^M#u~(2 zf$g2^N+ebzyq{N0f6j%md+!L}UF_>!{S9MF=iS9tk9Su;8V{l!mD6p>#iFBke@7S} zLa^2QAqTevv>Ik&_lw0&RymjfLlr2BCtNoxe1eq7Y~6hZo! zb!m{Epr=qp;t{|I?0z8K)ok!dLp2qlRS9ZBBt&~xF9d0OEcyF@?&cF!R(KLf9?3Ll zoM!|5ZZIpJw^8&o#`F-{1=Y4ItN2M^dZ?pR5Pvca2Lt`IquCBU<8AsJBuOXl7;t5u z1sWg zzaLR&ketAIuQ9v!P5wGa`m4bh4v^Q`ht7)qJcurOhTRdm%Xl{5hV-h;@zzMWR+ZDe zL36b)@L8Cmw10w30^gC^bmLNA%ugqs#?!%?cjfk}oS=25aqI@Vb7lHXvIfL+NB1>H zH~mhr8rtNY3d<_C_LZJVc#gvA=R4R$-b3&I0ngCV+EX7s2KG(enMPFOByVq6PGybX z?P=xud)-r$TGMH7*S$QDC)*=*Q#_l#Bb^p&;Fd_aZ>e0S^SxU3<>bP3UrGXqV1QDp94+pA>yg<2C%ukjd%-l^e5Rid43^C zKN6NxS^HieAbp@gK0YU8tE-qW*K9?ypOOaL+7jKArB&87p=*D{ueRrk`m=I%Gr=}31{kFSU5$wnp{YuT6?*ejQWHq+J!UClu|i2Ir!*M~qiALZ4D zR)#ii?WKJsuP+)gp_{H%ww8o+Rp@r4U&||%E@T>T<9deYS>S09HygY3pqdNlfhez1 zN_$=n^$`xpX6weGShi!eFP;YWW<8$;>cg8r?Whm>=F5yn)zepmy80-ucB-ttm%hSe zv;UdCy-V*lY^v`5=&Kncm4Ut~$Ah9UZ4~iAuok#-ckt}Wy~?xdn$EYkK0Ka_zC^mR zsNF?+)NP49PCKLY@44!C9(x+-K&iyK<%l24qB^yw9nZzWNgaxS`~Be^mDMa$WAtV z)(!sP%8_=@SAFrO=IWdMkxd)@Q42QvqZi%f*Dh)C$6VLyk6V79+1)6|ew&~@@&6!D z+v=0ee{XW&e(7tRE=SMV==KZ7T;-RiHu!nN>(B)bGyP?hFX_)kH51j&m%;tu1kk%{ zyRSXC8vVrw71wO;?1OYP>Q8y&Msyn3R_1In-ACQcySmLyv=7xYR;sO?zy<9&NzmK4 zT{*=!=JJh8ALRI;SoXmD5%rcPy>DPE2z+s5qmkW4Hgoyp$leB>!%?CcK0hm-yY^+D zkk6juOWCY7=#CS%=CvP44PqrnB}g44vKV%ckbW?%IsT(oNFnuFtCB2%U#yA4*h=@?`@n zopys?I&~Fxua$mD!wSD-%JrsO)|#b`?mnM&kX$35Gx;IOm+V>aW6%!v0O`}DgI@){ z2pMv~KXk6f-*GPC?>HUz54FSqx(VT4bQ8k#q7F}(4T=-!H4NuC+>V3OK+hm1@a~|z z*GNM}BqUG?Nu?pw1ZoZbek3od#|T8&bUj8DW2y&EkFkzA8HSmZ*Iwu#`8YkWh7#!L zU5M2UJJ~-ZX#)*t7B&`$yE#IXxb9`^^(N=&zEd_D?B~vPIBt=h^%@ z((hkAHTgzx_ZR(5q7n7SLN_nDUU?sb!O++DoM2D+hRW)_zwkZJdyul~<4;sa_YW7~ zRTg*me}wt+buiv*S-lB#M)wqS|B%nm1aNOK2kPV7s;~KzyzTG~>y-CjU^btT{WMU3Z_r0h?g;z{pQ5g4h!@`LU?+YZC%Hcy;~Ba852R~+*1qf= zy#>~!*M=N^Caf<=`KhoI7F1U-diQA0iTUw8?bqXHT8HuLqoY>NDNbj~X8Um#20VL=ZKv?Wt(L)q*pd_*@` z-y{>7P0D^xB^piOVNi}A=VFteyqNp|{{xgS|EM1Ki0te^Lkd5s zmtTAqHn(6;`xWsIfbJX0|0ysVxuYA#v9A@5pYIPDcMbQ8^~NXN*>%UayL_w}KHbW_ zd$rx;bl+4vfj{$aiPd+}e-d0wA08i(|Bhs|Jba{!8Arb4_TtO^Y~)1!jw0|Y>hdWH z`EOrLsn?WJ`uk4h zf>s+t_K*s*{i-dEO?R-uE1QgGP|!ZX{?H>YdTl`E^OFr129KOZ-?>wT_ZTO)>VAi8 zd7v)>{pZCVm)x(uTEFqjQr~rV$Q_#0xiPhW8hw_~eM#r^d>}buIuK2r9izeLB6Jib zypF=S*O40od?6vu#S@+(D~|0e?wtn5k)(Qt@>7O$Y$NA|IA%URw@rv2g{zsVd{-w8lW3v$B2sMNjjdLpQh&6=w7thR@SEv!}jK-8K+CYop{yZ={U&gn7sd zo#l|{PLTF4;B4>;Um{QETn`}!S>Z#<*nWsCVu5|}FQiG9NW9~D(&r6*xrfS1;`=y2 zzF@X{rD40@%e(eU$rmx?lag1I5eZlFyoGz_Q$gFODVx1F%;&z&xnc59NpJ05UAwr& z{3Nq-Bp-?gQ`x^PFdbu%5tMelC z|J1(fZvtIBz@KYx*~~TQDVq5`&}Q^%WAKT^pL0~3Us}6>v*;Sm(sKAFSz0ptLh-P3 z{p6O8_@(?><%6hwIvKeqZKZCj>Q(L{ruz<(?d8t5qs`2%JN+eAjIOk@bUXIKE|(8?`VF_ z+(4uhUc{dEgur}UPN9EbyC4Biit_!`-NZwPb`Gu zyvR-y^Mc(;yU<@}X*a58r`DVP?UO7onGE0fKH^?gRq&>JXf_t~;7z+-K!vK_b>19r zskev&V4k4@ZCtBvXG_@#wcFX2Hb8lccbm7BGEG*d zo*$Oidow_Tx84r1`Cb`y=Thb_oRFF+zn$JR^A_;;2AWkx--U5#I*Qa2HKVB95GgJGox@l?^ Date: Wed, 17 Apr 2019 15:08:48 -0500 Subject: [PATCH 23/44] Cleaned up installer code considerably - Cleaned up the installer code. - Removed state pattern, all screens now have their own buttons. - Moved installation related functions out of screens, into methods owned by the installer -> screens call these methods. - Renamed states ->screens. - Changed agreement checkboxes -> only 1 now, 'I agreee' unchecked by default. - Changed installation screen graphics -> progress bar smaller, to the left of the finish button, now displays percentage in the button while it's grayed out. --- src/nStaller/Installer.cpp | 290 ++++++++++-------- src/nStaller/Installer.h | 44 +-- src/nStaller/{nStaller.rc => Installer.rc} | Bin 3412 -> 3416 bytes src/nStaller/Screens/Agreement.cpp | 140 +++++++++ src/nStaller/Screens/Agreement.h | 39 +++ .../Directory.cpp} | 178 ++++++----- src/nStaller/Screens/Directory.h | 36 +++ src/nStaller/Screens/Fail.cpp | 112 +++++++ src/nStaller/Screens/Fail.h | 31 ++ .../FinishState.cpp => Screens/Finish.cpp} | 103 +++---- .../FinishState.h => Screens/Finish.h} | 21 +- src/nStaller/Screens/Install.cpp | 120 ++++++++ src/nStaller/Screens/Install.h | 32 ++ .../{States/State.h => Screens/Screen.h} | 22 +- src/nStaller/Screens/Welcome.cpp | 112 +++++++ src/nStaller/Screens/Welcome.h | 32 ++ src/nStaller/States/AgreementState.cpp | 130 -------- src/nStaller/States/AgreementState.h | 28 -- src/nStaller/States/DirectoryState.h | 27 -- src/nStaller/States/FailState.cpp | 131 -------- src/nStaller/States/FailState.h | 28 -- src/nStaller/States/InstallState.cpp | 166 ---------- src/nStaller/States/InstallState.h | 35 --- src/nStaller/States/WelcomeState.cpp | 94 ------ src/nStaller/States/WelcomeState.h | 23 -- src/nStaller/nStaller.cpp | 26 -- src/nSuite/nSuite.rc | Bin 1530 -> 1530 bytes src/unStaller/Uninstaller.cpp | 2 +- 28 files changed, 1000 insertions(+), 1002 deletions(-) rename src/nStaller/{nStaller.rc => Installer.rc} (96%) create mode 100644 src/nStaller/Screens/Agreement.cpp create mode 100644 src/nStaller/Screens/Agreement.h rename src/nStaller/{States/DirectoryState.cpp => Screens/Directory.cpp} (59%) create mode 100644 src/nStaller/Screens/Directory.h create mode 100644 src/nStaller/Screens/Fail.cpp create mode 100644 src/nStaller/Screens/Fail.h rename src/nStaller/{States/FinishState.cpp => Screens/Finish.cpp} (75%) rename src/nStaller/{States/FinishState.h => Screens/Finish.h} (52%) create mode 100644 src/nStaller/Screens/Install.cpp create mode 100644 src/nStaller/Screens/Install.h rename src/nStaller/{States/State.h => Screens/Screen.h} (65%) create mode 100644 src/nStaller/Screens/Welcome.cpp create mode 100644 src/nStaller/Screens/Welcome.h delete mode 100644 src/nStaller/States/AgreementState.cpp delete mode 100644 src/nStaller/States/AgreementState.h delete mode 100644 src/nStaller/States/DirectoryState.h delete mode 100644 src/nStaller/States/FailState.cpp delete mode 100644 src/nStaller/States/FailState.h delete mode 100644 src/nStaller/States/InstallState.cpp delete mode 100644 src/nStaller/States/InstallState.h delete mode 100644 src/nStaller/States/WelcomeState.cpp delete mode 100644 src/nStaller/States/WelcomeState.h delete mode 100644 src/nStaller/nStaller.cpp diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 5a1e5ec..1d97765 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -1,22 +1,52 @@ #include "Installer.h" #include "Common.h" +#include "DirectoryTools.h" #include "TaskLogger.h" +#include +#include +#include #include #include +#pragma warning(push) +#pragma warning(disable:4458) +#include +#pragma warning(pop) // States used in this GUI application -#include "States/WelcomeState.h" -#include "States/AgreementState.h" -#include "States/DirectoryState.h" -#include "States/InstallState.h" -#include "States/FinishState.h" -#include "States/FailState.h" +#include "Screens/Welcome.h" +#include "Screens/Agreement.h" +#include "Screens/Directory.h" +#include "Screens/Install.h" +#include "Screens/Finish.h" +#include "Screens/Fail.h" +static void Paint(const HWND &hWnd, Welcome * ptr); + static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) +{ + CoInitialize(NULL); + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + Installer installer(hInstance); + + // Main message loop: + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Close + CoUninitialize(); + return (int)msg.wParam; +} + Installer::Installer() - : m_archive(IDR_ARCHIVE, "ARCHIVE"), m_manifest(IDR_MANIFEST, "MANIFEST") + : m_archive(IDR_ARCHIVE, "ARCHIVE"), m_manifest(IDR_MANIFEST, "MANIFEST"), m_threader(1ull) { // Process manifest if (m_manifest.exists()) { @@ -70,15 +100,15 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; - wcex.lpszClassName = "nStaller"; + wcex.lpszClassName = "Installer"; wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); if (!RegisterClassEx(&wcex)) { TaskLogger::PushText("Critical failure: could not create main window.\r\n"); success = false; } else { - m_window = CreateWindowW( - L"nStaller",(m_mfStrings[L"name"] + L" Installer").c_str(), + m_hwnd = CreateWindowW( + L"Installer",(m_mfStrings[L"name"] + L" Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 500, @@ -86,27 +116,24 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() ); // Create - SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); - auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); - auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); + auto dwStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_EXSTYLE); RECT rc = { 0, 0, 800, 500 }; - ShowWindow(m_window, true); - UpdateWindow(m_window); + ShowWindow(m_hwnd, true); + UpdateWindow(m_hwnd); AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); - SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + SetWindowPos(m_hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); // The portions of the screen that change based on input - m_states[WELCOME_STATE] = new WelcomeState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[AGREEMENT_STATE] = new AgreementState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[DIRECTORY_STATE] = new DirectoryState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[INSTALL_STATE] = new InstallState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[FINISH_STATE] = new FinishState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[FAIL_STATE] = new FailState(this, hInstance, m_window, { 170,0,800,450 }); + m_states[WELCOME_STATE] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_states[AGREEMENT_STATE] = new Agreement(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_states[DIRECTORY_STATE] = new Directory(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_states[INSTALL_STATE] = new Install(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_states[FINISH_STATE] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_states[FAIL_STATE] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); setState(WELCOME_STATE); } @@ -119,16 +146,9 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() void Installer::invalidate() { setState(FAIL_STATE); - showButtons(false, false, true); - enableButtons(false, false, true); m_valid = false; } -void Installer::finish() -{ - m_finished = true; -} - void Installer::setState(const StateEnums & stateIndex) { if (m_valid) { @@ -137,15 +157,10 @@ void Installer::setState(const StateEnums & stateIndex) m_states[stateIndex]->setVisible(true); m_currentIndex = stateIndex; RECT rc = { 0, 0, 160, 500 }; - RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); } } -Installer::StateEnums Installer::getCurrentIndex() const -{ - return m_currentIndex; -} - std::string Installer::getDirectory() const { return m_directory; @@ -154,7 +169,6 @@ std::string Installer::getDirectory() const void Installer::setDirectory(const std::string & directory) { m_directory = directory; - try { const auto spaceInfo = std::filesystem::space(std::filesystem::path(getDirectory()).root_path()); m_capacity = spaceInfo.capacity; @@ -166,16 +180,6 @@ void Installer::setDirectory(const std::string & directory) } } -char * Installer::getPackagePointer() const -{ - return m_packagePtr; -} - -size_t Installer::getCompressedPackageSize() const -{ - return m_packageSize; -} - size_t Installer::getDirectorySizeCapacity() const { return m_capacity; @@ -196,97 +200,129 @@ std::string Installer::getPackageName() const return m_packageName; } -void Installer::updateButtons(const WORD btnHandle) +void Installer::beginInstallation() { - if (btnHandle == LOWORD(m_prevBtn)) - m_states[m_currentIndex]->pressPrevious(); - else if (btnHandle == LOWORD(m_nextBtn)) - m_states[m_currentIndex]->pressNext(); - else if (btnHandle == LOWORD(m_exitBtn)) - m_states[m_currentIndex]->pressClose(); - RECT rc = { 0, 0, 160, 500 }; - RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + m_threader.addJob([&]() { + // Acquire the uninstaller resource + Resource uninstaller(IDR_UNINSTALLER, "UNINSTALLER"), manifest(IDR_MANIFEST, "MANIFEST"); + if (!uninstaller.exists()) { + TaskLogger::PushText("Cannot access installer resource, aborting...\r\n"); + setState(Installer::FAIL_STATE); + } + else { + // Unpackage using the rest of the resource file + size_t byteCount(0ull), fileCount(0ull); + auto directory = getDirectory(); + sanitize_path(directory); + if (!DRT::DecompressDirectory(directory, m_packagePtr, m_packageSize, byteCount, fileCount)) + invalidate(); + else { + // Write uninstaller to disk + const auto uninstallerPath = directory + "\\uninstaller.exe"; + std::filesystem::create_directories(std::filesystem::path(uninstallerPath).parent_path()); + std::ofstream file(uninstallerPath, std::ios::binary | std::ios::out); + if (!file.is_open()) { + TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); + invalidate(); + } + TaskLogger::PushText("Writing Uninstaller:\"" + uninstallerPath + "\"\r\n"); + file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); + file.close(); + + // Update uninstaller's resources + std::string newDir = std::regex_replace(directory, std::regex("\\\\"), "\\\\"); + const std::string newManifest( + std::string(reinterpret_cast(manifest.getPtr()), manifest.getSize()) + + "\r\ndirectory \"" + newDir + "\"" + ); + auto handle = BeginUpdateResource(uninstallerPath.c_str(), false); + if (!(bool)UpdateResource(handle, "MANIFEST", MAKEINTRESOURCE(IDR_MANIFEST), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)newManifest.c_str(), (DWORD)newManifest.size())) { + TaskLogger::PushText("Cannot write manifest contents to the uninstaller, aborting...\r\n"); + invalidate(); + } + EndUpdateResource(handle, FALSE); + } + } + }); } -void Installer::showButtons(const bool & prev, const bool & next, const bool & close) +void Installer::dumpErrorLog() { - if (m_valid) { - ShowWindow(m_prevBtn, prev); - ShowWindow(m_nextBtn, next); - ShowWindow(m_exitBtn, close); - } + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Installer error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); } -void Installer::enableButtons(const bool & prev, const bool & next, const bool & close) +void Installer::paint() { - if (m_valid) { - EnableWindow(m_prevBtn, prev); - EnableWindow(m_nextBtn, next); - EnableWindow(m_exitBtn, close); + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + const LinearGradientBrush backgroundGradient1( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); + + // Draw Steps + const SolidBrush lineBrush(Color(255, 100, 100, 100)); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"EULA", L"Directory", L"Install", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; + const auto frameIndex = (int)m_currentIndex; + for (int x = 0; x < 5; ++x) { + // Draw Circle + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + if (x == 4 && frameIndex == 5) + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20); + + // Draw Text + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); + + if (x == 3) + vertical_offset = 460; + else + vertical_offset += 50; } + + EndPaint(m_hwnd, &ps); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (Installer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - // Draw Background - const LinearGradientBrush backgroundGradient1( - Point(0, 0), - Point(0, 500), - Color(255, 25, 25, 25), - Color(255, 75, 75, 75) - ); - graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); - - // Draw Steps - const SolidBrush lineBrush(Color(255,100,100,100)); - graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); - constexpr static wchar_t* step_labels[] = { L"Welcome", L"EULA", L"Directory", L"Install", L"Finish" }; - FontFamily fontFamily(L"Segoe UI"); - Font font(&fontFamily, 15, FontStyleBold, UnitPixel); - REAL vertical_offset = 15; - const auto frameIndex = (int)ptr->getCurrentIndex(); - for (int x = 0; x < 5; ++x) { - // Draw Circle - auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); - if (x == 4 && frameIndex == 5) - color = Color(255, 225, 25, 75); - const SolidBrush brush(color); - Pen pen(color); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20 ); - graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20 ); - - // Draw Text - graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); - - if (x == 3) - vertical_offset = 460; - else - vertical_offset += 50; - } - - // Draw -watermark- - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); - SolidBrush greyBrush(Color(255, 127, 127, 127)); - SolidBrush blueishBrush(Color(255, 100, 125, 175)); - graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); - graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 180, 475 }, &blueishBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } + const auto ptr = (Installer*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); else if (message == WM_DESTROY) PostQuitMessage(0); - else if (message == WM_COMMAND) { - if (HIWORD(wParam) == BN_CLICKED) { - ptr->updateButtons(LOWORD(lParam)); - return S_OK; - } - } return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 35239e8..8b8f446 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -3,12 +3,13 @@ #define INSTALLER_H #include "Resource.h" +#include "Threader.h" #include #include #include -class FrameState; +class Screen; /** Encapsulates the logical features of the installer. */ class Installer { @@ -28,26 +29,15 @@ class Installer { // Public Methods /** When called, invalidates the installer, halting it from progressing. */ void invalidate(); - /** Flags the installation as complete. */ - void finish(); /** Make the state identified by the supplied enum as active, deactivating the previous state. @param stateIndex the new state to use. */ void setState(const StateEnums & stateIndex); - /** Retrieves the current frame's enumeration. - @return the current frame's index, as an enumeration. */ - StateEnums getCurrentIndex() const; /** Retrieves the current directory chosen for installation. @return active installation directory. */ std::string getDirectory() const; /** Sets a new installation directory. @param directory new installation directory. */ void setDirectory(const std::string & directory); - /** Retrieves the pointer to the compressed packaged contents. - @return the package pointer (offset of folder name data). */ - char * getPackagePointer() const; - /** Retrieves the size of the compressed package. - @return the package size (minus the folder name data). */ - size_t getCompressedPackageSize() const; /** Retrieves the size of the drive used in the current directory. @return the drive capacity. */ size_t getDirectorySizeCapacity() const; @@ -60,19 +50,12 @@ class Installer { /** Retrieves the package name. @return the package name. */ std::string getPackageName() const; - /** Check which button has been active, and perform it's state operation. - @param btnHandle handle to the currently active button. */ - void updateButtons(const WORD btnHandle); - /** Sets the visibility state of all 3 buttons. - @param prev make the 'previous' button visible. - @param next make the 'next' button visible. - @param close make the 'close' button visible. */ - void showButtons(const bool & prev, const bool & next, const bool & close); - /** Sets the enable state of all 3 buttons. - @param prev make the 'previous' button enabled. - @param next make the 'next' button enabled. - @param close make the 'close' button enabled. */ - void enableButtons(const bool & prev, const bool & next, const bool & close); + /** Install the installer's package contents to the directory previously chosen. */ + void beginInstallation(); + /** Dumps error log to disk. */ + static void dumpErrorLog(); + /** Render this window. */ + void paint(); // Public manifest strings @@ -90,18 +73,15 @@ class Installer { // Private Attributes + Threader m_threader; Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; - bool m_valid = true, m_finished = false; + bool m_valid = true; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; StateEnums m_currentIndex = WELCOME_STATE; - FrameState * m_states[STATE_COUNT]; - HWND - m_window = nullptr, - m_prevBtn = nullptr, - m_nextBtn = nullptr, - m_exitBtn = nullptr; + Screen * m_states[STATE_COUNT]; + HWND m_hwnd = nullptr; }; diff --git a/src/nStaller/nStaller.rc b/src/nStaller/Installer.rc similarity index 96% rename from src/nStaller/nStaller.rc rename to src/nStaller/Installer.rc index 8cd2c661b975d92499b71555a2b8ea4efe79a566..7e9faa6212ff9a5ff57ea2109ec9385eb0b5fd32 100644 GIT binary patch delta 45 tcmca2bwg^y0;b7Qyj+`CF^RFVdNSlO6i=3BmY#f%{Sbn;c^}6#763`#4YU9N delta 39 qcmca1bwz5!0;b7xSU4E{Hm_t7XPvx&S#I(J_CruwZSxC`0u}%)dk)e7 diff --git a/src/nStaller/Screens/Agreement.cpp b/src/nStaller/Screens/Agreement.cpp new file mode 100644 index 0000000..4d60769 --- /dev/null +++ b/src/nStaller/Screens/Agreement.cpp @@ -0,0 +1,140 @@ +#include "Agreement.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Agreement::~Agreement() +{ + UnregisterClass("AGREEMENT_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_checkYes); +} + +Agreement::Agreement(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "AGREEMENT_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("AGREEMENT_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create eula + m_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"edit", m_installer->m_mfStrings[L"eula"].c_str(), WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + if (m_installer->m_mfStrings[L"eula"].empty()) + SetWindowTextW(m_log, + L"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers.\r\n" + L"This software is provided as - is, use it at your own risk." + ); + + // Create checkboxes + m_checkYes = CreateWindow("Button", "I accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, size.y - 35, 310, 15, m_hwnd, (HMENU)1, hInstance, NULL); + CheckDlgButton(m_hwnd, 1, BST_UNCHECKED); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnPrev = CreateWindow("BUTTON", "< Back", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 290, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnNext = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Agreement::enact() +{ + EnableWindow(m_btnNext, m_agrees); +} + +void Agreement::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Please read the following license agreement:", -1, ®Font, PointF{ 10, 50 }, &blackBrush); + + EndPaint(m_hwnd, &ps); +} + +void Agreement::checkYes() +{ + m_agrees = IsDlgButtonChecked(m_hwnd, 1); + CheckDlgButton(m_hwnd, 1, m_agrees ? BST_CHECKED : BST_UNCHECKED); + EnableWindow(m_btnNext, m_agrees); +} + +void Agreement::goPrevious() +{ + m_installer->setState(Installer::StateEnums::WELCOME_STATE); +} + +void Agreement::goNext() +{ + m_installer->setState(Installer::StateEnums::DIRECTORY_STATE); +} + +void Agreement::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Agreement*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make checkbox text background color transparent + if (controlHandle == ptr->m_checkYes) { + SetBkMode(HDC(wParam), TRANSPARENT); + return (LRESULT)GetStockObject(NULL_BRUSH); + } + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_checkYes) + ptr->checkYes(); + else if (controlHandle == ptr->m_btnPrev) + ptr->goPrevious(); + else if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Screens/Agreement.h b/src/nStaller/Screens/Agreement.h new file mode 100644 index 0000000..1e6fa51 --- /dev/null +++ b/src/nStaller/Screens/Agreement.h @@ -0,0 +1,39 @@ +#pragma once +#ifndef AGREEMENTSTATE_H +#define AGREEMENTSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Accept the license agreement" - Screen" state. */ +class Agreement : public Screen { +public: + // Public (de)Constructors + ~Agreement(); + Agreement(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Check the 'I agree' button. */ + void checkYes(); + /** Check the 'I doon't agree' button. */ + void checkNo(); + /** Switch to the previous state. */ + void goPrevious(); + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_log = nullptr, m_checkYes = nullptr, m_btnPrev = nullptr, m_btnNext = nullptr, m_btnCancel = nullptr; + bool m_agrees = false; +}; + +#endif // AGREEMENTSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.cpp b/src/nStaller/Screens/Directory.cpp similarity index 59% rename from src/nStaller/States/DirectoryState.cpp rename to src/nStaller/Screens/Directory.cpp index 2d8e8c9..8e001da 100644 --- a/src/nStaller/States/DirectoryState.cpp +++ b/src/nStaller/Screens/Directory.cpp @@ -1,4 +1,4 @@ -#include "DirectoryState.h" +#include "Directory.h" #include "../Installer.h" #include #include @@ -8,6 +8,8 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); +static HRESULT CreateDialogEventHandler(REFIID, void **); +static HRESULT OpenFileDialog(std::string &); static std::string directory_and_package(const std::string & dir, const std::string & pack) { @@ -20,7 +22,7 @@ static std::string directory_and_package(const std::string & dir, const std::str return directory; } -DirectoryState::~DirectoryState() +Directory::~Directory() { UnregisterClass("DIRECTORY_STATE", m_hinstance); DestroyWindow(m_hwnd); @@ -28,8 +30,8 @@ DirectoryState::~DirectoryState() DestroyWindow(m_browseButton); } -DirectoryState::DirectoryState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) +Directory::Directory(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) { // Create window class m_hinstance = hInstance; @@ -46,30 +48,98 @@ DirectoryState::DirectoryState(Installer * installer, const HINSTANCE hInstance, m_wcex.lpszClassName = "DIRECTORY_STATE"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("DIRECTORY_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("DIRECTORY_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); // Create directory lookup fields - m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory_and_package(m_installer->getDirectory(), m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); + m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnPrev = CreateWindow("BUTTON", "< Back", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 290, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnInst = CreateWindow("BUTTON", "Install >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); } -void DirectoryState::enact() +void Directory::enact() { - m_installer->showButtons(true, true, true); - m_installer->enableButtons(true, true, true); + // Does nothing } -void DirectoryState::pressPrevious() +void Directory::paint() { - m_installer->setState(Installer::AGREEMENT_STATE); + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); + graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 500); + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); + graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); + + constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { + auto remainingSize = (double)size; + constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB", L"EB" }; + int i = 0; + while (remainingSize > 1024.00) { + remainingSize /= 1024.00; + ++i; + } + std::wstringstream stream; + stream << std::fixed << std::setprecision(2) << remainingSize; + return stream.str() + units[i] + L" (" + std::to_wstring(size) + L" bytes )"; + }; + graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); + graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); + graphics.DrawString((L" Available:\t\t\t" + readableFileSize(m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); + graphics.DrawString((L" Required:\t\t\t" + readableFileSize(m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); + + + EndPaint(m_hwnd, &ps); } -void DirectoryState::pressNext() +void Directory::browse() { - auto directory = m_installer->getDirectory(); - + std::string directory(""); + if (SUCCEEDED(OpenFileDialog(directory))) { + if (directory != "" && directory.length() > 2ull) { + directory = directory_and_package(directory, m_installer->getPackageName()); + m_installer->setDirectory(directory); + SetWindowTextA(m_directoryField, directory.c_str()); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + } + } +} + +void Directory::goPrevious() +{ + m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); +} + +void Directory::goInstall() +{ + const auto directory = m_installer->getDirectory(); if (directory == "" || directory == " " || directory.length() < 3) MessageBox( NULL, @@ -81,9 +151,8 @@ void DirectoryState::pressNext() m_installer->setState(Installer::INSTALL_STATE); } -void DirectoryState::pressClose() +void Directory::goCancel() { - // No new screen PostQuitMessage(0); } @@ -149,7 +218,7 @@ static HRESULT CreateDialogEventHandler(REFIID riid, void **ppv) return hr; } -HRESULT OpenFileDialog(std::string & directory) +static HRESULT OpenFileDialog(std::string & directory) { // CoCreate the File Open Dialog object. IFileDialog *pfd = NULL; @@ -198,73 +267,24 @@ HRESULT OpenFileDialog(std::string & directory) static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (DirectoryState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - SolidBrush solidWhiteBackground(Color(255, 255, 255, 255)); - graphics.FillRectangle(&solidWhiteBackground, 0, 0, 630, 450); - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Where would you like to install to?", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Choose a folder by pressing the 'Browse' button.", -1, ®Font, PointF{ 10, 100 }, &blackBrush); - graphics.DrawString(L"Alternatively, type a specific directory into the box below.", -1, ®Font, PointF{ 10, 115 }, &blackBrush); - - constexpr static auto readableFileSize = [](const size_t & size) -> std::wstring { - auto remainingSize = (double)size; - constexpr static wchar_t * units[] = { L" B", L" KB", L" MB", L" GB", L" TB", L" PB" }; - int i = 0; - while (remainingSize > 1024.00) { - remainingSize /= 1024.00; - ++i; - } - std::wstringstream stream; - stream << std::fixed << std::setprecision(2) << remainingSize; - return stream.str() + units[i] + L"\t(" + std::to_wstring(size) + L" bytes )"; - }; - graphics.DrawString(L"Disk Space", -1, ®Font, PointF{ 10, 200 }, &blackBrush); - graphics.DrawString((L" Capacity:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeCapacity())).c_str(), -1, ®Font, PointF{ 10, 225 }, &blackBrush); - graphics.DrawString((L" Available:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeAvailable())).c_str(), -1, ®Font, PointF{ 10, 240 }, &blackBrush); - graphics.DrawString((L" Required:\t\t\t" + readableFileSize(ptr->m_installer->getDirectorySizeRequired())).c_str(), -1, ®Font, PointF{ 10, 255 }, &blackBrush); - - - EndPaint(hWnd, &ps); - return S_OK; - } + const auto ptr = (Directory*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); - auto controlHandle = HWND(lParam); if (notification == BN_CLICKED) { - std::string directory(""); - if (SUCCEEDED(OpenFileDialog(directory))) { - if (directory != "" && directory.length() > 2ull) { - directory = directory_and_package(directory, ptr->m_installer->getPackageName()); - ptr->m_installer->setDirectory(directory); - SetWindowTextA(ptr->m_directoryField, directory.c_str()); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; - } - } + if (controlHandle == ptr->m_browseButton) + ptr->browse(); + else if (controlHandle == ptr->m_btnPrev) + ptr->goPrevious(); + else if (controlHandle == ptr->m_btnInst) + ptr->goInstall(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); } else if (notification == EN_CHANGE) { + // Redraw 'disk space data' region of window when the text field changes std::vector data(GetWindowTextLength(controlHandle) + 1ull); GetWindowTextA(controlHandle, &data[0], (int)data.size()); ptr->m_installer->setDirectory(std::string(data.data())); diff --git a/src/nStaller/Screens/Directory.h b/src/nStaller/Screens/Directory.h new file mode 100644 index 0000000..65ff4e0 --- /dev/null +++ b/src/nStaller/Screens/Directory.h @@ -0,0 +1,36 @@ +#pragma once +#ifndef DIRECTORYSTATE_H +#define DIRECTORYSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Choose a directory - Screen" state. */ +class Directory : public Screen { +public: + // Public (de)Constructors + ~Directory(); + Directory(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Browse for an installation directory. */ + void browse(); + /** Switch to the previous state. */ + void goPrevious(); + /** Switch to the next state. */ + void goInstall(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_directoryField = nullptr, m_browseButton = nullptr, m_btnPrev = nullptr, m_btnInst = nullptr, m_btnCancel = nullptr; +}; + +#endif // DIRECTORYSTATE_H \ No newline at end of file diff --git a/src/nStaller/Screens/Fail.cpp b/src/nStaller/Screens/Fail.cpp new file mode 100644 index 0000000..0498a24 --- /dev/null +++ b/src/nStaller/Screens/Fail.cpp @@ -0,0 +1,112 @@ +#include "Fail.h" +#include "Common.h" +#include "TaskLogger.h" +#include "../Installer.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Fail::~Fail() +{ + UnregisterClass("FAIL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +Fail::Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + + +void Fail::enact() +{ + Installer::dumpErrorLog(); +} + +void Fail::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Fail::goClose() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Fail*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Screens/Fail.h b/src/nStaller/Screens/Fail.h new file mode 100644 index 0000000..9c9dcac --- /dev/null +++ b/src/nStaller/Screens/Fail.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef FAILSTATE_H +#define FAILSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Failure - Screen" state. */ +class Fail: public Screen { +public: + // Public (de)Constructors + ~Fail(); + Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_btnClose = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FinishState.cpp b/src/nStaller/Screens/Finish.cpp similarity index 75% rename from src/nStaller/States/FinishState.cpp rename to src/nStaller/Screens/Finish.cpp index 052e2a8..7de892b 100644 --- a/src/nStaller/States/FinishState.cpp +++ b/src/nStaller/Screens/Finish.cpp @@ -1,4 +1,4 @@ -#include "FinishState.h" +#include "Finish.h" #include "Common.h" #include "../Installer.h" #include @@ -9,7 +9,7 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -FinishState::~FinishState() +Finish::~Finish() { UnregisterClass("FINISH_STATE", m_hinstance); DestroyWindow(m_hwnd); @@ -18,8 +18,8 @@ FinishState::~FinishState() DestroyWindow(checkboxHandle); } -FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) +Finish::Finish(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) { // Create window class m_hinstance = hInstance; @@ -36,12 +36,12 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const m_wcex.lpszClassName = "FINISH_STATE"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); // Create checkboxes - m_checkbox = CreateWindow("Button", "Show installation directory on close", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); + m_checkbox = CreateWindow("Button", "Show installation directory on close", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, 150, size.x, 15, m_hwnd, (HMENU)1, hInstance, NULL); CheckDlgButton(m_hwnd, 1, BST_CHECKED); // Shortcuts @@ -85,41 +85,60 @@ FinishState::FinishState(Installer * installer, const HINSTANCE hInstance, const int vertical = 170, checkIndex = 2; for each (const auto & shortcut in m_shortcuts_d) { const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); - m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" on the desktop").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, size.x, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); vertical += 20; checkIndex++; } for each (const auto & shortcut in m_shortcuts_s) { const auto name = std::wstring(&shortcut[1], shortcut.length() - 1); - m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start-menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, 610, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); + m_shortcutCheckboxes.push_back(CreateWindowW(L"Button", (L"Create a shortcut for " + name + L" in the start-menu").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, vertical, size.x, 15, m_hwnd, (HMENU)(LONGLONG)checkIndex, hInstance, NULL)); CheckDlgButton(m_hwnd, checkIndex, BST_CHECKED); vertical += 20; checkIndex++; } + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); } -void FinishState::enact() -{ - m_installer->showButtons(false, false, true); - m_installer->enableButtons(false, false, true); - m_installer->finish(); -} - -void FinishState::pressPrevious() +void Finish::enact() { - // Should never happen + // Does nothing } -void FinishState::pressNext() +void Finish::paint() { - // Should never happen + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); } -void FinishState::pressClose() +void Finish::goClose() { + m_showDirectory = IsDlgButtonChecked(m_hwnd, 1); // Open the installation directory + if user wants it to - if (IsDlgButtonChecked(m_hwnd, 1)) + if (m_showDirectory) ShellExecute(NULL, "open", m_installer->getDirectory().c_str(), NULL, NULL, SW_SHOWDEFAULT); // Create Shortcuts @@ -152,44 +171,20 @@ void FinishState::pressClose() } x++; } - - PostQuitMessage(0); + PostQuitMessage(0); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (FinishState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 255, 125), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } + const auto ptr = (Finish*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); else if (message == WM_CTLCOLORSTATIC) { // Make checkbox text background color transparent - const auto handle = HWND(lParam); - bool isCheckbox = handle == ptr->m_checkbox; + bool isCheckbox = controlHandle == ptr->m_checkbox; for each (auto chkHandle in ptr->m_shortcutCheckboxes) - if (handle == chkHandle) { + if (controlHandle == chkHandle) { isCheckbox = true; break; } @@ -204,8 +199,8 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); if (notification == BN_CLICKED) { - ptr->m_showDirectory = IsDlgButtonChecked(hWnd, 1); - return S_OK; + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); } } return DefWindowProc(hWnd, message, wParam, lParam); diff --git a/src/nStaller/States/FinishState.h b/src/nStaller/Screens/Finish.h similarity index 52% rename from src/nStaller/States/FinishState.h rename to src/nStaller/Screens/Finish.h index 3906e8f..b68afd5 100644 --- a/src/nStaller/States/FinishState.h +++ b/src/nStaller/Screens/Finish.h @@ -2,27 +2,30 @@ #ifndef FINISHSTATE_H #define FINISHSTATE_H -#include "State.h" +#include "Screen.h" #include /** This state encapuslates the "Finished - Screen" state. */ -class FinishState: public FrameState { +class Finish: public Screen { public: // Public (de)Constructors - ~FinishState(); - FinishState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); + ~Finish(); + Finish(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); // Public Attributes - HWND m_checkbox = nullptr; + HWND m_checkbox = nullptr, m_btnClose = nullptr; bool m_showDirectory = true; std::vector m_shortcutCheckboxes; std::vector m_shortcuts_d, m_shortcuts_s; diff --git a/src/nStaller/Screens/Install.cpp b/src/nStaller/Screens/Install.cpp new file mode 100644 index 0000000..175ded9 --- /dev/null +++ b/src/nStaller/Screens/Install.cpp @@ -0,0 +1,120 @@ +#include "Install.h" +#include "Common.h" +#include "Resource.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Install::~Install() +{ + UnregisterClass("INSTALL_STATE", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +Install::Install(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "INSTALL_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("INSTALL_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, size.y - 40, size.x - 115, 30, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + + std::string progress = std::to_string(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + "%"; + EnableWindow(m_btnFinish, position == range); + SetWindowTextA(m_btnFinish, position == range ? "Finish >" : progress.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnFinish = CreateWindow("BUTTON", "Finish", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + EnableWindow(m_btnFinish, false); +} + +void Install::enact() +{ + m_installer->beginInstallation(); +} + +void Install::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Install::goFinish() +{ + m_installer->setState(Installer::StateEnums::FINISH_STATE); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Install*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnFinish) + ptr->goFinish(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Screens/Install.h b/src/nStaller/Screens/Install.h new file mode 100644 index 0000000..4f71945 --- /dev/null +++ b/src/nStaller/Screens/Install.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef INSTALLSTATE_H +#define INSTALLSTATE_H + +#include "Screen.h" +#include + + +/** This state encapuslates the "Installing - Screen" state. */ +class Install : public Screen { +public: + // Public (de)Constructors + ~Install(); + Install(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goFinish(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_btnFinish = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; +}; + +#endif // INSTALLSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/State.h b/src/nStaller/Screens/Screen.h similarity index 65% rename from src/nStaller/States/State.h rename to src/nStaller/Screens/Screen.h index 7823921..3a68a1d 100644 --- a/src/nStaller/States/State.h +++ b/src/nStaller/Screens/Screen.h @@ -1,6 +1,6 @@ #pragma once -#ifndef STATE_H -#define STATE_H +#ifndef SCREEN_H +#define SCREEN_H #include #pragma warning(push) @@ -11,12 +11,13 @@ using namespace Gdiplus; class Installer; +struct vec2 { int x = 0, y = 0; }; -/**Encapsulation of a windows GDI 'window' object, for a particular state of the application. */ -class FrameState { +/**Encapsulation of a windows GDI 'window' object, for a particular screen of the application. */ +class Screen { public: // Public (de)Constructors - FrameState(Installer * installer) : m_installer(installer) {} + Screen(Installer * installer, const vec2 & pos, const vec2 & size) : m_installer(installer), m_pos(pos), m_size(size) {} // Public Methods @@ -31,12 +32,8 @@ class FrameState { // Public Interface Declarations /** Trigger this state to perform its screen action. */ virtual void enact() = 0; - /** Cause this state to process the "previous" action. */ - virtual void pressPrevious() = 0; - /** Cause this state to process the "next" action. */ - virtual void pressNext() = 0; - /** Cause this state to process the "close" action. */ - virtual void pressClose() = 0; + /** Render this window. */ + virtual void paint() = 0; // Public Attributes @@ -48,6 +45,7 @@ class FrameState { // Private Attributes WNDCLASSEX m_wcex; HINSTANCE m_hinstance; + vec2 m_pos, m_size; }; -#endif // STATE_H \ No newline at end of file +#endif // SCREEN_H \ No newline at end of file diff --git a/src/nStaller/Screens/Welcome.cpp b/src/nStaller/Screens/Welcome.cpp new file mode 100644 index 0000000..7870445 --- /dev/null +++ b/src/nStaller/Screens/Welcome.cpp @@ -0,0 +1,112 @@ +#include "Welcome.h" +#include "../Installer.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Welcome::~Welcome() +{ + UnregisterClass("WELCOME_STATE", m_hinstance); + DestroyWindow(m_hwnd); +} + +Welcome::Welcome(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(installer, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_STATE"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnNext = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Welcome::enact() +{ + // Does nothing +} + +void Welcome::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = m_installer->m_mfStrings[L"name"] + L" " + m_installer->m_mfStrings[L"version"]; + if (m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); + graphics.DrawString(m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, REAL(m_size.x - 20), REAL(m_size.y - 200)), &format, &blackBrush); + + // Draw -watermark- + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF(10, REAL(m_size.y - 45)), &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF(10, REAL(m_size.y - 25)), &blueishBrush); + + EndPaint(m_hwnd, &ps); +} + +void Welcome::goNext() +{ + m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); +} + +void Welcome::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Welcome*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + auto controlHandle = HWND(lParam); + if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/nStaller/Screens/Welcome.h b/src/nStaller/Screens/Welcome.h new file mode 100644 index 0000000..5bb0309 --- /dev/null +++ b/src/nStaller/Screens/Welcome.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef WELCOMESTATE_H +#define WELCOMESTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Welcome - Screen" state. */ +class Welcome : public Screen { +public: + // Public (de)Constructors + ~Welcome(); + Welcome(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_btnNext = nullptr, m_btnCancel = nullptr; +}; + +#endif // WELCOMESTATE_H \ No newline at end of file diff --git a/src/nStaller/States/AgreementState.cpp b/src/nStaller/States/AgreementState.cpp deleted file mode 100644 index 4183524..0000000 --- a/src/nStaller/States/AgreementState.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "AgreementState.h" -#include "../Installer.h" - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -AgreementState::~AgreementState() -{ - UnregisterClass("AGREEMENT_STATE", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_checkNo); - DestroyWindow(m_checkYes); -} - -AgreementState::AgreementState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "AGREEMENT_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("AGREEMENT_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - // Create eula - m_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"edit", m_installer->m_mfStrings[L"eula"].c_str(), WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 125, m_hwnd, NULL, hInstance, NULL); - if (m_installer->m_mfStrings[L"eula"].empty()) - SetWindowTextW(m_log, - L"nSuite installers can be created freely by anyone, as such those who generate them are responsible for its contents, not the developers.\r\n" - L"This software is provided as - is, use it at your own risk." - ); - - // Create checkboxes - m_checkYes = CreateWindow("Button", "I accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, (rc.bottom - rc.top) - 40, 610, 15, m_hwnd, (HMENU)1, hInstance, NULL); - m_checkNo = CreateWindow("Button", "I do not accept the terms of this license agreement", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_AUTOCHECKBOX, 10, (rc.bottom - rc.top) - 20, 610, 15, m_hwnd, (HMENU)2, hInstance, NULL); - CheckDlgButton(m_hwnd, 2, BST_CHECKED); -} - -void AgreementState::enact() -{ - m_installer->showButtons(true, true, true); - m_installer->enableButtons(true, m_agrees, true); -} - -void AgreementState::pressPrevious() -{ - m_installer->setState(Installer::WELCOME_STATE); -} - -void AgreementState::pressNext() -{ - m_installer->setState(Installer::DIRECTORY_STATE); -} - -void AgreementState::pressClose() -{ - // No new screen - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (AgreementState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"License Agreement", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(L"Please read the following license agreement:", -1, ®Font, PointF{ 10, 50 }, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_CTLCOLORSTATIC) { - // Make checkbox text background color transparent - if (HWND(lParam) == ptr->m_checkYes || HWND(lParam) == ptr->m_checkNo) { - SetBkMode(HDC(wParam), TRANSPARENT); - return (LRESULT)GetStockObject(NULL_BRUSH); - } - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); - } - else if (message == WM_COMMAND) { - const auto notification = HIWORD(wParam); - if (notification == BN_CLICKED) { - auto controlHandle = HWND(lParam); - if (controlHandle == ptr->m_checkYes && IsDlgButtonChecked(hWnd, 1)) { - CheckDlgButton(ptr->m_hwnd, 2, BST_UNCHECKED); - ptr->m_installer->enableButtons(true, true, true); - ptr->m_agrees = true; - } - else if (controlHandle == ptr->m_checkNo && IsDlgButtonChecked(hWnd, 2)) { - CheckDlgButton(ptr->m_hwnd, 1, BST_UNCHECKED); - ptr->m_installer->enableButtons(true, false, true); - ptr->m_agrees = false; - } - } - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/States/AgreementState.h b/src/nStaller/States/AgreementState.h deleted file mode 100644 index 29ea376..0000000 --- a/src/nStaller/States/AgreementState.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#ifndef AGREEMENTSTATE_H -#define AGREEMENTSTATE_H - -#include "State.h" - - -/** This state encapuslates the "Accept the license agreement" - Screen" state. */ -class AgreementState : public FrameState { -public: - // Public (de)Constructors - ~AgreementState(); - AgreementState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_log = nullptr, m_checkNo = nullptr, m_checkYes = nullptr; - bool m_agrees = false; -}; - -#endif // AGREEMENTSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/DirectoryState.h b/src/nStaller/States/DirectoryState.h deleted file mode 100644 index d65957a..0000000 --- a/src/nStaller/States/DirectoryState.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifndef DIRECTORYSTATE_H -#define DIRECTORYSTATE_H - -#include "State.h" - - -/** This state encapuslates the "Choose a directory - Screen" state. */ -class DirectoryState : public FrameState { -public: - // Public (de)Constructors - ~DirectoryState(); - DirectoryState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_directoryField = nullptr, m_browseButton = nullptr; -}; - -#endif // DIRECTORYSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/FailState.cpp b/src/nStaller/States/FailState.cpp deleted file mode 100644 index 0f0b6ef..0000000 --- a/src/nStaller/States/FailState.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "FailState.h" -#include "Common.h" -#include "TaskLogger.h" -#include "../Installer.h" -#include -#include -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -FailState::~FailState() -{ - UnregisterClass("FAIL_STATE", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); -} - -FailState::FailState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FAIL_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - // Create error log - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); -} - -void FailState::enact() -{ - m_installer->showButtons(false, false, true); - m_installer->enableButtons(false, false, true); - - // Dump error log to disk - const auto dir = get_current_directory() + "\\error_log.txt"; - const auto t = std::time(0); - char dateData[127]; - ctime_s(dateData, 127, &t); - std::string logData(""); - - // If the log doesn't exist, add header text - if (!std::filesystem::exists(dir)) - logData += "Installer error log:\r\n"; - - // Add remaining log data - logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; - - // Try to create the file - std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); - std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); - if (!file.is_open()) - TaskLogger::PushText("Cannot dump error log to disk...\r\n"); - else - file.write(logData.c_str(), (std::streamsize)logData.size()); - file.close(); -} - -void FailState::pressPrevious() -{ - // Should never happen -} - -void FailState::pressNext() -{ - // Should never happen -} - -void FailState::pressClose() -{ - // No new screen - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - //auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 225, 25, 75), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_CTLCOLORSTATIC) { - // Make log color white - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/States/FailState.h b/src/nStaller/States/FailState.h deleted file mode 100644 index 711e7a5..0000000 --- a/src/nStaller/States/FailState.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#ifndef FAILSTATE_H -#define FAILSTATE_H - -#include "State.h" - - -/** This state encapuslates the "Failure - Screen" state. */ -class FailState: public FrameState { -public: - // Public (de)Constructors - ~FailState(); - FailState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_hwndLog = nullptr; - size_t m_logIndex = 0ull; -}; - -#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/InstallState.cpp b/src/nStaller/States/InstallState.cpp deleted file mode 100644 index 4890dda..0000000 --- a/src/nStaller/States/InstallState.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "InstallState.h" -#include "Common.h" -#include "BufferTools.h" -#include "DirectoryTools.h" -#include "Resource.h" -#include "../Installer.h" -#include -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -InstallState::~InstallState() -{ - UnregisterClass("INSTALL_STATE", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - DestroyWindow(m_hwndPrgsBar); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); - TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); -} - -InstallState::InstallState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "INSTALL_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("INSTALL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - // Create log box and progress bar - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); - m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { - SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); - SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); - m_progress = std::to_wstring(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + L"%"; - RECT rc = { 580, 410, 800, 450 }; - RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); - }); -} - -void InstallState::enact() -{ - m_installer->showButtons(true, true, true); - m_installer->enableButtons(false, false, false); - - m_thread = std::thread([&]() { - // Acquire the uninstaller resource - Resource uninstaller(IDR_UNINSTALLER, "UNINSTALLER"), manifest(IDR_MANIFEST, "MANIFEST"); - if (!uninstaller.exists()) { - TaskLogger::PushText("Cannot access installer resource, aborting...\r\n"); - m_installer->setState(Installer::FAIL_STATE); - } - else { - // Unpackage using the rest of the resource file - size_t byteCount(0ull), fileCount(0ull); - auto directory = m_installer->getDirectory(); - sanitize_path(directory); - if (!DRT::DecompressDirectory(directory, m_installer->getPackagePointer(), m_installer->getCompressedPackageSize(), byteCount, fileCount)) - m_installer->invalidate(); - else { - // Write uninstaller to disk - const auto uninstallerPath = m_installer->getDirectory() + "\\uninstaller.exe"; - std::filesystem::create_directories(std::filesystem::path(uninstallerPath).parent_path()); - std::ofstream file(uninstallerPath, std::ios::binary | std::ios::out); - if (!file.is_open()) { - TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); - m_installer->invalidate(); - } - TaskLogger::PushText("Writing Uninstaller:\"" + uninstallerPath + "\"\r\n"); - file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); - file.close(); - - // Update uninstaller's resources - std::string newDir = std::regex_replace(directory, std::regex("\\\\"), "\\\\"); - const std::string newManifest( - std::string(reinterpret_cast(manifest.getPtr()), manifest.getSize()) - + "\r\ndirectory \"" + newDir + "\"" - ); - auto handle = BeginUpdateResource(uninstallerPath.c_str(), false); - if (!(bool)UpdateResource(handle, "MANIFEST", MAKEINTRESOURCE(IDR_MANIFEST), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)newManifest.c_str(), (DWORD)newManifest.size())) { - TaskLogger::PushText("Cannot write manifest contents to the uninstaller, aborting...\r\n"); - m_installer->invalidate(); - } - EndUpdateResource(handle, FALSE); - - m_installer->enableButtons(false, true, false); - } - } - }); - m_thread.detach(); -} - -void InstallState::pressPrevious() -{ - // Should never happen -} - -void InstallState::pressNext() -{ - m_installer->setState(Installer::FINISH_STATE); -} - -void InstallState::pressClose() -{ - // Should never happen -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (InstallState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Installing", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_CTLCOLORSTATIC) { - // Make log color white - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/States/InstallState.h b/src/nStaller/States/InstallState.h deleted file mode 100644 index 4c3f10d..0000000 --- a/src/nStaller/States/InstallState.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#ifndef INSTALLSTATE_H -#define INSTALLSTATE_H - -#include "State.h" -#include - - -/** This state encapuslates the "Installing - Screen" state. */ -class InstallState : public FrameState { -public: - // Public (de)Constructors - ~InstallState(); - InstallState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; - size_t m_logIndex = 0ull, m_taskIndex = 0ull; - std::wstring m_progress = L"0%"; - - -private: - // Private Attributes - std::thread m_thread; -}; - -#endif // INSTALLSTATE_H \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.cpp b/src/nStaller/States/WelcomeState.cpp deleted file mode 100644 index 4b6c45b..0000000 --- a/src/nStaller/States/WelcomeState.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "WelcomeState.h" -#include "../Installer.h" - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -WelcomeState::~WelcomeState() -{ - UnregisterClass("WELCOME_STATE", m_hinstance); - DestroyWindow(m_hwnd); -} - -WelcomeState::WelcomeState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(installer) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "WELCOME_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); -} - -void WelcomeState::enact() -{ - m_installer->showButtons(false, true, true); - m_installer->enableButtons(false, true, true); -} - -void WelcomeState::pressPrevious() -{ - // Should never happen -} - -void WelcomeState::pressNext() -{ - m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); -} - -void WelcomeState::pressClose() -{ - // No new screen - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (WelcomeState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - StringFormat format = StringFormat::GenericTypographic(); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Welcome to the Installation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - auto nameVer = ptr->m_installer->m_mfStrings[L"name"] + L" " + ptr->m_installer->m_mfStrings[L"version"]; - if (ptr->m_installer->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; - graphics.DrawString((L"The Wizard will install " + nameVer + L" on to your computer.").c_str(), -1, ®Font, PointF{ 10, 100 }, &format, &blackBrush); - graphics.DrawString(ptr->m_installer->m_mfStrings[L"description"].c_str(), -1, ®Font, RectF(10, 150, 620, 300), &format, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/nStaller/States/WelcomeState.h b/src/nStaller/States/WelcomeState.h deleted file mode 100644 index bc15117..0000000 --- a/src/nStaller/States/WelcomeState.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#ifndef WELCOMESTATE_H -#define WELCOMESTATE_H - -#include "State.h" - - -/** This state encapuslates the "Welcome - Screen" state. */ -class WelcomeState : public FrameState { -public: - // Public (de)Constructors - ~WelcomeState(); - WelcomeState(Installer * installer, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); -}; - -#endif // WELCOMESTATE_H \ No newline at end of file diff --git a/src/nStaller/nStaller.cpp b/src/nStaller/nStaller.cpp deleted file mode 100644 index 773bebd..0000000 --- a/src/nStaller/nStaller.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "Installer.h" -#pragma warning(push) -#pragma warning(disable:4458) -#include -#pragma warning(pop) - - -int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) -{ - CoInitialize(NULL); - Gdiplus::GdiplusStartupInput gdiplusStartupInput; - ULONG_PTR gdiplusToken; - Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); - Installer installer(hInstance); - - // Main message loop: - MSG msg; - while (GetMessage(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - // Close - CoUninitialize(); - return (int)msg.wParam; -} \ No newline at end of file diff --git a/src/nSuite/nSuite.rc b/src/nSuite/nSuite.rc index 0e81f79f785fb93bf7493d550e76ef0e6212c60b..d258c8c671df1c7a8c32065fbbee14b710085d8c 100644 GIT binary patch delta 16 Ycmeyx{fm3UKgP)iSh+THFnwYM07BXZ&Hw-a delta 22 dcmeyx{fm3UKSs_JhE#?$1_cJc&FoB{nE_ul2ND1P diff --git a/src/unStaller/Uninstaller.cpp b/src/unStaller/Uninstaller.cpp index 075fe92..99eaaf8 100644 --- a/src/unStaller/Uninstaller.cpp +++ b/src/unStaller/Uninstaller.cpp @@ -33,7 +33,7 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ DispatchMessage(&msg); } -#ifdef NDEBUG +#ifndef DEBUG // Delete scraps of the installation directory if (uninstaller.isValid()) { std::wstring cmd(L"cmd.exe /C ping 1.1.1.1 -n 1 -w 5000 > Nul & rmdir /q/s \"" + uninstaller.getDirectory()); From 07d000f01bbae3a1b5ccec3510275baa5c7731d5 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Wed, 17 Apr 2019 18:08:15 -0500 Subject: [PATCH 24/44] Registry updated with uninstaller info Registry updated with uninstaller info --- src/nStaller/Installer.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index 1d97765..ffbd54e 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -225,7 +225,7 @@ void Installer::beginInstallation() TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); invalidate(); } - TaskLogger::PushText("Writing Uninstaller:\"" + uninstallerPath + "\"\r\n"); + TaskLogger::PushText("Uninstaller:\"" + uninstallerPath + "\"\r\n"); file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); file.close(); @@ -241,6 +241,31 @@ void Installer::beginInstallation() invalidate(); } EndUpdateResource(handle, FALSE); + + // Add uninstaller to system registry + HKEY hkey; + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, (L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + m_mfStrings[L"name"]).c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hkey, NULL) == ERROR_SUCCESS) { + auto + name = from_wideString(m_mfStrings[L"name"]), + version = from_wideString(m_mfStrings[L"version"]), + publisher = from_wideString(m_mfStrings[L"publisher"]), + icon = from_wideString(m_mfStrings[L"icon"]); + DWORD ONE = 1, SIZE = (DWORD)(m_maxSize/1024ull); + if (icon.empty()) + icon = uninstallerPath; + else + icon = directory + icon; + RegSetKeyValueA(hkey, 0, "UninstallString", REG_SZ, (LPCVOID)uninstallerPath.c_str(), (DWORD)uninstallerPath.size()); + RegSetKeyValueA(hkey, 0, "DisplayIcon", REG_SZ, (LPCVOID)icon.c_str(), (DWORD)icon.size()); + RegSetKeyValueA(hkey, 0, "DisplayName", REG_SZ, (LPCVOID)name.c_str(), (DWORD)name.size()); + RegSetKeyValueA(hkey, 0, "DisplayVersion", REG_SZ, (LPCVOID)version.c_str(), (DWORD)version.size()); + RegSetKeyValueA(hkey, 0, "InstallLocation", REG_SZ, (LPCVOID)directory.c_str(), (DWORD)directory.size()); + RegSetKeyValueA(hkey, 0, "Publisher", REG_SZ, (LPCVOID)publisher.c_str(), (DWORD)publisher.size()); + RegSetKeyValueA(hkey, 0, "NoModify", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); + RegSetKeyValueA(hkey, 0, "NoRepair", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); + RegSetKeyValueA(hkey, 0, "EstimatedSize", REG_DWORD, (LPCVOID)&SIZE, (DWORD)sizeof(DWORD)); + } + RegCloseKey(hkey); } } }); From 03b480adbadcb08a0ff679ea640a9682a4cb481a Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Wed, 17 Apr 2019 19:48:06 -0500 Subject: [PATCH 25/44] Updated uninstaller in similar vein to the installer changes Uninstaller now uses screens instead of states, and has more of its code compartmentalized properly. --- src/nStaller/Installer.cpp | 37 +- src/nStaller/Installer.h | 18 +- src/nStaller/Screens/Agreement.cpp | 13 +- src/nStaller/Screens/Agreement.h | 6 +- src/nStaller/Screens/Directory.cpp | 13 +- src/nStaller/Screens/Directory.h | 6 +- src/nStaller/Screens/Fail.cpp | 8 +- src/nStaller/Screens/Fail.h | 6 +- src/nStaller/Screens/Finish.cpp | 10 +- src/nStaller/Screens/Finish.h | 6 +- src/nStaller/Screens/Install.cpp | 10 +- src/nStaller/Screens/Install.h | 6 +- src/nStaller/Screens/Welcome.cpp | 10 +- src/nStaller/Screens/Welcome.h | 6 +- src/unStaller/Screens/Fail.cpp | 112 ++++++ src/unStaller/Screens/Fail.h | 31 ++ src/unStaller/Screens/Finish.cpp | 112 ++++++ src/unStaller/Screens/Finish.h | 30 ++ .../{States/State.h => Screens/Screen.h} | 20 +- src/unStaller/Screens/Uninstall.cpp | 119 +++++++ src/unStaller/Screens/Uninstall.h | 33 ++ src/unStaller/Screens/Welcome.cpp | 114 +++++++ src/unStaller/Screens/Welcome.h | 32 ++ src/unStaller/States/FailState.cpp | 131 ------- src/unStaller/States/FailState.h | 28 -- src/unStaller/States/FinishState.cpp | 92 ----- src/unStaller/States/FinishState.h | 23 -- src/unStaller/States/UninstallState.cpp | 199 ----------- src/unStaller/States/UninstallState.h | 36 -- src/unStaller/States/WelcomeState.cpp | 94 ----- src/unStaller/States/WelcomeState.h | 23 -- src/unStaller/Uninstaller.cpp | 322 ++++++++++-------- src/unStaller/Uninstaller.h | 63 ++-- 33 files changed, 869 insertions(+), 900 deletions(-) create mode 100644 src/unStaller/Screens/Fail.cpp create mode 100644 src/unStaller/Screens/Fail.h create mode 100644 src/unStaller/Screens/Finish.cpp create mode 100644 src/unStaller/Screens/Finish.h rename src/unStaller/{States/State.h => Screens/Screen.h} (67%) create mode 100644 src/unStaller/Screens/Uninstall.cpp create mode 100644 src/unStaller/Screens/Uninstall.h create mode 100644 src/unStaller/Screens/Welcome.cpp create mode 100644 src/unStaller/Screens/Welcome.h delete mode 100644 src/unStaller/States/FailState.cpp delete mode 100644 src/unStaller/States/FailState.h delete mode 100644 src/unStaller/States/FinishState.cpp delete mode 100644 src/unStaller/States/FinishState.h delete mode 100644 src/unStaller/States/UninstallState.cpp delete mode 100644 src/unStaller/States/UninstallState.h delete mode 100644 src/unStaller/States/WelcomeState.cpp delete mode 100644 src/unStaller/States/WelcomeState.h diff --git a/src/nStaller/Installer.cpp b/src/nStaller/Installer.cpp index ffbd54e..c05570b 100644 --- a/src/nStaller/Installer.cpp +++ b/src/nStaller/Installer.cpp @@ -21,8 +21,6 @@ #include "Screens/Fail.h" -static void Paint(const HWND &hWnd, Welcome * ptr); - static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ int) @@ -107,6 +105,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() success = false; } else { + // Create window m_hwnd = CreateWindowW( L"Installer",(m_mfStrings[L"name"] + L" Installer").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, @@ -114,11 +113,7 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() 800, 500, NULL, NULL, hInstance, NULL ); - - // Create - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); auto dwStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_STYLE); auto dwExStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_EXSTYLE); RECT rc = { 0, 0, 800, 500 }; @@ -128,13 +123,13 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() SetWindowPos(m_hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); // The portions of the screen that change based on input - m_states[WELCOME_STATE] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - m_states[AGREEMENT_STATE] = new Agreement(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - m_states[DIRECTORY_STATE] = new Directory(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - m_states[INSTALL_STATE] = new Install(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - m_states[FINISH_STATE] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - m_states[FAIL_STATE] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); - setState(WELCOME_STATE); + m_screens[WELCOME_SCREEN] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[AGREEMENT_SCREEN] = new Agreement(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[DIRECTORY_SCREEN] = new Directory(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[INSTALL_SCREEN] = new Install(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FINISH_SCREEN] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FAIL_SCREEN] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + setScreen(WELCOME_SCREEN); } #ifndef DEBUG @@ -145,17 +140,17 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() void Installer::invalidate() { - setState(FAIL_STATE); + setScreen(FAIL_SCREEN); m_valid = false; } -void Installer::setState(const StateEnums & stateIndex) +void Installer::setScreen(const ScreenEnums & screenIndex) { if (m_valid) { - m_states[m_currentIndex]->setVisible(false); - m_states[stateIndex]->enact(); - m_states[stateIndex]->setVisible(true); - m_currentIndex = stateIndex; + m_screens[m_currentIndex]->setVisible(false); + m_screens[screenIndex]->enact(); + m_screens[screenIndex]->setVisible(true); + m_currentIndex = screenIndex; RECT rc = { 0, 0, 160, 500 }; RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); } @@ -207,7 +202,7 @@ void Installer::beginInstallation() Resource uninstaller(IDR_UNINSTALLER, "UNINSTALLER"), manifest(IDR_MANIFEST, "MANIFEST"); if (!uninstaller.exists()) { TaskLogger::PushText("Cannot access installer resource, aborting...\r\n"); - setState(Installer::FAIL_STATE); + setScreen(Installer::FAIL_SCREEN); } else { // Unpackage using the rest of the resource file diff --git a/src/nStaller/Installer.h b/src/nStaller/Installer.h index 8b8f446..a36f1fb 100644 --- a/src/nStaller/Installer.h +++ b/src/nStaller/Installer.h @@ -20,18 +20,18 @@ class Installer { // Public Enumerations - const enum StateEnums { - WELCOME_STATE, AGREEMENT_STATE, DIRECTORY_STATE, INSTALL_STATE, FINISH_STATE, FAIL_STATE, - STATE_COUNT + const enum ScreenEnums { + WELCOME_SCREEN, AGREEMENT_SCREEN, DIRECTORY_SCREEN, INSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, + SCREEN_COUNT }; // Public Methods /** When called, invalidates the installer, halting it from progressing. */ void invalidate(); - /** Make the state identified by the supplied enum as active, deactivating the previous state. - @param stateIndex the new state to use. */ - void setState(const StateEnums & stateIndex); + /** Make the screen identified by the supplied enum as active, deactivating the previous screen. + @param screenIndex the new screen to use. */ + void setScreen(const ScreenEnums & screenIndex); /** Retrieves the current directory chosen for installation. @return active installation directory. */ std::string getDirectory() const; @@ -72,15 +72,15 @@ class Installer { Installer(); - // Private Attributes + // Private Attributes Threader m_threader; Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; bool m_valid = true; char * m_packagePtr = nullptr; size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; - StateEnums m_currentIndex = WELCOME_STATE; - Screen * m_states[STATE_COUNT]; + ScreenEnums m_currentIndex = WELCOME_SCREEN; + Screen * m_screens[SCREEN_COUNT]; HWND m_hwnd = nullptr; }; diff --git a/src/nStaller/Screens/Agreement.cpp b/src/nStaller/Screens/Agreement.cpp index 4d60769..bc2db9d 100644 --- a/src/nStaller/Screens/Agreement.cpp +++ b/src/nStaller/Screens/Agreement.cpp @@ -6,9 +6,12 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Agreement::~Agreement() { - UnregisterClass("AGREEMENT_STATE", m_hinstance); + UnregisterClass("AGREEMENT_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_checkYes); + DestroyWindow(m_btnPrev); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); } Agreement::Agreement(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) @@ -26,10 +29,10 @@ Agreement::Agreement(Installer * installer, const HINSTANCE hInstance, const HWN m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "AGREEMENT_STATE"; + m_wcex.lpszClassName = "AGREEMENT_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("AGREEMENT_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("AGREEMENT_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); @@ -95,12 +98,12 @@ void Agreement::checkYes() void Agreement::goPrevious() { - m_installer->setState(Installer::StateEnums::WELCOME_STATE); + m_installer->setScreen(Installer::ScreenEnums::WELCOME_SCREEN); } void Agreement::goNext() { - m_installer->setState(Installer::StateEnums::DIRECTORY_STATE); + m_installer->setScreen(Installer::ScreenEnums::DIRECTORY_SCREEN); } void Agreement::goCancel() diff --git a/src/nStaller/Screens/Agreement.h b/src/nStaller/Screens/Agreement.h index 1e6fa51..34195d1 100644 --- a/src/nStaller/Screens/Agreement.h +++ b/src/nStaller/Screens/Agreement.h @@ -1,6 +1,6 @@ #pragma once -#ifndef AGREEMENTSTATE_H -#define AGREEMENTSTATE_H +#ifndef AGREEMENT_H +#define AGREEMENT_H #include "Screen.h" @@ -36,4 +36,4 @@ class Agreement : public Screen { bool m_agrees = false; }; -#endif // AGREEMENTSTATE_H \ No newline at end of file +#endif // AGREEMENT_H \ No newline at end of file diff --git a/src/nStaller/Screens/Directory.cpp b/src/nStaller/Screens/Directory.cpp index 8e001da..21f07c2 100644 --- a/src/nStaller/Screens/Directory.cpp +++ b/src/nStaller/Screens/Directory.cpp @@ -24,10 +24,13 @@ static std::string directory_and_package(const std::string & dir, const std::str Directory::~Directory() { - UnregisterClass("DIRECTORY_STATE", m_hinstance); + UnregisterClass("DIRECTORY_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_directoryField); DestroyWindow(m_browseButton); + DestroyWindow(m_btnPrev); + DestroyWindow(m_btnInst); + DestroyWindow(m_btnCancel); } Directory::Directory(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) @@ -45,10 +48,10 @@ Directory::Directory(Installer * installer, const HINSTANCE hInstance, const HWN m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "DIRECTORY_STATE"; + m_wcex.lpszClassName = "DIRECTORY_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("DIRECTORY_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("DIRECTORY_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); @@ -134,7 +137,7 @@ void Directory::browse() void Directory::goPrevious() { - m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); + m_installer->setScreen(Installer::ScreenEnums::AGREEMENT_SCREEN); } void Directory::goInstall() @@ -148,7 +151,7 @@ void Directory::goInstall() MB_OK | MB_ICONERROR | MB_TASKMODAL ); else - m_installer->setState(Installer::INSTALL_STATE); + m_installer->setScreen(Installer::INSTALL_SCREEN); } void Directory::goCancel() diff --git a/src/nStaller/Screens/Directory.h b/src/nStaller/Screens/Directory.h index 65ff4e0..56c1c1c 100644 --- a/src/nStaller/Screens/Directory.h +++ b/src/nStaller/Screens/Directory.h @@ -1,6 +1,6 @@ #pragma once -#ifndef DIRECTORYSTATE_H -#define DIRECTORYSTATE_H +#ifndef DIRECTORY_H +#define DIRECTORY_H #include "Screen.h" @@ -33,4 +33,4 @@ class Directory : public Screen { HWND m_directoryField = nullptr, m_browseButton = nullptr, m_btnPrev = nullptr, m_btnInst = nullptr, m_btnCancel = nullptr; }; -#endif // DIRECTORYSTATE_H \ No newline at end of file +#endif // DIRECTORY_H \ No newline at end of file diff --git a/src/nStaller/Screens/Fail.cpp b/src/nStaller/Screens/Fail.cpp index 0498a24..0863fbc 100644 --- a/src/nStaller/Screens/Fail.cpp +++ b/src/nStaller/Screens/Fail.cpp @@ -12,9 +12,10 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Fail::~Fail() { - UnregisterClass("FAIL_STATE", m_hinstance); + UnregisterClass("FAIL_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_hwndLog); + DestroyWindow(m_btnClose); TaskLogger::RemoveCallback_TextAdded(m_logIndex); } @@ -33,10 +34,10 @@ Fail::Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FAIL_STATE"; + m_wcex.lpszClassName = "FAIL_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("FAIL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); @@ -52,7 +53,6 @@ Fail::Fail(Installer * installer, const HINSTANCE hInstance, const HWND parent, m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); } - void Fail::enact() { Installer::dumpErrorLog(); diff --git a/src/nStaller/Screens/Fail.h b/src/nStaller/Screens/Fail.h index 9c9dcac..7b24453 100644 --- a/src/nStaller/Screens/Fail.h +++ b/src/nStaller/Screens/Fail.h @@ -1,6 +1,6 @@ #pragma once -#ifndef FAILSTATE_H -#define FAILSTATE_H +#ifndef FAIL_H +#define FAIL_H #include "Screen.h" @@ -28,4 +28,4 @@ class Fail: public Screen { size_t m_logIndex = 0ull; }; -#endif // FAILSTATE_H \ No newline at end of file +#endif // FAIL_H \ No newline at end of file diff --git a/src/nStaller/Screens/Finish.cpp b/src/nStaller/Screens/Finish.cpp index 7de892b..408a327 100644 --- a/src/nStaller/Screens/Finish.cpp +++ b/src/nStaller/Screens/Finish.cpp @@ -11,9 +11,10 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Finish::~Finish() { - UnregisterClass("FINISH_STATE", m_hinstance); + UnregisterClass("FINISH_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_checkbox); + DestroyWindow(m_btnClose); for each (auto checkboxHandle in m_shortcutCheckboxes) DestroyWindow(checkboxHandle); } @@ -33,10 +34,10 @@ Finish::Finish(Installer * installer, const HINSTANCE hInstance, const HWND pare m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FINISH_STATE"; + m_wcex.lpszClassName = "FINISH_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("FINISH_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); @@ -192,9 +193,6 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l SetBkMode(HDC(wParam), TRANSPARENT); return (LRESULT)GetStockObject(NULL_BRUSH); } - // Make log color white - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); } else if (message == WM_COMMAND) { const auto notification = HIWORD(wParam); diff --git a/src/nStaller/Screens/Finish.h b/src/nStaller/Screens/Finish.h index b68afd5..2c407ca 100644 --- a/src/nStaller/Screens/Finish.h +++ b/src/nStaller/Screens/Finish.h @@ -1,6 +1,6 @@ #pragma once -#ifndef FINISHSTATE_H -#define FINISHSTATE_H +#ifndef FINISH_H +#define FINISH_H #include "Screen.h" #include @@ -31,4 +31,4 @@ class Finish: public Screen { std::vector m_shortcuts_d, m_shortcuts_s; }; -#endif // FINISHSTATE_H \ No newline at end of file +#endif // FINISH_H \ No newline at end of file diff --git a/src/nStaller/Screens/Install.cpp b/src/nStaller/Screens/Install.cpp index 175ded9..571daed 100644 --- a/src/nStaller/Screens/Install.cpp +++ b/src/nStaller/Screens/Install.cpp @@ -8,10 +8,11 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Install::~Install() { - UnregisterClass("INSTALL_STATE", m_hinstance); + UnregisterClass("INSTALL_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_hwndLog); DestroyWindow(m_hwndPrgsBar); + DestroyWindow(m_btnFinish); TaskLogger::RemoveCallback_TextAdded(m_logIndex); TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); } @@ -31,17 +32,16 @@ Install::Install(Installer * installer, const HINSTANCE hInstance, const HWND pa m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "INSTALL_STATE"; + m_wcex.lpszClassName = "INSTALL_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("INSTALL_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("INSTALL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); // Create log box and progress bar m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, size.y - 40, size.x - 115, 30, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Installation Log:\r\n"); m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); }); @@ -95,7 +95,7 @@ void Install::paint() void Install::goFinish() { - m_installer->setState(Installer::StateEnums::FINISH_STATE); + m_installer->setScreen(Installer::ScreenEnums::FINISH_SCREEN); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) diff --git a/src/nStaller/Screens/Install.h b/src/nStaller/Screens/Install.h index 4f71945..c7ea624 100644 --- a/src/nStaller/Screens/Install.h +++ b/src/nStaller/Screens/Install.h @@ -1,6 +1,6 @@ #pragma once -#ifndef INSTALLSTATE_H -#define INSTALLSTATE_H +#ifndef INSTALL_H +#define INSTALL_H #include "Screen.h" #include @@ -29,4 +29,4 @@ class Install : public Screen { size_t m_logIndex = 0ull, m_taskIndex = 0ull; }; -#endif // INSTALLSTATE_H \ No newline at end of file +#endif // INSTALL_H \ No newline at end of file diff --git a/src/nStaller/Screens/Welcome.cpp b/src/nStaller/Screens/Welcome.cpp index 7870445..7a991d4 100644 --- a/src/nStaller/Screens/Welcome.cpp +++ b/src/nStaller/Screens/Welcome.cpp @@ -6,8 +6,10 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Welcome::~Welcome() { - UnregisterClass("WELCOME_STATE", m_hinstance); + UnregisterClass("WELCOME_SCREEN", m_hinstance); DestroyWindow(m_hwnd); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); } Welcome::Welcome(Installer * installer, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) @@ -25,10 +27,10 @@ Welcome::Welcome(Installer * installer, const HINSTANCE hInstance, const HWND pa m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "WELCOME_STATE"; + m_wcex.lpszClassName = "WELCOME_SCREEN"; m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + m_hwnd = CreateWindow("WELCOME_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); setVisible(false); @@ -85,7 +87,7 @@ void Welcome::paint() void Welcome::goNext() { - m_installer->setState(Installer::StateEnums::AGREEMENT_STATE); + m_installer->setScreen(Installer::ScreenEnums::AGREEMENT_SCREEN); } void Welcome::goCancel() diff --git a/src/nStaller/Screens/Welcome.h b/src/nStaller/Screens/Welcome.h index 5bb0309..afc28d3 100644 --- a/src/nStaller/Screens/Welcome.h +++ b/src/nStaller/Screens/Welcome.h @@ -1,6 +1,6 @@ #pragma once -#ifndef WELCOMESTATE_H -#define WELCOMESTATE_H +#ifndef WELCOME_H +#define WELCOME_H #include "Screen.h" @@ -29,4 +29,4 @@ class Welcome : public Screen { HWND m_btnNext = nullptr, m_btnCancel = nullptr; }; -#endif // WELCOMESTATE_H \ No newline at end of file +#endif // WELCOME_H \ No newline at end of file diff --git a/src/unStaller/Screens/Fail.cpp b/src/unStaller/Screens/Fail.cpp new file mode 100644 index 0000000..caa8315 --- /dev/null +++ b/src/unStaller/Screens/Fail.cpp @@ -0,0 +1,112 @@ +#include "Fail.h" +#include "Common.h" +#include "TaskLogger.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Fail::~Fail() +{ + UnregisterClass("FAIL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_btnClose); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); +} + +Fail::Fail(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FAIL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FAIL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create error log + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Fail::enact() +{ + Uninstaller::dumpErrorLog(); +} + +void Fail::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 225, 25, 75), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Fail::goClose() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Fail*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/Screens/Fail.h b/src/unStaller/Screens/Fail.h new file mode 100644 index 0000000..1901a37 --- /dev/null +++ b/src/unStaller/Screens/Fail.h @@ -0,0 +1,31 @@ +#pragma once +#ifndef FAILSTATE_H +#define FAILSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Failure - Screen" state. */ +class Fail: public Screen { +public: + // Public (de)Constructors + ~Fail(); + Fail(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_btnClose = nullptr; + size_t m_logIndex = 0ull; +}; + +#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/unStaller/Screens/Finish.cpp b/src/unStaller/Screens/Finish.cpp new file mode 100644 index 0000000..6e71ce7 --- /dev/null +++ b/src/unStaller/Screens/Finish.cpp @@ -0,0 +1,112 @@ +#include "Finish.h" +#include "Common.h" +#include "../Uninstaller.h" +#include +#include +#include +#include + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Finish::~Finish() +{ + UnregisterClass("FINISH_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_btnClose); +} + +Finish::Finish(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "FINISH_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("FINISH_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnClose = CreateWindow("BUTTON", "Close", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Finish::enact() +{ + // Does nothing +} + +void Finish::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 255, 125), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstallation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Finish::goClose() +{ +#ifndef DEBUG + // Delete scraps of the installation directory (nuke the remaining directory) + std::wstring cmd(L"cmd.exe /C ping 1.1.1.1 -n 1 -w 5000 > Nul & rmdir /q/s \"" + m_uninstaller->getDirectory()); + cmd.erase(std::find(cmd.begin(), cmd.end(), L'\0'), cmd.end()); + cmd += L"\""; + STARTUPINFOW si = { 0 }; + PROCESS_INFORMATION pi = { 0 }; + + CreateProcessW(NULL, (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + std::error_code er; + std::filesystem::remove_all(m_uninstaller->getDirectory(), er); +#endif + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Finish*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnClose) + ptr->goClose(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/Screens/Finish.h b/src/unStaller/Screens/Finish.h new file mode 100644 index 0000000..08c9b6c --- /dev/null +++ b/src/unStaller/Screens/Finish.h @@ -0,0 +1,30 @@ +#pragma once +#ifndef FINISHSTATE_H +#define FINISHSTATE_H + +#include "Screen.h" + + +/** This state encapuslates the "Finished - Screen" state. */ +class Finish: public Screen { +public: + // Public (de)Constructors + ~Finish(); + Finish(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goClose(); + + + // Public Attributes + HWND m_btnClose = nullptr; +}; + +#endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/State.h b/src/unStaller/Screens/Screen.h similarity index 67% rename from src/unStaller/States/State.h rename to src/unStaller/Screens/Screen.h index bf90427..abb93b2 100644 --- a/src/unStaller/States/State.h +++ b/src/unStaller/Screens/Screen.h @@ -1,6 +1,6 @@ #pragma once -#ifndef STATE_H -#define STATE_H +#ifndef SCREEN_H +#define SCREEN_H #include #pragma warning(push) @@ -11,12 +11,13 @@ using namespace Gdiplus; class Uninstaller; +struct vec2 { int x = 0, y = 0; }; /**Encapsulation of a windows GDI 'window' object, for a particular state of the application. */ -class FrameState { +class Screen { public: // Public (de)Constructors - FrameState(Uninstaller * uninstaller) : m_uninstaller(uninstaller) {} + Screen(Uninstaller * uninstaller, const vec2 & pos, const vec2 & size) : m_uninstaller(uninstaller), m_pos(pos), m_size(size) {} // Public Methods @@ -31,12 +32,8 @@ class FrameState { // Public Interface Declarations /** Trigger this state to perform its screen action. */ virtual void enact() = 0; - /** Cause this state to process the "previous" action. */ - virtual void pressPrevious() = 0; - /** Cause this state to process the "next" action. */ - virtual void pressNext() = 0; - /** Cause this state to process the "close" action. */ - virtual void pressClose() = 0; + /** Render this window. */ + virtual void paint() = 0; // Public Attributes @@ -48,6 +45,7 @@ class FrameState { // Private Attributes WNDCLASSEX m_wcex; HINSTANCE m_hinstance; + vec2 m_pos, m_size; }; -#endif // STATE_H \ No newline at end of file +#endif // SCREEN_H \ No newline at end of file diff --git a/src/unStaller/Screens/Uninstall.cpp b/src/unStaller/Screens/Uninstall.cpp new file mode 100644 index 0000000..4aafc33 --- /dev/null +++ b/src/unStaller/Screens/Uninstall.cpp @@ -0,0 +1,119 @@ +#include "Uninstall.h" +#include "Common.h" +#include "../Uninstaller.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Uninstall::~Uninstall() +{ + UnregisterClass("UNINSTALL_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_hwndLog); + DestroyWindow(m_hwndPrgsBar); + DestroyWindow(m_btnFinish); + TaskLogger::RemoveCallback_TextAdded(m_logIndex); + TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); +} + +Uninstall::Uninstall(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "UNINSTALL_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("UNINSTALL_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create log box and progress bar + m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 75, size.x - 20, size.y - 125, m_hwnd, NULL, hInstance, NULL); + m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, size.y - 40, size.x - 115, 30, m_hwnd, NULL, hInstance, NULL); + m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); + }); + m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { + SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); + SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); + RECT rc = { 580, 410, 800, 450 }; + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); + + std::string progress = std::to_string(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + "%"; + EnableWindow(m_btnFinish, position == range); + SetWindowTextA(m_btnFinish, position == range ? "Finish >" : progress.c_str()); + }); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnFinish = CreateWindow("BUTTON", "Finish", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + EnableWindow(m_btnFinish, false); +} + +void Uninstall::enact() +{ + m_uninstaller->beginUninstallation(); +} + +void Uninstall::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Uninstalling", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + + EndPaint(m_hwnd, &ps); +} + +void Uninstall::goFinish() +{ + m_uninstaller->setScreen(Uninstaller::ScreenEnums::FINISH_SCREEN); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Uninstall*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + const auto controlHandle = HWND(lParam); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_CTLCOLORSTATIC) { + // Make log color white + SetBkColor(HDC(wParam), RGB(255, 255, 255)); + return (LRESULT)GetStockObject(WHITE_BRUSH); + } + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + if (controlHandle == ptr->m_btnFinish) + ptr->goFinish(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/Screens/Uninstall.h b/src/unStaller/Screens/Uninstall.h new file mode 100644 index 0000000..28b4d42 --- /dev/null +++ b/src/unStaller/Screens/Uninstall.h @@ -0,0 +1,33 @@ +#pragma once +#ifndef UNINSTALL_H +#define UNINSTALL_H + +#include "Screen.h" +#include +#include + + +/** This state encapuslates the "Uninstalling - Screen" state. */ +class Uninstall : public Screen { +public: + // Public (de)Constructors + ~Uninstall(); + Uninstall(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goFinish(); + + + // Public Attributes + HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr, m_btnFinish = nullptr; + size_t m_logIndex = 0ull, m_taskIndex = 0ull; +}; + +#endif // UNINSTALL_H \ No newline at end of file diff --git a/src/unStaller/Screens/Welcome.cpp b/src/unStaller/Screens/Welcome.cpp new file mode 100644 index 0000000..9e2a7c9 --- /dev/null +++ b/src/unStaller/Screens/Welcome.cpp @@ -0,0 +1,114 @@ +#include "Welcome.h" +#include "../Uninstaller.h" + + +static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +Welcome::~Welcome() +{ + UnregisterClass("WELCOME_SCREEN", m_hinstance); + DestroyWindow(m_hwnd); + DestroyWindow(m_btnNext); + DestroyWindow(m_btnCancel); +} + +Welcome::Welcome(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size) + : Screen(uninstaller, pos, size) +{ + // Create window class + m_hinstance = hInstance; + m_wcex.cbSize = sizeof(WNDCLASSEX); + m_wcex.style = CS_HREDRAW | CS_VREDRAW; + m_wcex.lpfnWndProc = WndProc; + m_wcex.cbClsExtra = 0; + m_wcex.cbWndExtra = 0; + m_wcex.hInstance = hInstance; + m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); + m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); + m_wcex.lpszMenuName = NULL; + m_wcex.lpszClassName = "WELCOME_SCREEN"; + m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); + RegisterClassEx(&m_wcex); + m_hwnd = CreateWindow("WELCOME_SCREEN", "", WS_OVERLAPPED | WS_CHILD | WS_VISIBLE, pos.x, pos.y, size.x, size.y, parent, NULL, hInstance, NULL); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); + setVisible(false); + + // Create Buttons + constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; + m_btnNext = CreateWindow("BUTTON", "Uninstall >", BUTTON_STYLES | BS_DEFPUSHBUTTON, size.x - 200, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); + m_btnCancel = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, size.x - 95, size.y - 40, 85, 30, m_hwnd, NULL, hInstance, NULL); +} + +void Welcome::enact() +{ + // Does nothing +} + +void Welcome::paint() +{ + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + LinearGradientBrush backgroundGradient( + Point(0, 0), + Point(0, m_size.y), + Color(50, 25, 125, 225), + Color(255, 255, 255, 255) + ); + graphics.FillRectangle(&backgroundGradient, 0, 0, m_size.x, m_size.y); + + // Preparing Fonts + FontFamily fontFamily(L"Segoe UI"); + Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); + Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); + Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); + SolidBrush blueBrush(Color(255, 25, 125, 225)); + SolidBrush blackBrush(Color(255, 0, 0, 0)); + SolidBrush greyBrush(Color(255, 127, 127, 127)); + SolidBrush blueishBrush(Color(255, 100, 125, 175)); + StringFormat format = StringFormat::GenericTypographic(); + + // Draw Text + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawString(L"Welcome to the Uninstallation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); + auto nameVer = m_uninstaller->m_mfStrings[L"name"] + L" " + m_uninstaller->m_mfStrings[L"version"]; + if (m_uninstaller->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; + graphics.DrawString((L"The Wizard will remove " + nameVer + L" from your computer.").c_str(), -1, ®Font, PointF{ 10, 75 }, &format, &blackBrush); + graphics.DrawString(L"Note: the installation directory for this software will be deleted.\r\nIf there are any files that you wish to preserve, move them before continuing.", -1, ®Font, RectF(10, 400, 620, 300), &format, &blackBrush); + + // Draw -watermark- + graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF(10, REAL(m_size.y - 45)), &greyBrush); + graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF(10, REAL(m_size.y - 25)), &blueishBrush); + + EndPaint(m_hwnd, &ps); +} + +void Welcome::goNext() +{ + m_uninstaller->setScreen(Uninstaller::ScreenEnums::UNINSTALL_SCREEN); +} + +void Welcome::goCancel() +{ + PostQuitMessage(0); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + const auto ptr = (Welcome*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); + else if (message == WM_COMMAND) { + const auto notification = HIWORD(wParam); + if (notification == BN_CLICKED) { + auto controlHandle = HWND(lParam); + if (controlHandle == ptr->m_btnNext) + ptr->goNext(); + else if (controlHandle == ptr->m_btnCancel) + ptr->goCancel(); + } + } + return DefWindowProc(hWnd, message, wParam, lParam); +} \ No newline at end of file diff --git a/src/unStaller/Screens/Welcome.h b/src/unStaller/Screens/Welcome.h new file mode 100644 index 0000000..2a477ca --- /dev/null +++ b/src/unStaller/Screens/Welcome.h @@ -0,0 +1,32 @@ +#pragma once +#ifndef WELCOME_H +#define WELCOME_H + +#include "Screen.h" + + +/** This state encapuslates the "Welcome - Screen" state. */ +class Welcome : public Screen { +public: + // Public (de)Constructors + ~Welcome(); + Welcome(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const vec2 & pos, const vec2 & size); + + + // Public Interface Implementations + virtual void enact() override; + virtual void paint() override; + + + // Public Methods + /** Switch to the next state. */ + void goNext(); + /** Switch to the cancel state. */ + void goCancel(); + + + // Public Attributes + HWND m_btnNext = nullptr, m_btnCancel = nullptr; +}; + +#endif // WELCOME_H \ No newline at end of file diff --git a/src/unStaller/States/FailState.cpp b/src/unStaller/States/FailState.cpp deleted file mode 100644 index 9b2ca21..0000000 --- a/src/unStaller/States/FailState.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "FailState.h" -#include "Common.h" -#include "TaskLogger.h" -#include "../Uninstaller.h" -#include -#include -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -FailState::~FailState() -{ - UnregisterClass("FAIL_STATE", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); -} - -FailState::FailState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(uninstaller) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FAIL_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FAIL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - // Create error log - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)"Error Log:\r\n"); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); -} - -void FailState::enact() -{ - m_uninstaller->showButtons(false, false, true); - m_uninstaller->enableButtons(false, false, true); - - // Dump error log to disk - const auto dir = get_current_directory() + "\\error_log.txt"; - const auto t = std::time(0); - char dateData[127]; - ctime_s(dateData, 127, &t); - std::string logData(""); - - // If the log doesn't exist, add header text - if (!std::filesystem::exists(dir)) - logData += "Uninstaller error log:\r\n"; - - // Add remaining log data - logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; - - // Try to create the file - std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); - std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); - if (!file.is_open()) - TaskLogger::PushText("Cannot dump error log to disk...\r\n"); - else - file.write(logData.c_str(), (std::streamsize)logData.size()); - file.close(); -} - -void FailState::pressPrevious() -{ - // Should never happen -} - -void FailState::pressNext() -{ - // Should never happen -} - -void FailState::pressClose() -{ - // No new screen - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - //auto ptr = (FailState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 225, 25, 75), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Uninstallation Incomplete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_CTLCOLORSTATIC) { - // Make log color white - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/unStaller/States/FailState.h b/src/unStaller/States/FailState.h deleted file mode 100644 index 98bca6f..0000000 --- a/src/unStaller/States/FailState.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#ifndef FAILSTATE_H -#define FAILSTATE_H - -#include "State.h" - - -/** This state encapuslates the "Failure - Screen" state. */ -class FailState: public FrameState { -public: - // Public (de)Constructors - ~FailState(); - FailState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_hwndLog = nullptr; - size_t m_logIndex = 0ull; -}; - -#endif // FAILSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/FinishState.cpp b/src/unStaller/States/FinishState.cpp deleted file mode 100644 index 41c99f1..0000000 --- a/src/unStaller/States/FinishState.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "FinishState.h" -#include "Common.h" -#include "../Uninstaller.h" -#include -#include -#include -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -FinishState::~FinishState() -{ - UnregisterClass("FINISH_STATE", m_hinstance); - DestroyWindow(m_hwnd); -} - -FinishState::FinishState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(uninstaller) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "FINISH_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("FINISH_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); -} - -void FinishState::enact() -{ - m_uninstaller->showButtons(false, false, true); - m_uninstaller->enableButtons(false, false, true); - m_uninstaller->finish(); -} - -void FinishState::pressPrevious() -{ - // Should never happen -} - -void FinishState::pressNext() -{ - // Should never happen -} - -void FinishState::pressClose() -{ - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (FinishState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 255, 125), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Uninstallation Complete", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/unStaller/States/FinishState.h b/src/unStaller/States/FinishState.h deleted file mode 100644 index bdb8a1a..0000000 --- a/src/unStaller/States/FinishState.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#ifndef FINISHSTATE_H -#define FINISHSTATE_H - -#include "State.h" - - -/** This state encapuslates the "Finished - Screen" state. */ -class FinishState: public FrameState { -public: - // Public (de)Constructors - ~FinishState(); - FinishState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); -}; - -#endif // FINISHSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/UninstallState.cpp b/src/unStaller/States/UninstallState.cpp deleted file mode 100644 index d50878b..0000000 --- a/src/unStaller/States/UninstallState.cpp +++ /dev/null @@ -1,199 +0,0 @@ -#include "UninstallState.h" -#include "Common.h" -#include "BufferTools.h" -#include "DirectoryTools.h" -#include "../Uninstaller.h" -#include - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -UninstallState::~UninstallState() -{ - UnregisterClass("UNINSTALL_STATE", m_hinstance); - DestroyWindow(m_hwnd); - DestroyWindow(m_hwndLog); - DestroyWindow(m_hwndPrgsBar); - TaskLogger::RemoveCallback_TextAdded(m_logIndex); - TaskLogger::RemoveCallback_ProgressUpdated(m_taskIndex); -} - -UninstallState::UninstallState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(uninstaller) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "UNINSTALL_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("UNINSTALL_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); - - // Create log box and progress bar - m_hwndLog = CreateWindowEx(WS_EX_CLIENTEDGE, "edit", 0, WS_VISIBLE | WS_OVERLAPPED | WS_CHILD | WS_VSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL, 10, 50, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 100, m_hwnd, NULL, hInstance, NULL); - m_hwndPrgsBar = CreateWindowEx(WS_EX_CLIENTEDGE, PROGRESS_CLASS, 0, WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | WS_DLGFRAME | WS_CLIPCHILDREN | PBS_SMOOTH, 10, (rc.bottom - rc.top) - 40, (rc.right - rc.left) - 70, 25, m_hwnd, NULL, hInstance, NULL); - m_logIndex = TaskLogger::AddCallback_TextAdded([&](const std::string & message) { - SendMessage(m_hwndLog, EM_REPLACESEL, FALSE, (LPARAM)message.c_str()); - }); - m_taskIndex = TaskLogger::AddCallback_ProgressUpdated([&](const size_t & position, const size_t & range) { - SendMessage(m_hwndPrgsBar, PBM_SETRANGE32, 0, LPARAM(int_fast32_t(range))); - SendMessage(m_hwndPrgsBar, PBM_SETPOS, WPARAM(int_fast32_t(position)), 0); - m_progress = std::to_wstring(position == range ? 100 : int(std::floorf((float(position) / float(range)) * 100.0f))) + L"%"; - RECT rc = { 580, 410, 800, 450 }; - RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); - }); -} - -void UninstallState::enact() -{ - m_uninstaller->showButtons(false, true, true); - m_uninstaller->enableButtons(false, false, false); - - m_thread = std::thread([&]() { - const auto directory = from_wideString(m_uninstaller->getDirectory()); - - // Find all installed files - const auto entries = get_file_paths(directory); - - // Find all shortcuts - const auto desktopStrings = m_uninstaller->m_mfStrings[L"shortcut"], startmenuStrings = m_uninstaller->m_mfStrings[L"startmenu"]; - size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; - std::vector shortcuts_d, shortcuts_s; - shortcuts_d.reserve(numD + numS); - shortcuts_s.reserve(numD + numS); - size_t last = 0; - if (!desktopStrings.empty()) - for (size_t x = 0; x < numD; ++x) { - // Find end of shortcut - auto nextComma = desktopStrings.find(L',', last); - if (nextComma == std::wstring::npos) - nextComma = desktopStrings.size(); - - // Find demarkation point where left half is the shortcut path, right half is the shortcut name - shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); - - // Skip whitespace, find next element - last = nextComma + 1ull; - while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) - last++; - } - last = 0; - if (!startmenuStrings.empty()) - for (size_t x = 0; x < numS; ++x) { - // Find end of shortcut - auto nextComma = startmenuStrings.find(L',', last); - if (nextComma == std::wstring::npos) - nextComma = startmenuStrings.size(); - - // Find demarkation point where left half is the shortcut path, right half is the shortcut name - shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); - - // Skip whitespace, find next element - last = nextComma + 1ull; - while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) - last++; - } - - // Set progress bar range to include all files + shortcuts + 1 (cleanup step) - TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 1); - size_t progress = 0ull; - - // Remove all files in the installation folder, list them - std::error_code er; - if (!entries.size()) - TaskLogger::PushText("Already uninstalled / no files found.\r\n"); - else { - for each (const auto & entry in entries) { - TaskLogger::PushText("Deleting file: \"" + entry.path().string() + "\"\r\n"); - std::filesystem::remove(entry, er); - TaskLogger::SetProgress(++progress); - } - } - - // Remove all shortcuts - for each (const auto & shortcut in shortcuts_d) { - const auto path = get_users_desktop() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; - TaskLogger::PushText("Deleting desktop shortcut: \"" + path + "\"\r\n"); - std::filesystem::remove(path, er); - progress++; - } - for each (const auto & shortcut in shortcuts_s) { - const auto path = get_users_startmenu() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; - TaskLogger::PushText("Deleting start-menu shortcut: \"" + path + "\"\r\n"); - std::filesystem::remove(path, er); - progress++; - } - - // Clean up whatever's left (empty folders) - std::filesystem::remove_all(directory, er); - TaskLogger::SetProgress(++progress); - - m_uninstaller->enableButtons(false, true, false); - }); - m_thread.detach(); -} - -void UninstallState::pressPrevious() -{ - // Should never happen -} - -void UninstallState::pressNext() -{ - m_uninstaller->setState(Uninstaller::FINISH_STATE); -} - -void UninstallState::pressClose() -{ - // Should never happen -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (UninstallState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regBoldFont(&fontFamily, 14, FontStyleBold, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Uninstalling", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - graphics.DrawString(ptr->m_progress.c_str(), -1, ®BoldFont, PointF{ 580, 412 }, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - else if (message == WM_CTLCOLORSTATIC) { - // Make log color white - SetBkColor(HDC(wParam), RGB(255, 255, 255)); - return (LRESULT)GetStockObject(WHITE_BRUSH); - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/unStaller/States/UninstallState.h b/src/unStaller/States/UninstallState.h deleted file mode 100644 index 1c61a7a..0000000 --- a/src/unStaller/States/UninstallState.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#ifndef UNINSTALLSTATE_H -#define UNINSTALLSTATE_H - -#include "State.h" -#include -#include - - -/** This state encapuslates the "Uninstalling - Screen" state. */ -class UninstallState : public FrameState { -public: - // Public (de)Constructors - ~UninstallState(); - UninstallState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); - - - // Public Attributes - HWND m_hwndLog = nullptr, m_hwndPrgsBar = nullptr; - size_t m_logIndex = 0ull, m_taskIndex = 0ull; - std::wstring m_progress = L"0%"; - - -private: - // Private Attributes - std::thread m_thread; -}; - -#endif // UNINSTALLSTATE_H \ No newline at end of file diff --git a/src/unStaller/States/WelcomeState.cpp b/src/unStaller/States/WelcomeState.cpp deleted file mode 100644 index 0d0bac3..0000000 --- a/src/unStaller/States/WelcomeState.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "WelcomeState.h" -#include "../Uninstaller.h" - - -static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); - -WelcomeState::~WelcomeState() -{ - UnregisterClass("WELCOME_STATE", m_hinstance); - DestroyWindow(m_hwnd); -} - -WelcomeState::WelcomeState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc) - : FrameState(uninstaller) -{ - // Create window class - m_hinstance = hInstance; - m_wcex.cbSize = sizeof(WNDCLASSEX); - m_wcex.style = CS_HREDRAW | CS_VREDRAW; - m_wcex.lpfnWndProc = WndProc; - m_wcex.cbClsExtra = 0; - m_wcex.cbWndExtra = 0; - m_wcex.hInstance = hInstance; - m_wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - m_wcex.hCursor = LoadCursor(NULL, IDC_ARROW); - m_wcex.hbrBackground = (HBRUSH)(COLOR_WINDOWFRAME); - m_wcex.lpszMenuName = NULL; - m_wcex.lpszClassName = "WELCOME_STATE"; - m_wcex.hIconSm = LoadIcon(m_wcex.hInstance, IDI_APPLICATION); - RegisterClassEx(&m_wcex); - m_hwnd = CreateWindow("WELCOME_STATE", "", WS_OVERLAPPED | WS_VISIBLE | WS_CHILD, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, parent, NULL, hInstance, NULL); - SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this); - setVisible(false); -} - -void WelcomeState::enact() -{ - m_uninstaller->showButtons(false, true, true); - m_uninstaller->enableButtons(false, true, true); -} - -void WelcomeState::pressPrevious() -{ - // Should never happen -} - -void WelcomeState::pressNext() -{ - m_uninstaller->setState(Uninstaller::StateEnums::UNINSTALL_STATE); -} - -void WelcomeState::pressClose() -{ - // No new screen - PostQuitMessage(0); -} - -static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - auto ptr = (WelcomeState*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - - // Draw Background - LinearGradientBrush backgroundGradient( - Point(0, 0), - Point(0, 450), - Color(50, 25, 125, 225), - Color(255, 255, 255, 255) - ); - graphics.FillRectangle(&backgroundGradient, 0, 0, 630, 450); - - // Preparing Fonts - FontFamily fontFamily(L"Segoe UI"); - Font bigFont(&fontFamily, 25, FontStyleBold, UnitPixel); - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - SolidBrush blueBrush(Color(255, 25, 125, 225)); - SolidBrush blackBrush(Color(255, 0, 0, 0)); - StringFormat format = StringFormat::GenericTypographic(); - - // Draw Text - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawString(L"Welcome to the Uninstallation Wizard", -1, &bigFont, PointF{ 10, 10 }, &blueBrush); - auto nameVer = ptr->m_uninstaller->m_mfStrings[L"name"] + L" " + ptr->m_uninstaller->m_mfStrings[L"version"]; - if (ptr->m_uninstaller->m_mfStrings[L"name"].empty()) nameVer = L"it's contents"; - graphics.DrawString((L"The Wizard will remove " + nameVer + L" from your computer.").c_str(), -1, ®Font, PointF{ 10, 75 }, &format, &blackBrush); - graphics.DrawString(L"Note: the installation directory for this software will be deleted.\r\nIf there are any files that you wish to preserve, move them before continuing.", -1, ®Font, RectF(10, 400, 620, 300), &format, &blackBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} \ No newline at end of file diff --git a/src/unStaller/States/WelcomeState.h b/src/unStaller/States/WelcomeState.h deleted file mode 100644 index 1df156d..0000000 --- a/src/unStaller/States/WelcomeState.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#ifndef WELCOMESTATE_H -#define WELCOMESTATE_H - -#include "State.h" - - -/** This state encapuslates the "Welcome - Screen" state. */ -class WelcomeState : public FrameState { -public: - // Public (de)Constructors - ~WelcomeState(); - WelcomeState(Uninstaller * uninstaller, const HINSTANCE hInstance, const HWND parent, const RECT & rc); - - - // Public Interface Implementations - virtual void enact(); - virtual void pressPrevious(); - virtual void pressNext(); - virtual void pressClose(); -}; - -#endif // WELCOMESTATE_H \ No newline at end of file diff --git a/src/unStaller/Uninstaller.cpp b/src/unStaller/Uninstaller.cpp index 99eaaf8..80b3f14 100644 --- a/src/unStaller/Uninstaller.cpp +++ b/src/unStaller/Uninstaller.cpp @@ -1,19 +1,22 @@ #include "Uninstaller.h" #include "Common.h" +#include "DirectoryTools.h" #include "TaskLogger.h" +#include +#include +#include #include -#include #include #pragma warning(push) #pragma warning(disable:4458) #include #pragma warning(pop) -// States used in this GUI application -#include "States/WelcomeState.h" -#include "States/UninstallState.h" -#include "States/FinishState.h" -#include "States/FailState.h" +// Screens used in this GUI application +#include "Screens/Welcome.h" +#include "Screens/Uninstall.h" +#include "Screens/Finish.h" +#include "Screens/Fail.h" static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); @@ -33,32 +36,13 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE, _In_ LPSTR, _In_ DispatchMessage(&msg); } -#ifndef DEBUG - // Delete scraps of the installation directory - if (uninstaller.isValid()) { - std::wstring cmd(L"cmd.exe /C ping 1.1.1.1 -n 1 -w 5000 > Nul & rmdir /q/s \"" + uninstaller.getDirectory()); - cmd.erase(std::find(cmd.begin(), cmd.end(), L'\0'), cmd.end()); - cmd += L"\""; - STARTUPINFOW si = { 0 }; - PROCESS_INFORMATION pi = { 0 }; - - CreateProcessW(NULL, (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); - - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - - std::error_code er; - std::filesystem::remove_all(uninstaller.getDirectory(), er); - } -#endif - // Close CoUninitialize(); return (int)msg.wParam; } Uninstaller::Uninstaller() - : m_manifest(IDR_MANIFEST, "MANIFEST") + : m_manifest(IDR_MANIFEST, "MANIFEST"), m_threader(1ull) { // Process manifest if (m_manifest.exists()) { @@ -109,7 +93,7 @@ Uninstaller::Uninstaller(const HINSTANCE hInstance) : Uninstaller() success = false; } else { - m_window = CreateWindowW( + m_hwnd = CreateWindowW( L"Uninstaller",(m_mfStrings[L"name"] + L" Uninstaller").c_str(), WS_OVERLAPPED | WS_VISIBLE | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, @@ -118,26 +102,20 @@ Uninstaller::Uninstaller(const HINSTANCE hInstance) : Uninstaller() ); // Create - SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); - constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; - m_prevBtn = CreateWindow("BUTTON", "< Back", BUTTON_STYLES, 510, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_nextBtn = CreateWindow("BUTTON", "Next >", BUTTON_STYLES | BS_DEFPUSHBUTTON, 600, 460, 85, 30, m_window, NULL, hInstance, NULL); - m_exitBtn = CreateWindow("BUTTON", "Cancel", BUTTON_STYLES, 710, 460, 85, 30, m_window, NULL, hInstance, NULL); - - auto dwStyle = (DWORD)GetWindowLongPtr(m_window, GWL_STYLE); - auto dwExStyle = (DWORD)GetWindowLongPtr(m_window, GWL_EXSTYLE); + SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);auto dwStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_STYLE); + auto dwExStyle = (DWORD)GetWindowLongPtr(m_hwnd, GWL_EXSTYLE); RECT rc = { 0, 0, 800, 500 }; - ShowWindow(m_window, true); - UpdateWindow(m_window); + ShowWindow(m_hwnd, true); + UpdateWindow(m_hwnd); AdjustWindowRectEx(&rc, dwStyle, false, dwExStyle); - SetWindowPos(m_window, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); + SetWindowPos(m_hwnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE); // The portions of the screen that change based on input - m_states[WELCOME_STATE] = new WelcomeState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[UNINSTALL_STATE] = new UninstallState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[FINISH_STATE] = new FinishState(this, hInstance, m_window, { 170,0,800,450 }); - m_states[FAIL_STATE] = new FailState(this, hInstance, m_window, { 170,0,800,450 }); - setState(WELCOME_STATE); + m_screens[WELCOME_SCREEN] = new Welcome(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[UNINSTALL_SCREEN] = new Uninstall(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FINISH_SCREEN] = new Finish(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + m_screens[FAIL_SCREEN] = new Fail(this, hInstance, m_hwnd, { 170,0 }, { 630, 500 }); + setScreen(WELCOME_SCREEN); } #ifndef DEBUG @@ -148,135 +126,187 @@ Uninstaller::Uninstaller(const HINSTANCE hInstance) : Uninstaller() void Uninstaller::invalidate() { - setState(FAIL_STATE); - showButtons(false, false, true); - enableButtons(false, false, true); + setScreen(FAIL_SCREEN); m_valid = false; } -bool Uninstaller::isValid() const -{ - return m_valid; -} - -void Uninstaller::finish() -{ - m_finished = true; -} - -void Uninstaller::setState(const StateEnums & stateIndex) +void Uninstaller::setScreen(const ScreenEnums & screenIndex) { if (m_valid) { - m_states[m_currentIndex]->setVisible(false); - m_states[stateIndex]->enact(); - m_states[stateIndex]->setVisible(true); - m_currentIndex = stateIndex; + m_screens[m_currentIndex]->setVisible(false); + m_screens[screenIndex]->enact(); + m_screens[screenIndex]->setVisible(true); + m_currentIndex = screenIndex; RECT rc = { 0, 0, 160, 500 }; - RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + RedrawWindow(m_hwnd, &rc, NULL, RDW_INVALIDATE); } } -Uninstaller::StateEnums Uninstaller::getCurrentIndex() const -{ - return m_currentIndex; -} - std::wstring Uninstaller::getDirectory() const { return m_directory; } -void Uninstaller::updateButtons(const WORD btnHandle) +void Uninstaller::beginUninstallation() { - if (btnHandle == LOWORD(m_prevBtn)) - m_states[m_currentIndex]->pressPrevious(); - else if (btnHandle == LOWORD(m_nextBtn)) - m_states[m_currentIndex]->pressNext(); - else if (btnHandle == LOWORD(m_exitBtn)) - m_states[m_currentIndex]->pressClose(); - RECT rc = { 0, 0, 160, 500 }; - RedrawWindow(m_window, &rc, NULL, RDW_INVALIDATE); + m_threader.addJob([&]() { + const auto directory = from_wideString(m_directory); + + // Find all installed files + const auto entries = get_file_paths(directory); + + // Find all shortcuts + const auto desktopStrings = m_mfStrings[L"shortcut"], startmenuStrings = m_mfStrings[L"startmenu"]; + size_t numD = std::count(desktopStrings.begin(), desktopStrings.end(), L',') + 1ull, numS = std::count(startmenuStrings.begin(), startmenuStrings.end(), L',') + 1ull; + std::vector shortcuts_d, shortcuts_s; + shortcuts_d.reserve(numD + numS); + shortcuts_s.reserve(numD + numS); + size_t last = 0; + if (!desktopStrings.empty()) + for (size_t x = 0; x < numD; ++x) { + // Find end of shortcut + auto nextComma = desktopStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = desktopStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_d.push_back(desktopStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < desktopStrings.size() && (desktopStrings[last] == L' ' || desktopStrings[last] == L'\r' || desktopStrings[last] == L'\t' || desktopStrings[last] == L'\n')) + last++; + } + last = 0; + if (!startmenuStrings.empty()) + for (size_t x = 0; x < numS; ++x) { + // Find end of shortcut + auto nextComma = startmenuStrings.find(L',', last); + if (nextComma == std::wstring::npos) + nextComma = startmenuStrings.size(); + + // Find demarkation point where left half is the shortcut path, right half is the shortcut name + shortcuts_s.push_back(startmenuStrings.substr(last, nextComma - last)); + + // Skip whitespace, find next element + last = nextComma + 1ull; + while (last < startmenuStrings.size() && (startmenuStrings[last] == L' ' || startmenuStrings[last] == L'\r' || startmenuStrings[last] == L'\t' || startmenuStrings[last] == L'\n')) + last++; + } + + // Set progress bar range to include all files + shortcuts + 1 (cleanup step) + TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 1); + size_t progress = 0ull; + + // Remove all files in the installation folder, list them + std::error_code er; + if (!entries.size()) + TaskLogger::PushText("Already uninstalled / no files found.\r\n"); + else { + for each (const auto & entry in entries) { + TaskLogger::PushText("Deleting file: \"" + entry.path().string() + "\"\r\n"); + std::filesystem::remove(entry, er); + TaskLogger::SetProgress(++progress); + } + } + + // Remove all shortcuts + for each (const auto & shortcut in shortcuts_d) { + const auto path = get_users_desktop() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting desktop shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + for each (const auto & shortcut in shortcuts_s) { + const auto path = get_users_startmenu() + "\\" + std::filesystem::path(shortcut).filename().string() + ".lnk"; + TaskLogger::PushText("Deleting start-menu shortcut: \"" + path + "\"\r\n"); + std::filesystem::remove(path, er); + progress++; + } + + // Clean up whatever's left (empty folders) + std::filesystem::remove_all(directory, er); + TaskLogger::SetProgress(++progress); + }); } -void Uninstaller::showButtons(const bool & prev, const bool & next, const bool & close) +void Uninstaller::dumpErrorLog() { - if (m_valid) { - ShowWindow(m_prevBtn, prev); - ShowWindow(m_nextBtn, next); - ShowWindow(m_exitBtn, close); - } + // Dump error log to disk + const auto dir = get_current_directory() + "\\error_log.txt"; + const auto t = std::time(0); + char dateData[127]; + ctime_s(dateData, 127, &t); + std::string logData(""); + + // If the log doesn't exist, add header text + if (!std::filesystem::exists(dir)) + logData += "Uninstaller error log:\r\n"; + + // Add remaining log data + logData += std::string(dateData) + TaskLogger::PullText() + "\r\n"; + + // Try to create the file + std::filesystem::create_directories(std::filesystem::path(dir).parent_path()); + std::ofstream file(dir, std::ios::binary | std::ios::out | std::ios::app); + if (!file.is_open()) + TaskLogger::PushText("Cannot dump error log to disk...\r\n"); + else + file.write(logData.c_str(), (std::streamsize)logData.size()); + file.close(); } -void Uninstaller::enableButtons(const bool & prev, const bool & next, const bool & close) +void Uninstaller::paint() { - if (m_valid) { - EnableWindow(m_prevBtn, prev); - EnableWindow(m_nextBtn, next); - EnableWindow(m_exitBtn, close); + PAINTSTRUCT ps; + Graphics graphics(BeginPaint(m_hwnd, &ps)); + + // Draw Background + const LinearGradientBrush backgroundGradient1( + Point(0, 0), + Point(0, 500), + Color(255, 25, 25, 25), + Color(255, 75, 75, 75) + ); + graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); + + // Draw Steps + const SolidBrush lineBrush(Color(255, 100, 100, 100)); + graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); + constexpr static wchar_t* step_labels[] = { L"Welcome", L"Uninstall", L"Finish" }; + FontFamily fontFamily(L"Segoe UI"); + Font font(&fontFamily, 15, FontStyleBold, UnitPixel); + REAL vertical_offset = 15; + const auto frameIndex = (int)m_currentIndex; + for (int x = 0; x < 3; ++x) { + // Draw Circle + auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); + if (x == 2 && frameIndex == 3) + color = Color(255, 225, 25, 75); + const SolidBrush brush(color); + Pen pen(color); + graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); + graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20); + graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20); + + // Draw Text + graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); + + if (x == 1) + vertical_offset = 460; + else + vertical_offset += 50; } + + EndPaint(m_hwnd, &ps); } static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - auto ptr = (Uninstaller*)GetWindowLongPtr(hWnd, GWLP_USERDATA); - if (message == WM_PAINT) { - PAINTSTRUCT ps; - Graphics graphics(BeginPaint(hWnd, &ps)); - // Draw Background - const LinearGradientBrush backgroundGradient1( - Point(0, 0), - Point(0, 500), - Color(255, 25, 25, 25), - Color(255, 75, 75, 75) - ); - graphics.FillRectangle(&backgroundGradient1, 0, 0, 170, 500); - - // Draw Steps - const SolidBrush lineBrush(Color(255,100,100,100)); - graphics.FillRectangle(&lineBrush, 28, 0, 5, 500); - constexpr static wchar_t* step_labels[] = { L"Welcome", L"Uninstall", L"Finish" }; - FontFamily fontFamily(L"Segoe UI"); - Font font(&fontFamily, 15, FontStyleBold, UnitPixel); - REAL vertical_offset = 15; - const auto frameIndex = (int)ptr->getCurrentIndex(); - for (int x = 0; x < 3; ++x) { - // Draw Circle - auto color = x < frameIndex ? Color(255, 100, 100, 100) : x == frameIndex ? Color(255, 25, 225, 125) : Color(255, 255, 255, 255); - if (x == 2 && frameIndex == 3) - color = Color(255, 225, 25, 75); - const SolidBrush brush(color); - Pen pen(color); - graphics.SetSmoothingMode(SmoothingMode::SmoothingModeAntiAlias); - graphics.DrawEllipse(&pen, 20, (int)vertical_offset, 20, 20 ); - graphics.FillEllipse(&brush, 20, (int)vertical_offset, 20, 20 ); - - // Draw Text - graphics.DrawString(step_labels[x], -1, &font, PointF{ 50, vertical_offset }, &brush); - - if (x == 1) - vertical_offset = 460; - else - vertical_offset += 50; - } - - // Draw -watermark- - Font regFont(&fontFamily, 14, FontStyleRegular, UnitPixel); - Font regUnderFont(&fontFamily, 14, FontStyleUnderline, UnitPixel); - SolidBrush greyBrush(Color(255, 127, 127, 127)); - SolidBrush blueishBrush(Color(255, 100, 125, 175)); - graphics.DrawString(L"This software was generated using nSuite", -1, ®Font, PointF{ 180, 455 }, &greyBrush); - graphics.DrawString(L"https://github.com/Yattabyte/nSuite", -1, ®UnderFont, PointF{ 180, 475 }, &blueishBrush); - - EndPaint(hWnd, &ps); - return S_OK; - } + const auto ptr = (Uninstaller*)GetWindowLongPtr(hWnd, GWLP_USERDATA); + if (message == WM_PAINT) + ptr->paint(); else if (message == WM_DESTROY) - PostQuitMessage(0); - else if (message == WM_COMMAND) { - if (HIWORD(wParam) == BN_CLICKED) { - ptr->updateButtons(LOWORD(lParam)); - return S_OK; - } - } + PostQuitMessage(0); return DefWindowProc(hWnd, message, wParam, lParam); } \ No newline at end of file diff --git a/src/unStaller/Uninstaller.h b/src/unStaller/Uninstaller.h index 0f6d9ea..95c3a84 100644 --- a/src/unStaller/Uninstaller.h +++ b/src/unStaller/Uninstaller.h @@ -1,14 +1,15 @@ #pragma once -#ifndef INSTALLER_H -#define INSTALLER_H +#ifndef UNINSTALLER_H +#define UNINSTALLER_H #include "Resource.h" +#include "Threader.h" #include #include #include -class FrameState; +class Screen; /** Encapsulates the logical features of the uninstaller. */ class Uninstaller { @@ -19,42 +20,27 @@ class Uninstaller { // Public Enumerations - const enum StateEnums { - WELCOME_STATE, UNINSTALL_STATE, FINISH_STATE, FAIL_STATE, - STATE_COUNT + const enum ScreenEnums { + WELCOME_SCREEN, UNINSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, + SCREEN_COUNT }; // Public Methods /** When called, invalidates the uninstaller, halting it from progressing. */ void invalidate(); - /** Returns whether or not the uninstaller has been invalidated. - @return true if valid, false otherwise. */ - bool isValid() const; - /** Flags the installation as complete. */ - void finish(); - /** Make the state identified by the supplied enum as active, deactivating the previous state. - @param stateIndex the new state to use. */ - void setState(const StateEnums & stateIndex); - /** Retrieves the current frame's enumeration. - @return the current frame's index, as an enumeration. */ - StateEnums getCurrentIndex() const; - /** Retrieves the current directory chosen for installation. + /** Make the screen identified by the supplied enum as active, deactivating the previous screen. + @param screenIndex the new screen to use. */ + void setScreen(const ScreenEnums & screenIndex); + /** Retrieves the installation directory. @return active installation directory. */ std::wstring getDirectory() const; - /** Check which button has been active, and perform it's state operation. - @param btnHandle handle to the currently active button. */ - void updateButtons(const WORD btnHandle); - /** Sets the visibility state of all 3 buttons. - @param prev make the 'previous' button visible. - @param next make the 'next' button visible. - @param close make the 'close' button visible. */ - void showButtons(const bool & prev, const bool & next, const bool & close); - /** Sets the enable state of all 3 buttons. - @param prev make the 'previous' button enabled. - @param next make the 'next' button enabled. - @param close make the 'close' button enabled. */ - void enableButtons(const bool & prev, const bool & next, const bool & close); + /** Uninstall the application. */ + void beginUninstallation(); + /** Dumps error log to disk. */ + static void dumpErrorLog(); + /** Render this window. */ + void paint(); // Public manifest strings @@ -72,17 +58,14 @@ class Uninstaller { // Private Attributes + Threader m_threader; Resource m_manifest; std::wstring m_directory = L""; - bool m_valid = true, m_finished = false; - StateEnums m_currentIndex = WELCOME_STATE; - FrameState * m_states[STATE_COUNT]; - HWND - m_window = nullptr, - m_prevBtn = nullptr, - m_nextBtn = nullptr, - m_exitBtn = nullptr; + bool m_valid = true; + ScreenEnums m_currentIndex = WELCOME_SCREEN; + Screen * m_screens[SCREEN_COUNT]; + HWND m_hwnd = nullptr; }; -#endif // INSTALLER_H \ No newline at end of file +#endif // UNINSTALLER_H \ No newline at end of file From 3b119a333f772609b5d6a54f12595ed7c5b44f05 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Wed, 17 Apr 2019 19:56:36 -0500 Subject: [PATCH 26/44] Uninstaller removes registry entry The uninstaller now removes its registry entry. --- src/unStaller/Uninstaller.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/unStaller/Uninstaller.cpp b/src/unStaller/Uninstaller.cpp index 80b3f14..6f42f9b 100644 --- a/src/unStaller/Uninstaller.cpp +++ b/src/unStaller/Uninstaller.cpp @@ -195,7 +195,7 @@ void Uninstaller::beginUninstallation() } // Set progress bar range to include all files + shortcuts + 1 (cleanup step) - TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 1); + TaskLogger::SetRange(entries.size() + shortcuts_d.size() + shortcuts_s.size() + 2); size_t progress = 0ull; // Remove all files in the installation folder, list them @@ -227,6 +227,10 @@ void Uninstaller::beginUninstallation() // Clean up whatever's left (empty folders) std::filesystem::remove_all(directory, er); TaskLogger::SetProgress(++progress); + + // Remove registry entry for this uninstaller + RegDeleteKeyExW(HKEY_LOCAL_MACHINE, (L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + m_mfStrings[L"name"]).c_str(), KEY_ALL_ACCESS, NULL); + TaskLogger::SetProgress(++progress); }); } From 325b77f1ed9b349da5dd7faab17a75deeac97776 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 19 Apr 2019 15:12:56 -0500 Subject: [PATCH 27/44] Added portable-installer, naming changes Added portable installer, basically our old non-gui option (for lightweight installing with no registry changes) Changed naming so nStaller is Installer, nUpdater is updater, etc (nSuite stays same) --- CMakeLists.txt | 7 +- README.md | 50 +++++----- src/{nStaller => Installer}/CMakeLists.txt | 10 +- src/{nStaller => Installer}/Installer.cpp | 0 .../Installer.dir/Release/Installer.res | Bin 0 -> 225008 bytes src/{nStaller => Installer}/Installer.h | 0 src/{nStaller => Installer}/Installer.rc | Bin 3416 -> 3418 bytes .../Screens/Agreement.cpp | 0 .../Screens/Agreement.h | 0 .../Screens/Directory.cpp | 0 .../Screens/Directory.h | 0 src/{nStaller => Installer}/Screens/Fail.cpp | 0 src/{nStaller => Installer}/Screens/Fail.h | 0 .../Screens/Finish.cpp | 0 src/{nStaller => Installer}/Screens/Finish.h | 0 .../Screens/Install.cpp | 0 src/{nStaller => Installer}/Screens/Install.h | 0 src/{nStaller => Installer}/Screens/Screen.h | 0 .../Screens/Welcome.cpp | 0 src/{nStaller => Installer}/Screens/Welcome.h | 0 src/{nStaller => Installer}/archive.npack | 0 src/{nStaller => Installer}/icon.ico | Bin src/{nStaller => Installer}/manifest.nman | 0 src/{unStaller => Uninstaller}/CMakeLists.txt | 8 +- .../Screens/Fail.cpp | 0 src/{unStaller => Uninstaller}/Screens/Fail.h | 0 .../Screens/Finish.cpp | 0 .../Screens/Finish.h | 0 .../Screens/Screen.h | 0 .../Screens/Uninstall.cpp | 0 .../Screens/Uninstall.h | 0 .../Screens/Welcome.cpp | 0 .../Screens/Welcome.h | 0 .../Uninstaller.cpp | 0 .../Uninstaller.dir}/Release/Uninstaller.res | Bin 33960 -> 33952 bytes src/{unStaller => Uninstaller}/Uninstaller.h | 0 src/{unStaller => Uninstaller}/Uninstaller.rc | Bin 3038 -> 3030 bytes src/{unStaller => Uninstaller}/icon.ico | Bin src/{unStaller => Uninstaller}/manifest.nman | 0 src/Unpacker/CMakeLists.txt | 49 ++++++++++ src/Unpacker/Unpacker.cpp | 54 +++++++++++ .../Unpacker.dir/Release/Unpacker.res | Bin 0 -> 33920 bytes src/Unpacker/Unpacker.h | 88 ++++++++++++++++++ src/Unpacker/Unpacker.rc | Bin 0 -> 3002 bytes src/Unpacker/archive.npack | 0 src/Unpacker/icon.ico | Bin 0 -> 32888 bytes src/{nUpdater => Updater}/CMakeLists.txt | 6 +- .../nUpdater.cpp => Updater/Updater.cpp} | 6 +- src/Updater/Updater.dir/Release/Updater.res | Bin 0 -> 22424 bytes src/Updater/Updater.rc | Bin 0 -> 2932 bytes src/{nUpdater => Updater}/icon.ico | Bin src/nSuite/CMakeLists.txt | 2 +- src/nSuite/Commands/DiffCommand.cpp | 2 +- src/nSuite/Commands/InstallerCommand.cpp | 2 +- src/nSuite/Commands/PackCommand.cpp | 8 +- src/nSuite/Commands/PackagerCommand.cpp | 77 +++++++++++++++ src/nSuite/Commands/PackagerCommand.h | 15 +++ src/nSuite/Commands/PatchCommand.cpp | 2 +- src/nSuite/Commands/UnpackCommand.cpp | 2 +- src/nSuite/README.md | 39 ++++++++ src/nSuite/nSuite.cpp | 13 ++- src/nSuite/nSuite.rc | Bin 1530 -> 3474 bytes src/nUpdater/nUpdater.rc | Bin 1240 -> 0 bytes src/resource.h | 9 +- 64 files changed, 387 insertions(+), 62 deletions(-) rename src/{nStaller => Installer}/CMakeLists.txt (94%) rename src/{nStaller => Installer}/Installer.cpp (100%) create mode 100644 src/Installer/Installer.dir/Release/Installer.res rename src/{nStaller => Installer}/Installer.h (100%) rename src/{nStaller => Installer}/Installer.rc (89%) rename src/{nStaller => Installer}/Screens/Agreement.cpp (100%) rename src/{nStaller => Installer}/Screens/Agreement.h (100%) rename src/{nStaller => Installer}/Screens/Directory.cpp (100%) rename src/{nStaller => Installer}/Screens/Directory.h (100%) rename src/{nStaller => Installer}/Screens/Fail.cpp (100%) rename src/{nStaller => Installer}/Screens/Fail.h (100%) rename src/{nStaller => Installer}/Screens/Finish.cpp (100%) rename src/{nStaller => Installer}/Screens/Finish.h (100%) rename src/{nStaller => Installer}/Screens/Install.cpp (100%) rename src/{nStaller => Installer}/Screens/Install.h (100%) rename src/{nStaller => Installer}/Screens/Screen.h (100%) rename src/{nStaller => Installer}/Screens/Welcome.cpp (100%) rename src/{nStaller => Installer}/Screens/Welcome.h (100%) rename src/{nStaller => Installer}/archive.npack (100%) rename src/{nStaller => Installer}/icon.ico (100%) rename src/{nStaller => Installer}/manifest.nman (100%) rename src/{unStaller => Uninstaller}/CMakeLists.txt (95%) rename src/{unStaller => Uninstaller}/Screens/Fail.cpp (100%) rename src/{unStaller => Uninstaller}/Screens/Fail.h (100%) rename src/{unStaller => Uninstaller}/Screens/Finish.cpp (100%) rename src/{unStaller => Uninstaller}/Screens/Finish.h (100%) rename src/{unStaller => Uninstaller}/Screens/Screen.h (100%) rename src/{unStaller => Uninstaller}/Screens/Uninstall.cpp (100%) rename src/{unStaller => Uninstaller}/Screens/Uninstall.h (100%) rename src/{unStaller => Uninstaller}/Screens/Welcome.cpp (100%) rename src/{unStaller => Uninstaller}/Screens/Welcome.h (100%) rename src/{unStaller => Uninstaller}/Uninstaller.cpp (100%) rename src/{unStaller/unStaller.dir => Uninstaller/Uninstaller.dir}/Release/Uninstaller.res (97%) rename src/{unStaller => Uninstaller}/Uninstaller.h (100%) rename src/{unStaller => Uninstaller}/Uninstaller.rc (92%) rename src/{unStaller => Uninstaller}/icon.ico (100%) rename src/{unStaller => Uninstaller}/manifest.nman (100%) create mode 100644 src/Unpacker/CMakeLists.txt create mode 100644 src/Unpacker/Unpacker.cpp create mode 100644 src/Unpacker/Unpacker.dir/Release/Unpacker.res create mode 100644 src/Unpacker/Unpacker.h create mode 100644 src/Unpacker/Unpacker.rc create mode 100644 src/Unpacker/archive.npack create mode 100644 src/Unpacker/icon.ico rename src/{nUpdater => Updater}/CMakeLists.txt (96%) rename src/{nUpdater/nUpdater.cpp => Updater/Updater.cpp} (96%) create mode 100644 src/Updater/Updater.dir/Release/Updater.res create mode 100644 src/Updater/Updater.rc rename src/{nUpdater => Updater}/icon.ico (100%) create mode 100644 src/nSuite/Commands/PackagerCommand.cpp create mode 100644 src/nSuite/Commands/PackagerCommand.h create mode 100644 src/nSuite/README.md delete mode 100644 src/nUpdater/nUpdater.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index 66f0e6f..e261141 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,9 +24,10 @@ link_libraries( ) # add all sub-projects and plugins here -add_subdirectory( "src/unStaller" ) -add_subdirectory( "src/nStaller" ) -add_subdirectory( "src/nUpdater" ) +add_subdirectory( "src/Installer" ) +add_subdirectory( "src/Uninstaller" ) +add_subdirectory( "src/Unpacker" ) +add_subdirectory( "src/Updater" ) add_subdirectory( "src/nSuite" ) # Visual studio specific setting: make nSuite the startup project diff --git a/README.md b/README.md index b525c12..7e8fae1 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,35 @@ -# nSuite Packaging Tools +# nSuite Directory Tools -This project is is a toolset that allows users to easily package folders into portable installers, as well as easily diff 2 directories, generating patchfiles that can be used by our updater tool. -The project executables are made to work for Windows 7/10. -However, the library was made in C++17, and the functions provided in BufferTools/DirectoryTools should prove general enough to be adapted for other OS's. +This toolset provides users with the ability to package and diff directories and files. -## nSuite -The nSuite program is intended to be used by developers or those who wish to package/diff/distribute one or many files. It is run by command-line, and requires one of the following sets of arguments to be fulfilled: -- #### `-installer -src= -dst=` - - Packages + compress an entire **source** directory into a **destionation** installer *.exe file* (filename optional) - -- #### `-pack -src= -dst=` - - Same as the installer, though doesn't embed into an installer, just a *.npack* file (filename optional) - - Package files can be read-through by the diffing command, so many versions of a directory can be easily stored on disk in single snapshots. + +## Packaging +nSuite can package directories in 3 ways: +- A fully fledged installer with a GUI (Windows) + - Customizable by writing attributes into a manifest file + - Generates an uninstaller (adds it to the registry) + - .npack file embedded within -- #### `-unpack -src= -dst=` - - Decompresses the files held in the **source** *.npack* file, dumping into the **destination** directory. +- A lightweight portable package/installer + - Extracts to a folder in the directory it runs from + - Runs in a terminal, no user input + - Doesn't modify registry - no uninstaller + - .npack file embedded within -- #### `-diff -old= -new= -dst=` - - Finds all the common, new, and old files between the **old** and **new** directories. All common files are analyzed bytewise for their differences, and patch instructions are generated. All instructions are compressed and stored in a **destination** *.ndiff* file. ***All*** files are hashed, to ensure the right versions of files are consumed at patch-time. - - Can use *.npack* files as the **old** ***or*** **new** directories (serving as snapshots). Diffing packages means storing less files and folders across multiple versions on disk -> just 1 snapshot per version. +- A .npack file + - Can be unpacked using nSuite + - - #### `-patch -src= -dst=` - - Uses a **source** *.ndiff* file and executes all the instructions contained within, patching the **destination** directory. All files within the directory are hashed, and must match the hashes found in the patch. Additionally, the post-patch results must match what's expected in the hash. If any of the strict conditions aren't met, it will halt prior to any files being modified (preventing against file corruption). - - -## Installer -The installer tool is a portable version of the unpack command, and is generated by nSuite. Each one will have a custom *.npack* file embedded within. The current version of the installer is implemented using Windows GDI+ (whereas the remaining tools are all command-line). +## Diffing +nSuite can also be used to generate patch files. These can be applied to a directory using nSuite, or by using our stand-alone updater tool (also provided). + +The updater tool automatically applies all .ndiff files it can find next to it, and if successfull, deletes them afterwards. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers for real-world use. -## Updater -The updater tool is a portable version of the patch command. It automatically applies all *.ndiff* files it can find, and if successfull, deletes them afterwards. This tool is a naiive implementation of an updater, and would ideally be expanded on by other developers. For instance, if patches were found that would modify an app from v.1 -> v.3 -> v.2, the updater won't stop at v.3, it will proceed to v.2. Further, this updater cannot connect to any servers to fetch patch data, but that would be the next logical step after implementing versioning. +The tool and diff files should be kept at the root of an affected directory. It will attempt to apply all patches it can find, even if the patched version is technically 'older'. # Dependencies/Requirements - - 64-bit only + - C++ 17 + - 64-bit - Windows 7/8/10 - Uses [CMake](https://cmake.org/) - Requires the [LZ4 - Compression Library](https://github.com/lz4/lz4) to build, but **does not** come bundled with it diff --git a/src/nStaller/CMakeLists.txt b/src/Installer/CMakeLists.txt similarity index 94% rename from src/nStaller/CMakeLists.txt rename to src/Installer/CMakeLists.txt index 3c1447a..4cd1239 100644 --- a/src/nStaller/CMakeLists.txt +++ b/src/Installer/CMakeLists.txt @@ -1,7 +1,7 @@ -################ -### nStaller ### -################ -set (Module nStaller) +################# +### Installer ### +################# +set (Module Installer) # Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") @@ -35,7 +35,7 @@ target_link_libraries(${Module} ) # This module requires the uninstaller to be built first -add_dependencies(nStaller unStaller) +add_dependencies(Installer Uninstaller) # Set windows/visual studio settings set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") diff --git a/src/nStaller/Installer.cpp b/src/Installer/Installer.cpp similarity index 100% rename from src/nStaller/Installer.cpp rename to src/Installer/Installer.cpp diff --git a/src/Installer/Installer.dir/Release/Installer.res b/src/Installer/Installer.dir/Release/Installer.res new file mode 100644 index 0000000000000000000000000000000000000000..b4757323e20831e2a98b88c57c46f5e48b767899 GIT binary patch literal 225008 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 literal 0 HcmV?d00001 diff --git a/src/nStaller/Installer.h b/src/Installer/Installer.h similarity index 100% rename from src/nStaller/Installer.h rename to src/Installer/Installer.h diff --git a/src/nStaller/Installer.rc b/src/Installer/Installer.rc similarity index 89% rename from src/nStaller/Installer.rc rename to src/Installer/Installer.rc index 7e9faa6212ff9a5ff57ea2109ec9385eb0b5fd32..d4b7cc5a7555b35008291abc022f59e5b1c242bc 100644 GIT binary patch delta 116 zcmca1bxUf)DyGRxSOg~TV2Wc6WyoX5oP3a7ezF;J276FQWoT7|+V0jOAB}RkI+3YJBCokdVvGx-Gv-{b^NDWGsQ H=Urw1Gn66+ delta 104 zcmca5bwg^yDyGS6m~vQ48S)r{Co3{H!&&m1&oCci;#Xh@U?^h9XUJkmWk_ZyncT~k x$)C(n%23Qu0u;??;`=L4v73PA%tMW&k359H#&P diff --git a/src/nStaller/Screens/Agreement.cpp b/src/Installer/Screens/Agreement.cpp similarity index 100% rename from src/nStaller/Screens/Agreement.cpp rename to src/Installer/Screens/Agreement.cpp diff --git a/src/nStaller/Screens/Agreement.h b/src/Installer/Screens/Agreement.h similarity index 100% rename from src/nStaller/Screens/Agreement.h rename to src/Installer/Screens/Agreement.h diff --git a/src/nStaller/Screens/Directory.cpp b/src/Installer/Screens/Directory.cpp similarity index 100% rename from src/nStaller/Screens/Directory.cpp rename to src/Installer/Screens/Directory.cpp diff --git a/src/nStaller/Screens/Directory.h b/src/Installer/Screens/Directory.h similarity index 100% rename from src/nStaller/Screens/Directory.h rename to src/Installer/Screens/Directory.h diff --git a/src/nStaller/Screens/Fail.cpp b/src/Installer/Screens/Fail.cpp similarity index 100% rename from src/nStaller/Screens/Fail.cpp rename to src/Installer/Screens/Fail.cpp diff --git a/src/nStaller/Screens/Fail.h b/src/Installer/Screens/Fail.h similarity index 100% rename from src/nStaller/Screens/Fail.h rename to src/Installer/Screens/Fail.h diff --git a/src/nStaller/Screens/Finish.cpp b/src/Installer/Screens/Finish.cpp similarity index 100% rename from src/nStaller/Screens/Finish.cpp rename to src/Installer/Screens/Finish.cpp diff --git a/src/nStaller/Screens/Finish.h b/src/Installer/Screens/Finish.h similarity index 100% rename from src/nStaller/Screens/Finish.h rename to src/Installer/Screens/Finish.h diff --git a/src/nStaller/Screens/Install.cpp b/src/Installer/Screens/Install.cpp similarity index 100% rename from src/nStaller/Screens/Install.cpp rename to src/Installer/Screens/Install.cpp diff --git a/src/nStaller/Screens/Install.h b/src/Installer/Screens/Install.h similarity index 100% rename from src/nStaller/Screens/Install.h rename to src/Installer/Screens/Install.h diff --git a/src/nStaller/Screens/Screen.h b/src/Installer/Screens/Screen.h similarity index 100% rename from src/nStaller/Screens/Screen.h rename to src/Installer/Screens/Screen.h diff --git a/src/nStaller/Screens/Welcome.cpp b/src/Installer/Screens/Welcome.cpp similarity index 100% rename from src/nStaller/Screens/Welcome.cpp rename to src/Installer/Screens/Welcome.cpp diff --git a/src/nStaller/Screens/Welcome.h b/src/Installer/Screens/Welcome.h similarity index 100% rename from src/nStaller/Screens/Welcome.h rename to src/Installer/Screens/Welcome.h diff --git a/src/nStaller/archive.npack b/src/Installer/archive.npack similarity index 100% rename from src/nStaller/archive.npack rename to src/Installer/archive.npack diff --git a/src/nStaller/icon.ico b/src/Installer/icon.ico similarity index 100% rename from src/nStaller/icon.ico rename to src/Installer/icon.ico diff --git a/src/nStaller/manifest.nman b/src/Installer/manifest.nman similarity index 100% rename from src/nStaller/manifest.nman rename to src/Installer/manifest.nman diff --git a/src/unStaller/CMakeLists.txt b/src/Uninstaller/CMakeLists.txt similarity index 95% rename from src/unStaller/CMakeLists.txt rename to src/Uninstaller/CMakeLists.txt index 2f00e7a..5c28e26 100644 --- a/src/unStaller/CMakeLists.txt +++ b/src/Uninstaller/CMakeLists.txt @@ -1,7 +1,7 @@ -################# -### unStaller ### -################# -set (Module unStaller) +################### +### Uninstaller ### +################### +set (Module Uninstaller) # Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") diff --git a/src/unStaller/Screens/Fail.cpp b/src/Uninstaller/Screens/Fail.cpp similarity index 100% rename from src/unStaller/Screens/Fail.cpp rename to src/Uninstaller/Screens/Fail.cpp diff --git a/src/unStaller/Screens/Fail.h b/src/Uninstaller/Screens/Fail.h similarity index 100% rename from src/unStaller/Screens/Fail.h rename to src/Uninstaller/Screens/Fail.h diff --git a/src/unStaller/Screens/Finish.cpp b/src/Uninstaller/Screens/Finish.cpp similarity index 100% rename from src/unStaller/Screens/Finish.cpp rename to src/Uninstaller/Screens/Finish.cpp diff --git a/src/unStaller/Screens/Finish.h b/src/Uninstaller/Screens/Finish.h similarity index 100% rename from src/unStaller/Screens/Finish.h rename to src/Uninstaller/Screens/Finish.h diff --git a/src/unStaller/Screens/Screen.h b/src/Uninstaller/Screens/Screen.h similarity index 100% rename from src/unStaller/Screens/Screen.h rename to src/Uninstaller/Screens/Screen.h diff --git a/src/unStaller/Screens/Uninstall.cpp b/src/Uninstaller/Screens/Uninstall.cpp similarity index 100% rename from src/unStaller/Screens/Uninstall.cpp rename to src/Uninstaller/Screens/Uninstall.cpp diff --git a/src/unStaller/Screens/Uninstall.h b/src/Uninstaller/Screens/Uninstall.h similarity index 100% rename from src/unStaller/Screens/Uninstall.h rename to src/Uninstaller/Screens/Uninstall.h diff --git a/src/unStaller/Screens/Welcome.cpp b/src/Uninstaller/Screens/Welcome.cpp similarity index 100% rename from src/unStaller/Screens/Welcome.cpp rename to src/Uninstaller/Screens/Welcome.cpp diff --git a/src/unStaller/Screens/Welcome.h b/src/Uninstaller/Screens/Welcome.h similarity index 100% rename from src/unStaller/Screens/Welcome.h rename to src/Uninstaller/Screens/Welcome.h diff --git a/src/unStaller/Uninstaller.cpp b/src/Uninstaller/Uninstaller.cpp similarity index 100% rename from src/unStaller/Uninstaller.cpp rename to src/Uninstaller/Uninstaller.cpp diff --git a/src/unStaller/unStaller.dir/Release/Uninstaller.res b/src/Uninstaller/Uninstaller.dir/Release/Uninstaller.res similarity index 97% rename from src/unStaller/unStaller.dir/Release/Uninstaller.res rename to src/Uninstaller/Uninstaller.dir/Release/Uninstaller.res index b2f51aa75b6e013ec53a9b46dc91fa03345d89dc..c44a914bbbafed2ff2d3e305b0a15e69535e3024 100644 GIT binary patch delta 146 zcmZ3{$+V!8X+utZ}1nEban459Vz9z22F1zEO_ti}45Z~inEban43k1%?2IB8Gg1EQVBuWQG!kVz9;}2HDBQO_tor45dJk5}FdA-V WZ02R0e4t5evPz2%qv7Ph7JmT#(IKz^ diff --git a/src/unStaller/Uninstaller.h b/src/Uninstaller/Uninstaller.h similarity index 100% rename from src/unStaller/Uninstaller.h rename to src/Uninstaller/Uninstaller.h diff --git a/src/unStaller/Uninstaller.rc b/src/Uninstaller/Uninstaller.rc similarity index 92% rename from src/unStaller/Uninstaller.rc rename to src/Uninstaller/Uninstaller.rc index c59850fced2e936302cafea88ec2d6182bb1c476..0099a1a83bfe89b7a887aa80eeb2cc3a823c1a31 100644 GIT binary patch delta 103 zcmca7eocJCFBZn)$@MJalNDGwChuY4n{2^qC7RBV%8&=dMGT1yB|tibL4l!kvLd_q wGx)>QlCpWS>PR?TIoBWJjiqT**Bgb840MK_EcK`qY delta 93 zcmca6eouVEFBU-sh5&{lhJ1!BhE#@Rh7yM2$&4K0lLc5gCg-v7P1azw;!kEMWhe&9 vx?q_$Lyo*(d(Qxx!){TseAc0Iq!^wA9:DEBUG>) +set_target_properties(${Module} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + PDB_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} + VS_DEBUGGER_WORKING_DIRECTORY "$(SolutionDir)app" +) + +# Force highest c++ version supported +if (MSVC_VERSION GREATER_EQUAL "1900") + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) + if (_cpp_latest_flag_supported) + add_compile_options("/std:c++latest") + set_target_properties(${Module} PROPERTIES CXX_STANDARD 17) + set_target_properties(${Module} PROPERTIES CXX_STANDARD_REQUIRED ON) + endif() +endif() \ No newline at end of file diff --git a/src/Unpacker/Unpacker.cpp b/src/Unpacker/Unpacker.cpp new file mode 100644 index 0000000..5ea4e34 --- /dev/null +++ b/src/Unpacker/Unpacker.cpp @@ -0,0 +1,54 @@ +#include "Common.h" +#include "DirectoryTools.h" +#include "Resource.h" +#include + + +/** Entry point. */ +int main() +{ + // Check command line arguments + const std::string dstDirectory(get_current_directory()); + + // Report an overview of supplied procedure + std::cout << + " ~\r\n" + " Unpackager /\r\n" + " ~------------------~\r\n" + " /\r\n" + "~\r\n\r\n"; + + // Acquire archive resource + const auto start = std::chrono::system_clock::now(); + size_t fileCount(0ull), byteCount(0ull); + Resource archive(IDR_ARCHIVE, "ARCHIVE"); + if (!archive.exists()) + exit_program("Cannot access archive resource (may be absent, corrupt, or have different identifiers), aborting...\r\n"); + + // Read the package header + char * packBufferOffset = reinterpret_cast(archive.getPtr()); + const auto folderSize = *reinterpret_cast(packBufferOffset); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + const char * folderArray = reinterpret_cast(packBufferOffset); + const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + // Report an overview of supplied procedure + std::cout << + "Unpacking to the following directory:\r\n" + "\t> " + finalDestionation + + "\r\n"; + + // Unpackage using the resource file + if (!DRT::DecompressDirectory(finalDestionation, packBufferOffset, archive.getSize() - (size_t(sizeof(size_t)) + folderSize), byteCount, fileCount)) + exit_program("Cannot decompress embedded package resource, aborting...\r\n"); + + // Success, report results + const auto end = std::chrono::system_clock::now(); + const std::chrono::duration elapsed_seconds = end - start; + std::cout + << "Files written: " << fileCount << "\r\n" + << "Bytes written: " << byteCount << "\r\n" + << "Total duration: " << elapsed_seconds.count() << " seconds\r\n\r\n"; + system("pause"); + exit(EXIT_SUCCESS); +} \ No newline at end of file diff --git a/src/Unpacker/Unpacker.dir/Release/Unpacker.res b/src/Unpacker/Unpacker.dir/Release/Unpacker.res new file mode 100644 index 0000000000000000000000000000000000000000..31ba97344e871787c35f5195400dfe13c875830b GIT binary patch literal 33920 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 Gko`ZR-^O$R literal 0 HcmV?d00001 diff --git a/src/Unpacker/Unpacker.h b/src/Unpacker/Unpacker.h new file mode 100644 index 0000000..a36f1fb --- /dev/null +++ b/src/Unpacker/Unpacker.h @@ -0,0 +1,88 @@ +#pragma once +#ifndef INSTALLER_H +#define INSTALLER_H + +#include "Resource.h" +#include "Threader.h" +#include +#include +#include + + +class Screen; + +/** Encapsulates the logical features of the installer. */ +class Installer { +public: + // Public (de)Constructors + ~Installer() = default; + Installer(const HINSTANCE hInstance); + + + // Public Enumerations + const enum ScreenEnums { + WELCOME_SCREEN, AGREEMENT_SCREEN, DIRECTORY_SCREEN, INSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, + SCREEN_COUNT + }; + + + // Public Methods + /** When called, invalidates the installer, halting it from progressing. */ + void invalidate(); + /** Make the screen identified by the supplied enum as active, deactivating the previous screen. + @param screenIndex the new screen to use. */ + void setScreen(const ScreenEnums & screenIndex); + /** Retrieves the current directory chosen for installation. + @return active installation directory. */ + std::string getDirectory() const; + /** Sets a new installation directory. + @param directory new installation directory. */ + void setDirectory(const std::string & directory); + /** Retrieves the size of the drive used in the current directory. + @return the drive capacity. */ + size_t getDirectorySizeCapacity() const; + /** Retrieves the remaining size of the drive used in the current directory. + @return the available size. */ + size_t getDirectorySizeAvailable() const; + /** Retrieves the required size of the uncompressed package to be installed. + @return the (uncompressed) package size. */ + size_t getDirectorySizeRequired() const; + /** Retrieves the package name. + @return the package name. */ + std::string getPackageName() const; + /** Install the installer's package contents to the directory previously chosen. */ + void beginInstallation(); + /** Dumps error log to disk. */ + static void dumpErrorLog(); + /** Render this window. */ + void paint(); + + + // Public manifest strings + struct compare_string { + bool operator()(const wchar_t * a, const wchar_t * b) const { + return wcscmp(a, b) < 0; + } + }; + std::map m_mfStrings; + + +private: + // Private Constructors + Installer(); + + + // Private Attributes + Threader m_threader; + Resource m_archive, m_manifest; + std::string m_directory = "", m_packageName = ""; + bool m_valid = true; + char * m_packagePtr = nullptr; + size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; + ScreenEnums m_currentIndex = WELCOME_SCREEN; + Screen * m_screens[SCREEN_COUNT]; + HWND m_hwnd = nullptr; +}; + + +#endif // INSTALLER_H \ No newline at end of file diff --git a/src/Unpacker/Unpacker.rc b/src/Unpacker/Unpacker.rc new file mode 100644 index 0000000000000000000000000000000000000000..42c58347b723bf81d029c118809ef9f898df03ca GIT binary patch literal 3002 zcmchZZEM<46vxlo4fY+f_-2DqOxrhmSsy1&Rn$la1+iAGY*SIYb+FIA?RW0=8e^hN zDI?_R-1GeZ-;@0Los)@7WguVWNgiYQ?j%T%>$WU_|ogC3cRS$0y zH9W{W@~z12I`=ktMpRLxjvjS{9PhFE#o_I>?BJs%RV>QX9busnL!@=25%(%9-bP~e zs=U|e$tU^1S{uE>UX32BwtMQiX8PUZUNoH!l zUXw1_sa0CyOW&~I5ij2j7p5{tHjpoCPN>H&x%G^f7Ej8AI`1{E6`sAF6(?6q4~-gC zmv?kL3FwNRd2?FbCK)%VJtFVUKUp~M71P-&+1*fOTds+mmuI*u%hmDdw8NLn<=&-%V3M?EgrHgUH)?5d{Wlvd$#h}SKf zl3Ffu0lTU}$Hw)D<{9oa=&N?dCxaEZ>pmTW(1gl{SWL-vw#MCBsa=-0cr^qu=T$4o z%SThuJy**~r|a&AJvyx}IELV(`^qQ6OuiXrPUR=O>+?25t~<-8SAFVPalL1C#maI| z>}>C@10`cQFrAs>XJU4ki*iMOJkBZz-Gjqne&(!A^KzEU9p;jgYs+4w`GMNi&J{V zV?Gzn1U{#>0PY+Uv$2k$Jl47#j{3IEkC&*gNvs+B`H9)MN_8;Firsj})Rc^KIurCu zRa#FT%eJ&czen-z?b5ZyriZ6YgXEs=;H?ibOOVn1xy@#sYIasAGXhcc^Lw(5^VWHm zgJ;`w)yj0r{*Q0Ja#bU~?6<$jtg9QpaW(t@i1ho~1-ENvqeWP)0_yJ|x#g-FMKeA1 zE9V){*;6)?cVE>3>3B?%<3)r#q8C*Rr#^I5DeJVr8>$A0rR_ K*QQb2_Wc)9r-9)B literal 0 HcmV?d00001 diff --git a/src/Unpacker/archive.npack b/src/Unpacker/archive.npack new file mode 100644 index 0000000..e69de29 diff --git a/src/Unpacker/icon.ico b/src/Unpacker/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..24e5ebbf3aa6312a3144ab637e7305ce87e54763 GIT binary patch literal 32888 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 Op!Z_(Qq)NYtN1_W`7y`< literal 0 HcmV?d00001 diff --git a/src/nUpdater/CMakeLists.txt b/src/Updater/CMakeLists.txt similarity index 96% rename from src/nUpdater/CMakeLists.txt rename to src/Updater/CMakeLists.txt index 0571401..cdc70e9 100644 --- a/src/nUpdater/CMakeLists.txt +++ b/src/Updater/CMakeLists.txt @@ -1,7 +1,7 @@ +############### +### Updater ### ################ -### nUpdater ### -################ -set (Module nUpdater) +set (Module Updater) # Get source files file (GLOB_RECURSE ROOT RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*.cpp" "*.c" "*.h" "*.rc") diff --git a/src/nUpdater/nUpdater.cpp b/src/Updater/Updater.cpp similarity index 96% rename from src/nUpdater/nUpdater.cpp rename to src/Updater/Updater.cpp index d6c96cf..0305e6e 100644 --- a/src/nUpdater/nUpdater.cpp +++ b/src/Updater/Updater.cpp @@ -30,9 +30,9 @@ int main() // Report an overview of supplied procedure TaskLogger::PushText( - " ~\r\n" - " Updater /\r\n" - " ~-----------------~\r\n" + " ~\r\n" + " Updater /\r\n" + " ~------------------~\r\n" " /\r\n" "~\r\n\r\n" "There are " + std::to_string(patches.size()) + " patches(s) found.\r\n" diff --git a/src/Updater/Updater.dir/Release/Updater.res b/src/Updater/Updater.dir/Release/Updater.res new file mode 100644 index 0000000000000000000000000000000000000000..1efe3ba99c715fa92569880a1acc0aa7cdaaae94 GIT binary patch literal 22424 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 literal 0 HcmV?d00001 diff --git a/src/Updater/Updater.rc b/src/Updater/Updater.rc new file mode 100644 index 0000000000000000000000000000000000000000..adf55e4483861161ebf279775f16a6dac02f6c20 GIT binary patch literal 2932 zcmchZ+iu!G5QgWrmHG~@xoM>;!bxs=84jT+PCzk6sf2_`;-r?^fPkp2)Msz|%`S^s zFsRU~vV5AI!$0%Sto`d}MRIwPJNYcnaxa+#63R9AnT(N4S;^!`J|TT2FQsC19*m9Z zo=H~{=}9ULX-h|j*fx`scvkxvgo^Fq;D|1& zdU);Sa4&DbdoDK%>OJIMH#%iI@tOvPOg>#8f{QE*K`7j$U@JmIjwFN{~fTV;Oza8h4X%HD$xSn zCDlvhg2+|*CMR@3yzHWfWnU;)P3o6YX&-0d$$E&E_8DD2qpx=BS6B444|5Rs79H?e z-=|>I<8)zDRIA4nbWIOv77jzPZrS&>T;v*dEyIqD8xSopz0#qsTA2_8bEZ}2>Jf%= zGRv@d0_%7&yVgqW%Dfe;F^oB{nn_jOn~ZL`+75EM?!Mcj)9S-x3_m&tArZ#%MJ6%= z<9GUQ#9M}3Cn2O(Bl4MZy=8UI%4|#Qa__DJ4P$v{Dl@@PZsx&7IU{z!nXuF4CQ3l{ zG9(!miXek;o%)>jjA@!P(^sFF3)}ZD&LBE%6&u! ztXDN%!<^BHTJzPea){mCXDvqm(fC&WCR9Ru?vd}q96y%3&6R*Xwr+OOV={~Ar+s2} z7BRH9H7m!hzODAm-)Gk)(im2s4b$t@^ZUe)wog8Ol*4;KzcdYxyG@CHPl{>oQ?co? zeuwN1emC^+rt>?46`h3D>C#=!?j70{ktp-kEocke+KCl#9o9%xz`n~PL+XZ{2Wmv5u^6w$J;i?*SvzzKyFEC!qci|9@Hvi@ZaAAAa zul@0XG0OfTL)rYN3uOSF0sQ(Cqw;b@CkG:DEBUG>) diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 4150e62..0355b93 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -11,7 +11,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const // Supply command header to console TaskLogger::PushText( " ~\r\n" - " Patch Maker /\r\n" + " Patch Maker /\r\n" " ~-----------------~\r\n" " /\r\n" "~\r\n\r\n" diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index cc39ed0..6786a1a 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -29,7 +29,7 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const else exit_program( " Arguments Expected:\r\n" - " -src=[path to the directory to compress]\r\n" + " -src=[path to the directory to package]\r\n" " -dst=[path to write the installer] (can omit filename)\r\n" "\r\n" ); diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index 465441e..9d5eb03 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -10,9 +10,9 @@ void PackCommand::execute(const int & argc, char * argv[]) const { // Supply command header to console TaskLogger::PushText( - " ~\r\n" - " Packager /\r\n" - " ~-----------------~\r\n" + " ~\r\n" + " Packager /\r\n" + " ~----------------~\r\n" " /\r\n" "~\r\n\r\n" ); @@ -28,7 +28,7 @@ void PackCommand::execute(const int & argc, char * argv[]) const else exit_program( " Arguments Expected:\r\n" - " -src=[path to the directory to compress]\r\n" + " -src=[path to the directory to package]\r\n" " -dst=[path to write the package] (can omit filename)\r\n" "\r\n" ); diff --git a/src/nSuite/Commands/PackagerCommand.cpp b/src/nSuite/Commands/PackagerCommand.cpp new file mode 100644 index 0000000..f2bbcba --- /dev/null +++ b/src/nSuite/Commands/PackagerCommand.cpp @@ -0,0 +1,77 @@ +#include "PackagerCommand.h" +#include "BufferTools.h" +#include "Common.h" +#include "DirectoryTools.h" +#include "TaskLogger.h" +#include "Resource.h" +#include + + +void PackagerCommand::execute(const int & argc, char * argv[]) const +{ + // Supply command header to console + TaskLogger::PushText( + " ~\r\n" + " Portable Package Maker /\r\n" + " ~------------------------~\r\n" + " /\r\n" + "~\r\n\r\n" + ); + + // Check command line arguments + std::string srcDirectory(""), dstDirectory(""); + for (int x = 2; x < argc; ++x) { + std::string command = string_to_lower(std::string(argv[x], 5)); + if (command == "-src=") + srcDirectory = std::string(&argv[x][5]); + else if (command == "-dst=") + dstDirectory = std::string(&argv[x][5]); + else + exit_program( + " Arguments Expected:\r\n" + " -src=[path to the directory to package]\r\n" + " -dst=[path to write the portable package] (can omit filename)\r\n" + "\r\n" + ); + } + + // If user provides a directory only, append a filename + if (std::filesystem::is_directory(dstDirectory)) + dstDirectory += "\\package.exe"; + + // Ensure a file-extension is chosen + if (!std::filesystem::path(dstDirectory).has_extension()) + dstDirectory += ".exe"; + + // Compress the directory specified + char * packBuffer(nullptr); + size_t packSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) + exit_program("Cannot create installer from the directory specified, aborting...\r\n"); + + // Acquire installer resource + Resource unpacker(IDR_UNPACKER, "UNPACKER"); + if (!unpacker.exists()) + exit_program("Cannot access unpacker resource, aborting...\r\n"); + + // Write installer to disk + std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); + std::ofstream file(dstDirectory, std::ios::binary | std::ios::out); + if (!file.is_open()) + exit_program("Cannot write package to disk, aborting...\r\n"); + file.write(reinterpret_cast(unpacker.getPtr()), (std::streamsize)unpacker.getSize()); + file.close(); + + // Update installer's resource + auto handle = BeginUpdateResource(dstDirectory.c_str(), false); + if (!(bool)UpdateResource(handle, "ARCHIVE", MAKEINTRESOURCE(IDR_ARCHIVE), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), packBuffer, (DWORD)packSize)) + exit_program("Cannot write archive contents to the package, aborting...\r\n"); + EndUpdateResource(handle, FALSE); + delete[] packBuffer; + + // Output results + TaskLogger::PushText( + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(packSize) + "\r\n" + ); +} \ No newline at end of file diff --git a/src/nSuite/Commands/PackagerCommand.h b/src/nSuite/Commands/PackagerCommand.h new file mode 100644 index 0000000..a393ef7 --- /dev/null +++ b/src/nSuite/Commands/PackagerCommand.h @@ -0,0 +1,15 @@ +#pragma once +#ifndef PACKAGERCOMMAND_H +#define PACKAGERCOMMAND_H + +#include "Command.h" + + +/** Command to compress an entire directory into a portable installer. */ +class PackagerCommand : public Command { +public: + // Public interface implementation + virtual void execute(const int & argc, char * argv[]) const override; +}; + +#endif // PACKAGERCOMMAND_H \ No newline at end of file diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 3df08bd..091bb6e 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -11,7 +11,7 @@ void PatchCommand::execute(const int & argc, char * argv[]) const // Supply command header to console TaskLogger::PushText( " ~\r\n" - " Patcher /\r\n" + " Patcher /\r\n" " ~-----------------~\r\n" " /\r\n" "~\r\n\r\n" diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index e3c54ad..5069fff 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -11,7 +11,7 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const // Supply command header to console TaskLogger::PushText( " ~\r\n" - " Unpacker /\r\n" + " Unpacker /\r\n" " ~-----------------~\r\n" " /\r\n" "~\r\n\r\n" diff --git a/src/nSuite/README.md b/src/nSuite/README.md new file mode 100644 index 0000000..5ad7e10 --- /dev/null +++ b/src/nSuite/README.md @@ -0,0 +1,39 @@ +# nSuite +The nSuite program is intended to be used by developers or those who wish to package/diff/distribute one or many files. +It is a command-line application and is run by using one of the following arguments: + +- #### `-installer -src= -dst=` + - Creates a fully-fledged installer (Windows) with all the contents of the source directory + - Writes the installer to the destination path specified + - Generates an uninstaller, and links it up in the user's registry + - Can write a 'manifest.nman' file in the root source directory + - Specify string attributes for the installer, such as name, version, derscriptions, shortcuts + +- #### `-packager -src= -dst=` + - Creates a mini installer with no GUI (terminal only) with all the contents of the source directory + - No uninstaller, registry modifications, or manifest file + +- #### `-pack -src= -dst=` + - Packages and compresses all the contents of the source directory into an .npack file + - Writes package file to the destination path specified + - Requires nSuite to unpackage + - Note: these package files are what is embedded in the installers above + +- #### `-unpack -src= -dst=` + - Decompresses and unpackages the contents held in an .npack file + - Writes package contents to the destination path specified + - Note: this command is executed what is executed in the installers above + +- #### `-diff -old= -new= -dst=` + - Finds all the common, added, and removed files between the old and new directories specified + - Generates patch instructions for all the differences found between all these files (including add/delete file instructions) + - Files are analyzed byte/8byte wise, and is accelerated by multiple threads + - **Instead of directories, can specify .npack files. Usefull if needing to maintain multiple versions on disk.** + - nSuite will virtualize the content within, treating it as a directory for you. + + - #### `-patch -src= -dst=` + - Uses a source .ndiff file, and executes all the patch instructions it contains on the destination directory specified + - Security: + - Patch file has before/after hashes + - File hashes must match, otherwise the application is aborted prior to writing-out to disk + - Strict conditions to prevent against file corruption. \ No newline at end of file diff --git a/src/nSuite/nSuite.cpp b/src/nSuite/nSuite.cpp index 7bc8582..55eed46 100644 --- a/src/nSuite/nSuite.cpp +++ b/src/nSuite/nSuite.cpp @@ -5,6 +5,7 @@ // Command inclusions #include "Commands/Command.h" #include "Commands/InstallerCommand.h" +#include "Commands/PackagerCommand.h" #include "Commands/DiffCommand.h" #include "Commands/PatchCommand.h" #include "Commands/PackCommand.h" @@ -19,6 +20,7 @@ int main(int argc, char *argv[]) struct compare_string { bool operator()(const char * a, const char * b) const { return strcmp(a, b) < 0; } }; const std::map commandMap{ { "-installer" , new InstallerCommand() }, + { "-packager" , new PackagerCommand() }, { "-pack" , new PackCommand() }, { "-unpack" , new UnpackCommand() }, { "-diff" , new DiffCommand() }, @@ -37,11 +39,12 @@ int main(int argc, char *argv[]) " /\r\n" "~\r\n\r\n" " Operations Supported:\r\n" - " -installer (To package and compress an entire directory into an executable file)\r\n" - " -pack (To compress an entire directory into a single .npack file)\r\n" - " -unpack (To decompress an entire directory from a .npack file)\r\n" - " -diff (To diff an entire directory into a single .ndiff file)\r\n" - " -patch (To patch an entire directory from a .ndiff file)\r\n" + " -installer (Packages a directory into a GUI Installer (for Windows))\r\n" + " -packager (Packages a directory into a portable package executable (like a mini installer))\r\n" + " -pack (Packages a directory into an .npack file)\r\n" + " -unpack (Unpackages into a directory from an .npack file)\r\n" + " -diff (Diff. 2 directories into an .ndiff file)\r\n" + " -patch (Patches a directory from an .ndiff file)\r\n" "\r\n\r\n" ); diff --git a/src/nSuite/nSuite.rc b/src/nSuite/nSuite.rc index d258c8c671df1c7a8c32065fbbee14b710085d8c..3db002a22fb6138ac5625c8561386fe0b79cceb7 100644 GIT binary patch literal 3474 zcmchaU27Uq5QgVAh5m=FxoM$@Cb{WlqR~oC6m=zqfS5#6sny^IO`*TN?em`Fx~{Hi zwIM9)*_|`<&iCx`_pd`6*vM{eXmh)>o&}b%2J6(uP$um3ENfq&zP49(=y>{0M!ct1 zx2Cl$w4zn4W*ubDp%!_*w5olE(&YET&Y)Cz_9&Y=zdOu{(;Vyb+;c3HZT-D9Mp}jN zCgH=Ky~W;=T`g>HVrPgK1^npXN0sXxQa`zZt&%M;8dgT)9KS;(q_GP15^Bg=W+zxu zJYSjj3NiU;@7Zg@cea(I!>*5>kSi|U9oB-2blwiQqBlJskg);T8xyqVQjzt_* zd3uia5IZCM2HsS#U->=7t|}Z2r)85SZCS2su4q*i4Y|A2QqS#Fuw`m9C_}e1fwyOm z+}<*IC9NS^>*y@lr-jZ<%D-t0(fEMATlhR(X@^0^4&4SjR}BK{_|{oPmP zdq5Y-TitDP+u>JvZIWyCtj~Fg=U{C|wII4A1D-0bB9hX9<*8-eId^;BI`}nm8Y+(J1Ry`!FK@~btloj3NhkpyIB&t7-Y~ewr{RO zNA`pI>hh*(pm~@fR$ctjytrX^#?Ew$?sW7h0_u|~GeD7f0B7K)Vx;91UHYOaR>5*w zbcvvkqij*%XmSZb{Vfc|SbJTb?iM_KkThp{(8S{>o#zb}azp4d{K@X+OWWa3!O&!n ze8_{QJLm57{G`W7)uG5vSY_>c4o1|}+*z!1!0NP)@YdI4hO5ZjJ7neh0lyW)drlOy zrS+RT0bk{U`|cj$Q=Zz}#i!arhvF`uqB_vqYJL5$Z*}yH$^D$#nmrl0U93-{?w@a+ zM2d@P?Q^tA(Rm~8^EUCBETg5$*mZ6sbKe3@-7zHx>g3g$)fD$rMLm=^P*O}EP+wn?mKF4*02>^ub4>AA% diff --git a/src/nUpdater/nUpdater.rc b/src/nUpdater/nUpdater.rc deleted file mode 100644 index 7330268102d8ba92fefe6700d474db236a2ee5b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1240 zcmchW-Acni5QWdJg6|O18wDv=pTMRy24j_`5rsl7Y>Nf4P$}N{?CLkWX=AAoEGP^6 zb9R2u%zV5zG}W^n^rVGG8Y@t!KIe;OAaj1kYU&YmPrGV3nTgXe*)!cJ(?Gd8N_4FP z-7aW{drztEK{EC$wLwzu-pZ!WS7&qXJXij_^nwU2w#n8WmtM`n{#p(r9T4xi?#kFR z;>^h+B9DSRQr00_Z>*qJbAXM$Vl=wsougqMQ_vo0&KdI)tPn33^G@L8LPz{%$hB)F z3VuC%W;t|J0u8=v|oS31G%c-%RsC=Ak z>EpXe4YfHpp@zgWq`to1Rg`rRoo*S?Z~Zi2$tHQd>nhB1T=n8lQ@>fiuhu^SVcMnn diff --git a/src/resource.h b/src/resource.h index e95eca7..466d698 100644 --- a/src/resource.h +++ b/src/resource.h @@ -6,13 +6,14 @@ #define IDI_ICON1 101 // Used by installerMaker.rc #define IDR_INSTALLER 102 +#define IDR_UNPACKER 103 // Used by uninstaller.rc -#define IDR_ARCHIVE 103 -#define IDR_MANIFEST 104 -#define IDR_UNINSTALLER 105 +#define IDR_ARCHIVE 104 +#define IDR_MANIFEST 105 +#define IDR_UNINSTALLER 106 #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 106 +#define _APS_NEXT_RESOURCE_VALUE 107 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 From 112c96116766e388322cc2ea56060b7a5df7bc14 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 19 Apr 2019 15:54:50 -0500 Subject: [PATCH 28/44] Updated readme files with more documentation Updated readme files with more documentation --- src/Installer/README.md | 21 +++++++++++++++++++++ src/Uninstaller/README.md | 29 +++++++++++++++++++++++++++++ src/Unpacker/README.md | 15 +++++++++++++++ src/Updater/README.md | 12 ++++++++++++ src/nSuite/README.md | 4 +++- 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Installer/README.md create mode 100644 src/Uninstaller/README.md create mode 100644 src/Unpacker/README.md create mode 100644 src/Updater/README.md diff --git a/src/Installer/README.md b/src/Installer/README.md new file mode 100644 index 0000000..c280ab3 --- /dev/null +++ b/src/Installer/README.md @@ -0,0 +1,21 @@ +# Installer +This program is a fully-fledged installation application, made to run on Windows. It generates an uninstaller, and links it up in the user's registry. + +It uses the Windows GDI library for rendering. + +This application has 3 (three) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_ARCHIVE the package to be installed + - IDR_MANIFEST the installer manifest (attributes, strings, instructions) + +This (raw) application is useless on its own, and is intended to be fullfilled by nSuite using the `-installer` command. +The following is how nSuite uses this application: + - writes out a copy of this application to disk + - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource + - tries to find an installer manifest, embedding it into **this application's** IDR_MANIFEST resource +Of course the output program generated by nSuite is intended to be usable by anyone. + +This installer has several screens it displays to the user. It starts off with a welcome screen, and requires that the user accept a EULA in order to proceed. +If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). + +The uninstaller may reuse several of the manifest strings provided by its proceeding installer, such as name, version, and where shortcuts may be located (to remove them). \ No newline at end of file diff --git a/src/Uninstaller/README.md b/src/Uninstaller/README.md new file mode 100644 index 0000000..0936cea --- /dev/null +++ b/src/Uninstaller/README.md @@ -0,0 +1,29 @@ +# Uninstaller +This program is a fully-fledged uninstallation application, made to run on Windows. It is generated by the installer application, and removes itself from the user's registry on completion. + +It uses the Windows GDI library for rendering. + +On its own, this application is useless, though it can be mostly tested stand-alone if compiled in debug mode. + +This application has 2 (two) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_MANIFEST the installer manifest (attributes, strings, instructions) + +This (raw) application is useless on its own, and is intended to be fullfilled by the Installer application. +The following is how the Installer uses this application: + - writes out a copy of this application to disk + - embedds its own manifest into **this application's** IDR_MANIFEST resource (to facilitate uninstallation) +Of course the output program generated by the installer is intended to be usable by anyone. + +This uninstaller has several screens it displays to the user. +If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). + +The installer manifest has optional strings the developer can implement to customize the installation process. *Quotes are required* + - name "string" + - version "string" + - description "string" + - eula "string" + - shortcut "\\relative path within installation directory" + - icon "\\relative path within installation directory" + +Any and all of these manifest values can be omitted. \ No newline at end of file diff --git a/src/Unpacker/README.md b/src/Unpacker/README.md new file mode 100644 index 0000000..16b2832 --- /dev/null +++ b/src/Unpacker/README.md @@ -0,0 +1,15 @@ +# Unpacker +This program is a mini portable installation application. It doesn't modify the registry, nor generates any further applications. +Its goal is to be a quick means of dumping some package into the directory it runs from. + +It doesn't use any fancy rendering library, it instead runs in a terminal and requires no user input. + +This application has 2 (two) resources embedded within it: + - IDI_ICON1 the application icon + - IDR_ARCHIVE the package to be installed + +This (raw) application is useless on its own, and is intended to be fullfilled by nSuite using the `-packager` command. +The following is how nSuite uses this application: + - writes out a copy of this application to disk + - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource +Of course the output program generated by nSuite is intended to be usable by anyone. \ No newline at end of file diff --git a/src/Updater/README.md b/src/Updater/README.md new file mode 100644 index 0000000..4020ec4 --- /dev/null +++ b/src/Updater/README.md @@ -0,0 +1,12 @@ +# Updater +This program is a naiive upadater application. It consumes .ndiff files found next to it, and applies them automatically if it can. +Its goal is to be a quick means of updating a directory for a user. It attempts to provide reasonable security against corruption by comparing hashes before/after patching, and aborts before any actual changes are made to disk. + +.ndiff files are generated by nSuite using the `-diff` command. + +This program essentially encapsulates nSuite's `-patch` command. + +It doesn't use any fancy rendering library, it instead runs in a terminal and requires minimal user input. + +This application has 1 (one) resource embedded within it: + - IDI_ICON1 the application icon \ No newline at end of file diff --git a/src/nSuite/README.md b/src/nSuite/README.md index 5ad7e10..cd15ce3 100644 --- a/src/nSuite/README.md +++ b/src/nSuite/README.md @@ -17,7 +17,9 @@ It is a command-line application and is run by using one of the following argume - Packages and compresses all the contents of the source directory into an .npack file - Writes package file to the destination path specified - Requires nSuite to unpackage - - Note: these package files are what is embedded in the installers above + - Note: + - these package files are what is embedded in the installers above + - can be used in diffing as a substitution for a source 'old/new' directory - #### `-unpack -src= -dst=` - Decompresses and unpackages the contents held in an .npack file From 1de5b800236faee87559c37cb9cbf69bd6e5d5e8 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 19 Apr 2019 15:58:04 -0500 Subject: [PATCH 29/44] Fixed readme's Fixed readme's --- src/Installer/README.md | 3 +-- src/Uninstaller/README.md | 3 +-- src/Unpacker/README.md | 5 ++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Installer/README.md b/src/Installer/README.md index c280ab3..fa79767 100644 --- a/src/Installer/README.md +++ b/src/Installer/README.md @@ -8,12 +8,11 @@ This application has 3 (three) resources embedded within it: - IDR_ARCHIVE the package to be installed - IDR_MANIFEST the installer manifest (attributes, strings, instructions) -This (raw) application is useless on its own, and is intended to be fullfilled by nSuite using the `-installer` command. +The raw version of this application is useless on its own, and is intended to be fullfilled by nSuite using the `-installer` command. The following is how nSuite uses this application: - writes out a copy of this application to disk - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource - tries to find an installer manifest, embedding it into **this application's** IDR_MANIFEST resource -Of course the output program generated by nSuite is intended to be usable by anyone. This installer has several screens it displays to the user. It starts off with a welcome screen, and requires that the user accept a EULA in order to proceed. If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). diff --git a/src/Uninstaller/README.md b/src/Uninstaller/README.md index 0936cea..dfd29d5 100644 --- a/src/Uninstaller/README.md +++ b/src/Uninstaller/README.md @@ -9,11 +9,10 @@ This application has 2 (two) resources embedded within it: - IDI_ICON1 the application icon - IDR_MANIFEST the installer manifest (attributes, strings, instructions) -This (raw) application is useless on its own, and is intended to be fullfilled by the Installer application. +The raw version of this application is useless on its own, and is intended to be fullfilled by the Installer application. The following is how the Installer uses this application: - writes out a copy of this application to disk - embedds its own manifest into **this application's** IDR_MANIFEST resource (to facilitate uninstallation) -Of course the output program generated by the installer is intended to be usable by anyone. This uninstaller has several screens it displays to the user. If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). diff --git a/src/Unpacker/README.md b/src/Unpacker/README.md index 16b2832..d6f9284 100644 --- a/src/Unpacker/README.md +++ b/src/Unpacker/README.md @@ -8,8 +8,7 @@ This application has 2 (two) resources embedded within it: - IDI_ICON1 the application icon - IDR_ARCHIVE the package to be installed -This (raw) application is useless on its own, and is intended to be fullfilled by nSuite using the `-packager` command. +The raw version of this application is useless on its own, and is intended to be fullfilled by nSuite using the `-packager` command. The following is how nSuite uses this application: - writes out a copy of this application to disk - - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource -Of course the output program generated by nSuite is intended to be usable by anyone. \ No newline at end of file + - packages a directory, embedding the package resource into **this application's** IDR_ARCHIVE resource \ No newline at end of file From 197c02d9ec6abc9a0cf5c6258805633e293423e6 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Fri, 19 Apr 2019 16:02:44 -0500 Subject: [PATCH 30/44] Even more readme changes Even more readme changes --- src/Installer/README.md | 10 +++++++++- src/Uninstaller/README.md | 12 +----------- src/Updater/README.md | 7 ++++++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Installer/README.md b/src/Installer/README.md index fa79767..3d28054 100644 --- a/src/Installer/README.md +++ b/src/Installer/README.md @@ -17,4 +17,12 @@ The following is how nSuite uses this application: This installer has several screens it displays to the user. It starts off with a welcome screen, and requires that the user accept a EULA in order to proceed. If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). -The uninstaller may reuse several of the manifest strings provided by its proceeding installer, such as name, version, and where shortcuts may be located (to remove them). \ No newline at end of file +The installer manifest has optional strings the developer can implement to customize the installation process. *Quotes are required* + - name "string" + - version "string" + - description "string" + - eula "string" + - shortcut "\\relative path within installation directory" + - icon "\\relative path within installation directory" + +Any and all of these manifest values can be omitted. \ No newline at end of file diff --git a/src/Uninstaller/README.md b/src/Uninstaller/README.md index dfd29d5..ab582e1 100644 --- a/src/Uninstaller/README.md +++ b/src/Uninstaller/README.md @@ -3,8 +3,6 @@ This program is a fully-fledged uninstallation application, made to run on Windo It uses the Windows GDI library for rendering. -On its own, this application is useless, though it can be mostly tested stand-alone if compiled in debug mode. - This application has 2 (two) resources embedded within it: - IDI_ICON1 the application icon - IDR_MANIFEST the installer manifest (attributes, strings, instructions) @@ -17,12 +15,4 @@ The following is how the Installer uses this application: This uninstaller has several screens it displays to the user. If at any point an error occurs, the program enters a failure state and dumps its entire operation log to disk (next to the program, error_log.txt). -The installer manifest has optional strings the developer can implement to customize the installation process. *Quotes are required* - - name "string" - - version "string" - - description "string" - - eula "string" - - shortcut "\\relative path within installation directory" - - icon "\\relative path within installation directory" - -Any and all of these manifest values can be omitted. \ No newline at end of file +The uninstaller may reuse several of the manifest strings provided by its proceeding installer, such as name, version, and where shortcuts may be located (to remove them). \ No newline at end of file diff --git a/src/Updater/README.md b/src/Updater/README.md index 4020ec4..f871132 100644 --- a/src/Updater/README.md +++ b/src/Updater/README.md @@ -1,7 +1,12 @@ # Updater This program is a naiive upadater application. It consumes .ndiff files found next to it, and applies them automatically if it can. -Its goal is to be a quick means of updating a directory for a user. It attempts to provide reasonable security against corruption by comparing hashes before/after patching, and aborts before any actual changes are made to disk. +Its goal is to be a quick means of updating a directory for a user. + +It attempts to provide reasonable security against corruption by comparing hashes before/after patching, and aborts before any actual changes are made to disk. + + +## Notes: .ndiff files are generated by nSuite using the `-diff` command. This program essentially encapsulates nSuite's `-patch` command. From 871d734e1dbc0853749b29c78a6d52be8bd72a5a Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 20 Apr 2019 13:38:38 -0500 Subject: [PATCH 31/44] Tidyied up compression logic - BFT used to generate huge 'compressed' buffers, because a large destination buffer was needed to do compression ops. -> it now creates a temporary buffer 2x the size, but memcopy's it to a small destination buffer. - Improved BFT documentation + comments to make it clear that there is a header on compressed buffers - Improved DFT directory decompression, it now properly does the reverse of its compression function. Explanation: the decomp function can now take in the exact same package that the comp function produces, whereas in the previous version the function callers had to parse the package header for the directory name + size, and offset it. - Improved DFT documentation + comments - DFT decompression now appends the package name to the destionation folder it receives, so all 3 utility programs produced in this project can now easily forget about parsing the header for package name info (except installer) - installer now shows package name next to the directory bar, rather than appending it into the end of it. Our DFT library effectively enforces this anyways now. --- src/BufferTools.cpp | 37 ++++---- src/BufferTools.h | 10 +- src/DirectoryTools.cpp | 29 ++++-- src/DirectoryTools.h | 3 + src/Installer/Installer.cpp | 26 +++--- .../Installer.dir/Release/Installer.res | Bin 225008 -> 0 bytes src/Installer/Installer.h | 5 +- src/Installer/Screens/Directory.cpp | 23 +++-- src/Installer/Screens/Directory.h | 2 +- src/Installer/Screens/Finish.cpp | 5 +- .../Uninstaller.dir/Release/Uninstaller.res | Bin 33952 -> 0 bytes src/Unpacker/Unpacker.cpp | 8 +- .../Unpacker.dir/Release/Unpacker.res | Bin 33920 -> 0 bytes src/Unpacker/Unpacker.h | 88 ------------------ src/Updater/Updater.dir/Release/Updater.res | Bin 22424 -> 0 bytes src/nSuite/Commands/UnpackCommand.cpp | 10 +- 16 files changed, 89 insertions(+), 157 deletions(-) delete mode 100644 src/Installer/Installer.dir/Release/Installer.res delete mode 100644 src/Uninstaller/Uninstaller.dir/Release/Uninstaller.res delete mode 100644 src/Unpacker/Unpacker.dir/Release/Unpacker.res delete mode 100644 src/Unpacker/Unpacker.h delete mode 100644 src/Updater/Updater.dir/Release/Updater.res diff --git a/src/BufferTools.cpp b/src/BufferTools.cpp index e01e64c..e0759fb 100644 --- a/src/BufferTools.cpp +++ b/src/BufferTools.cpp @@ -6,30 +6,33 @@ #include -bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize, const size_t & headerPadding) +bool BFT::CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize) { - // Allocate enough room for the compressed buffer (2 size_t bigger than source buffer) - destinationSize = (sourceSize * 2ull) + size_t(sizeof(size_t)) + headerPadding; - *destinationBuffer = new char[destinationSize]; - - // Offset pointer by the amount requested for header-space - char * pointer = reinterpret_cast(PTR_ADD(*destinationBuffer, headerPadding)); - - // First chunk of data = the total uncompressed size - *reinterpret_cast(pointer) = sourceSize; - - // Increment pointer so that the compression works on the remaining part of the buffer - pointer = reinterpret_cast(pointer) + size_t(sizeof(size_t)); + // Pre-allocate a huge buffer to allow for compression OPS + char * compressedBuffer = new char[sourceSize * 2ull]; - // Compress the buffer + // Try to compress the source buffer auto result = LZ4_compress_default( sourceBuffer, - pointer, + compressedBuffer, int(sourceSize), - int(destinationSize - size_t(sizeof(size_t)) - headerPadding) + int(sourceSize * 2ull) ); - destinationSize = headerPadding + size_t(sizeof(size_t)) + size_t(result); + // Create the final buffer (done separate b/c we now know the final reduced buffer size) + constexpr size_t HEADER_SIZE = size_t(sizeof(size_t)); + destinationSize = HEADER_SIZE + size_t(result); + *destinationBuffer = new char[destinationSize]; + char * HEADER_ADDRESS = *destinationBuffer; + char * DATA_ADDRESS = HEADER_ADDRESS + HEADER_SIZE; + + // Header = the total uncompressed size + *reinterpret_cast(HEADER_ADDRESS) = sourceSize; + + // Data = compressed source buffer + std::memcpy(DATA_ADDRESS, compressedBuffer, size_t(result)); + delete[] compressedBuffer; + return (result > 0); } diff --git a/src/BufferTools.h b/src/BufferTools.h index cb3b8c5..b6f07d5 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -6,13 +6,19 @@ /** Namespace to keep buffer-related operations grouped together. */ namespace BFT { /** Compresses a source buffer into an equal or smaller sized destination buffer. + --------------- + | buffer data | + --------------- + v + ----------------------------------------- + | compression header | compressed data | + ----------------------------------------- @param sourceBuffer the original buffer to read from. @param sourceSize the size in bytes of the source buffer. @param destinationBuffer pointer to the destination buffer, which will hold compressed contents. @param destinationSize reference updated with the size in bytes of the compressed destinationBuffer. - @param headerPadding amount of headroom to allocate for a header on top of the final compressed buffer. (optional, defaults to 0) @return true if compression success, false otherwise. */ - bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize, const size_t & headerPadding = 0ull); + bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); /** Decompressess a source buffer into an equal or larger sized destination buffer. @param sourceBuffer the original buffer to read from. @param sourceSize the size in bytes of the source buffer. diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 24098db..1d69c7c 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -108,22 +108,27 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer continue; threader.shutdown(); - // Compress the archive, pad the beginning with header-space - if (!BFT::CompressBuffer(filebuffer, archiveSize, packBuffer, packSize, sizeof(size_t) + folderName.size())) { + // Compress the archive + char * compBuffer(nullptr); + size_t compSize(0ull); + if (!BFT::CompressBuffer(filebuffer, archiveSize, &compBuffer, compSize)) { TaskLogger::PushText("Critical failure: cannot perform compression operation on the set of joined files.\r\n"); return false; } delete[] filebuffer; - // Write root folder name and size + // Move compressed buffer data to a new buffer that has a special header + packSize = compSize + sizeof(size_t) + folderName.size(); + *packBuffer = new char[packSize]; + memcpy(&(*packBuffer)[sizeof(size_t) + folderName.size()], compBuffer, compSize); + delete[] compBuffer; + + // Write the header (root folder name and size) pointer = *packBuffer; auto pathSize = folderName.size(); memcpy(pointer, reinterpret_cast(&pathSize), size_t(sizeof(size_t))); pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); memcpy(pointer, folderName.data(), pathSize); - pointer = PTR_ADD(pointer, pathSize); - - // Clean up return true; } @@ -135,9 +140,17 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe return false; } + // Read the directory header + char * packBufferOffset = packBuffer; + const auto folderSize = *reinterpret_cast(packBufferOffset); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + const char * folderArray = reinterpret_cast(packBufferOffset); + const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + char * decompressedBuffer(nullptr); size_t decompressedSize(0ull); - if (!BFT::DecompressBuffer(packBuffer, packSize, &decompressedBuffer, decompressedSize)) { + if (!BFT::DecompressBuffer(packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), &decompressedBuffer, decompressedSize)) { TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); return false; } @@ -154,7 +167,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe // Read the file path string, from the archive const char * path_array = reinterpret_cast(readingPtr); - std::string fullPath = dstDirectory + std::string(path_array, pathSize); + std::string fullPath = finalDestionation + std::string(path_array, pathSize); readingPtr = PTR_ADD(readingPtr, pathSize); // Read the file size in bytes, from the archive diff --git a/src/DirectoryTools.h b/src/DirectoryTools.h index 3c3c776..4226aa7 100644 --- a/src/DirectoryTools.h +++ b/src/DirectoryTools.h @@ -9,6 +9,9 @@ /** Namespace to keep directory-related operations grouped together. */ namespace DRT { /** Compresses a source directory into an equal or smaller sized destination buffer. + ------------------------------------------------------ + | Directory name header | compressed directory data | + ------------------------------------------------------ @note caller is responsible for cleaning-up packBuffer. @param srcDirectory the absolute path to the directory to compress. @param packBuffer pointer to the destination buffer, which will hold compressed contents. diff --git a/src/Installer/Installer.cpp b/src/Installer/Installer.cpp index c05570b..6691dac 100644 --- a/src/Installer/Installer.cpp +++ b/src/Installer/Installer.cpp @@ -76,11 +76,14 @@ Installer::Installer(const HINSTANCE hInstance) : Installer() success = false; } else { - const auto folderSize = *reinterpret_cast(m_archive.getPtr()); - m_packageName = std::string(reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)))), folderSize); - m_packagePtr = reinterpret_cast(PTR_ADD(m_archive.getPtr(), size_t(sizeof(size_t)) + folderSize)); - m_packageSize = m_archive.getSize() - (size_t(sizeof(size_t)) + folderSize); - m_maxSize = *reinterpret_cast(m_packagePtr); + // Read directory header data + void * pointer = m_archive.getPtr(); + const auto folderSize = *reinterpret_cast(pointer); + pointer = PTR_ADD(pointer, size_t(sizeof(size_t))); + m_packageName = std::string(reinterpret_cast(pointer), folderSize); + pointer = PTR_ADD(pointer, folderSize); + // Read compressed package header + m_maxSize = *reinterpret_cast(reinterpret_cast(pointer)); // If no name is found, use the package name (if available) if (m_mfStrings[L"name"].empty() && !m_packageName.empty()) @@ -209,23 +212,24 @@ void Installer::beginInstallation() size_t byteCount(0ull), fileCount(0ull); auto directory = getDirectory(); sanitize_path(directory); - if (!DRT::DecompressDirectory(directory, m_packagePtr, m_packageSize, byteCount, fileCount)) + if (!DRT::DecompressDirectory(directory, reinterpret_cast(m_archive.getPtr()), m_archive.getSize(), byteCount, fileCount)) invalidate(); else { // Write uninstaller to disk - const auto uninstallerPath = directory + "\\uninstaller.exe"; + const auto fullDirectory = directory + "\\" + m_packageName; + const auto uninstallerPath = fullDirectory + "\\uninstaller.exe"; std::filesystem::create_directories(std::filesystem::path(uninstallerPath).parent_path()); std::ofstream file(uninstallerPath, std::ios::binary | std::ios::out); if (!file.is_open()) { TaskLogger::PushText("Cannot write uninstaller to disk, aborting...\r\n"); invalidate(); } - TaskLogger::PushText("Uninstaller:\"" + uninstallerPath + "\"\r\n"); + TaskLogger::PushText("Uninstaller: \"" + uninstallerPath + "\"\r\n"); file.write(reinterpret_cast(uninstaller.getPtr()), (std::streamsize)uninstaller.getSize()); file.close(); // Update uninstaller's resources - std::string newDir = std::regex_replace(directory, std::regex("\\\\"), "\\\\"); + std::string newDir = std::regex_replace(fullDirectory, std::regex("\\\\"), "\\\\"); const std::string newManifest( std::string(reinterpret_cast(manifest.getPtr()), manifest.getSize()) + "\r\ndirectory \"" + newDir + "\"" @@ -249,12 +253,12 @@ void Installer::beginInstallation() if (icon.empty()) icon = uninstallerPath; else - icon = directory + icon; + icon = fullDirectory + icon; RegSetKeyValueA(hkey, 0, "UninstallString", REG_SZ, (LPCVOID)uninstallerPath.c_str(), (DWORD)uninstallerPath.size()); RegSetKeyValueA(hkey, 0, "DisplayIcon", REG_SZ, (LPCVOID)icon.c_str(), (DWORD)icon.size()); RegSetKeyValueA(hkey, 0, "DisplayName", REG_SZ, (LPCVOID)name.c_str(), (DWORD)name.size()); RegSetKeyValueA(hkey, 0, "DisplayVersion", REG_SZ, (LPCVOID)version.c_str(), (DWORD)version.size()); - RegSetKeyValueA(hkey, 0, "InstallLocation", REG_SZ, (LPCVOID)directory.c_str(), (DWORD)directory.size()); + RegSetKeyValueA(hkey, 0, "InstallLocation", REG_SZ, (LPCVOID)fullDirectory.c_str(), (DWORD)fullDirectory.size()); RegSetKeyValueA(hkey, 0, "Publisher", REG_SZ, (LPCVOID)publisher.c_str(), (DWORD)publisher.size()); RegSetKeyValueA(hkey, 0, "NoModify", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); RegSetKeyValueA(hkey, 0, "NoRepair", REG_DWORD, (LPCVOID)&ONE, (DWORD)sizeof(DWORD)); diff --git a/src/Installer/Installer.dir/Release/Installer.res b/src/Installer/Installer.dir/Release/Installer.res deleted file mode 100644 index b4757323e20831e2a98b88c57c46f5e48b767899..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 225008 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 diff --git a/src/Installer/Installer.h b/src/Installer/Installer.h index a36f1fb..cabc8b2 100644 --- a/src/Installer/Installer.h +++ b/src/Installer/Installer.h @@ -76,9 +76,8 @@ class Installer { Threader m_threader; Resource m_archive, m_manifest; std::string m_directory = "", m_packageName = ""; - bool m_valid = true; - char * m_packagePtr = nullptr; - size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; + bool m_valid = true; + size_t m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; ScreenEnums m_currentIndex = WELCOME_SCREEN; Screen * m_screens[SCREEN_COUNT]; HWND m_hwnd = nullptr; diff --git a/src/Installer/Screens/Directory.cpp b/src/Installer/Screens/Directory.cpp index 21f07c2..701f8a4 100644 --- a/src/Installer/Screens/Directory.cpp +++ b/src/Installer/Screens/Directory.cpp @@ -27,6 +27,7 @@ Directory::~Directory() UnregisterClass("DIRECTORY_SCREEN", m_hinstance); DestroyWindow(m_hwnd); DestroyWindow(m_directoryField); + DestroyWindow(m_packageField); DestroyWindow(m_browseButton); DestroyWindow(m_btnPrev); DestroyWindow(m_btnInst); @@ -56,8 +57,9 @@ Directory::Directory(Installer * installer, const HINSTANCE hInstance, const HWN setVisible(false); // Create directory lookup fields - m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", directory_and_package(m_installer->getDirectory(), m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 490, 25, m_hwnd, NULL, hInstance, NULL); - m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 510, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); + m_directoryField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", m_installer->getDirectory().c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_AUTOHSCROLL, 10, 150, 400, 25, m_hwnd, NULL, hInstance, NULL); + m_packageField = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", ("\\" + m_installer->getPackageName()).c_str(), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_READONLY, 410, 150, 100, 25, m_hwnd, NULL, hInstance, NULL); + m_browseButton = CreateWindow("BUTTON", "Browse", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 520, 149, 100, 25, m_hwnd, NULL, hInstance, NULL); // Create Buttons constexpr auto BUTTON_STYLES = WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON; @@ -126,7 +128,6 @@ void Directory::browse() std::string directory(""); if (SUCCEEDED(OpenFileDialog(directory))) { if (directory != "" && directory.length() > 2ull) { - directory = directory_and_package(directory, m_installer->getPackageName()); m_installer->setDirectory(directory); SetWindowTextA(m_directoryField, directory.c_str()); RECT rc = { 10, 200, 600, 300 }; @@ -287,13 +288,15 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM l ptr->goCancel(); } else if (notification == EN_CHANGE) { - // Redraw 'disk space data' region of window when the text field changes - std::vector data(GetWindowTextLength(controlHandle) + 1ull); - GetWindowTextA(controlHandle, &data[0], (int)data.size()); - ptr->m_installer->setDirectory(std::string(data.data())); - RECT rc = { 10, 200, 600, 300 }; - RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); - return S_OK; + if (controlHandle == ptr->m_directoryField) { + // Redraw 'disk space data' region of window when the text field changes + std::vector data(GetWindowTextLength(controlHandle) + 1ull); + GetWindowTextA(controlHandle, &data[0], (int)data.size()); + ptr->m_installer->setDirectory(std::string(data.data())); + RECT rc = { 10, 200, 600, 300 }; + RedrawWindow(hWnd, &rc, NULL, RDW_INVALIDATE); + return S_OK; + } } } return DefWindowProc(hWnd, message, wParam, lParam); diff --git a/src/Installer/Screens/Directory.h b/src/Installer/Screens/Directory.h index 56c1c1c..4683357 100644 --- a/src/Installer/Screens/Directory.h +++ b/src/Installer/Screens/Directory.h @@ -30,7 +30,7 @@ class Directory : public Screen { // Public Attributes - HWND m_directoryField = nullptr, m_browseButton = nullptr, m_btnPrev = nullptr, m_btnInst = nullptr, m_btnCancel = nullptr; + HWND m_directoryField = nullptr, m_packageField = nullptr, m_browseButton = nullptr, m_btnPrev = nullptr, m_btnInst = nullptr, m_btnCancel = nullptr; }; #endif // DIRECTORY_H \ No newline at end of file diff --git a/src/Installer/Screens/Finish.cpp b/src/Installer/Screens/Finish.cpp index 408a327..8a580a9 100644 --- a/src/Installer/Screens/Finish.cpp +++ b/src/Installer/Screens/Finish.cpp @@ -138,9 +138,10 @@ void Finish::paint() void Finish::goClose() { m_showDirectory = IsDlgButtonChecked(m_hwnd, 1); + const auto instDir = m_installer->getDirectory() + "\\" + m_installer->getPackageName(); // Open the installation directory + if user wants it to if (m_showDirectory) - ShellExecute(NULL, "open", m_installer->getDirectory().c_str(), NULL, NULL, SW_SHOWDEFAULT); + ShellExecute(NULL, "open", instDir.c_str(), NULL, NULL, SW_SHOWDEFAULT); // Create Shortcuts int x = 2; @@ -148,7 +149,6 @@ void Finish::goClose() if (IsDlgButtonChecked(m_hwnd, x)) { std::error_code ec; const auto nonwideShortcut = from_wideString(shortcut); - auto instDir = m_installer->getDirectory(); auto srcPath = instDir; if (srcPath.back() == '\\') srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); @@ -162,7 +162,6 @@ void Finish::goClose() if (IsDlgButtonChecked(m_hwnd, x)) { std::error_code ec; const auto nonwideShortcut = from_wideString(shortcut); - auto instDir = m_installer->getDirectory(); auto srcPath = instDir; if (srcPath.back() == '\\') srcPath = std::string(&srcPath[0], srcPath.size() - 1ull); diff --git a/src/Uninstaller/Uninstaller.dir/Release/Uninstaller.res b/src/Uninstaller/Uninstaller.dir/Release/Uninstaller.res deleted file mode 100644 index c44a914bbbafed2ff2d3e305b0a15e69535e3024..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33952 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.getPtr()); const auto folderSize = *reinterpret_cast(packBufferOffset); packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); - const char * folderArray = reinterpret_cast(packBufferOffset); - const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); - packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + const auto folderName = std::string(reinterpret_cast(packBufferOffset), folderSize); // Report an overview of supplied procedure std::cout << "Unpacking to the following directory:\r\n" - "\t> " + finalDestionation + + "\t> " + dstDirectory + "\\" + folderName + "\r\n"; // Unpackage using the resource file - if (!DRT::DecompressDirectory(finalDestionation, packBufferOffset, archive.getSize() - (size_t(sizeof(size_t)) + folderSize), byteCount, fileCount)) + if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) exit_program("Cannot decompress embedded package resource, aborting...\r\n"); // Success, report results diff --git a/src/Unpacker/Unpacker.dir/Release/Unpacker.res b/src/Unpacker/Unpacker.dir/Release/Unpacker.res deleted file mode 100644 index 31ba97344e871787c35f5195400dfe13c875830b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33920 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 Gko`ZR-^O$R diff --git a/src/Unpacker/Unpacker.h b/src/Unpacker/Unpacker.h deleted file mode 100644 index a36f1fb..0000000 --- a/src/Unpacker/Unpacker.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once -#ifndef INSTALLER_H -#define INSTALLER_H - -#include "Resource.h" -#include "Threader.h" -#include -#include -#include - - -class Screen; - -/** Encapsulates the logical features of the installer. */ -class Installer { -public: - // Public (de)Constructors - ~Installer() = default; - Installer(const HINSTANCE hInstance); - - - // Public Enumerations - const enum ScreenEnums { - WELCOME_SCREEN, AGREEMENT_SCREEN, DIRECTORY_SCREEN, INSTALL_SCREEN, FINISH_SCREEN, FAIL_SCREEN, - SCREEN_COUNT - }; - - - // Public Methods - /** When called, invalidates the installer, halting it from progressing. */ - void invalidate(); - /** Make the screen identified by the supplied enum as active, deactivating the previous screen. - @param screenIndex the new screen to use. */ - void setScreen(const ScreenEnums & screenIndex); - /** Retrieves the current directory chosen for installation. - @return active installation directory. */ - std::string getDirectory() const; - /** Sets a new installation directory. - @param directory new installation directory. */ - void setDirectory(const std::string & directory); - /** Retrieves the size of the drive used in the current directory. - @return the drive capacity. */ - size_t getDirectorySizeCapacity() const; - /** Retrieves the remaining size of the drive used in the current directory. - @return the available size. */ - size_t getDirectorySizeAvailable() const; - /** Retrieves the required size of the uncompressed package to be installed. - @return the (uncompressed) package size. */ - size_t getDirectorySizeRequired() const; - /** Retrieves the package name. - @return the package name. */ - std::string getPackageName() const; - /** Install the installer's package contents to the directory previously chosen. */ - void beginInstallation(); - /** Dumps error log to disk. */ - static void dumpErrorLog(); - /** Render this window. */ - void paint(); - - - // Public manifest strings - struct compare_string { - bool operator()(const wchar_t * a, const wchar_t * b) const { - return wcscmp(a, b) < 0; - } - }; - std::map m_mfStrings; - - -private: - // Private Constructors - Installer(); - - - // Private Attributes - Threader m_threader; - Resource m_archive, m_manifest; - std::string m_directory = "", m_packageName = ""; - bool m_valid = true; - char * m_packagePtr = nullptr; - size_t m_packageSize = 0ull, m_maxSize = 0ull, m_capacity = 0ull, m_available = 0ull; - ScreenEnums m_currentIndex = WELCOME_SCREEN; - Screen * m_screens[SCREEN_COUNT]; - HWND m_hwnd = nullptr; -}; - - -#endif // INSTALLER_H \ No newline at end of file diff --git a/src/Updater/Updater.dir/Release/Updater.res b/src/Updater/Updater.dir/Release/Updater.res deleted file mode 100644 index 1efe3ba99c715fa92569880a1acc0aa7cdaaae94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22424 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 diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index 5069fff..91b9613 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -45,17 +45,9 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const packFile.read(packBuffer, std::streamsize(packSize)); packFile.close(); - // Read the package header - char * packBufferOffset = packBuffer; - const auto folderSize = *reinterpret_cast(packBufferOffset); - packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); - const char * folderArray = reinterpret_cast(packBufferOffset); - const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); - packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); - // Unpackage using the resource file size_t fileCount(0ull), byteCount(0ull); - if (!DRT::DecompressDirectory(finalDestionation, packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), byteCount, fileCount)) + if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, byteCount, fileCount)) exit_program("Cannot decompress package file, aborting...\r\n"); delete[] packBuffer; From 2700cb1b09b5e6712bb11a673534282ecf42646e Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 20 Apr 2019 15:28:05 -0500 Subject: [PATCH 32/44] Update .gitignore --- .gitignore | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d49fcf1..305b6fa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,11 @@ x64/ x32/ Documentation/ .vs/ -src/nStaller/nStaller.dir/ -src/nUpdater/nUpdater.dir/ +src/Installer/Installer.dir/ src/nSuite/nSuite.dir/ +src/Uninstaller/Uninstaller.dir/ +src/Unpacker/Unpacker.dir/ +src/Updater/Updater.dir/ app/ # Other From 604d633fc9d9bc6199874c884a237b29fe74b846 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sat, 20 Apr 2019 15:32:42 -0500 Subject: [PATCH 33/44] Update README.md fixed typo --- src/Updater/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Updater/README.md b/src/Updater/README.md index f871132..6eb6083 100644 --- a/src/Updater/README.md +++ b/src/Updater/README.md @@ -1,5 +1,5 @@ # Updater -This program is a naiive upadater application. It consumes .ndiff files found next to it, and applies them automatically if it can. +This program is a naive upadater application. It consumes .ndiff files found next to it, and applies them automatically if it can. Its goal is to be a quick means of updating a directory for a user. @@ -14,4 +14,4 @@ This program essentially encapsulates nSuite's `-patch` command. It doesn't use any fancy rendering library, it instead runs in a terminal and requires minimal user input. This application has 1 (one) resource embedded within it: - - IDI_ICON1 the application icon \ No newline at end of file + - IDI_ICON1 the application icon From 85d7fbd0912ca81dbf52332f4b2de1849fac9895 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sun, 21 Apr 2019 12:20:22 -0500 Subject: [PATCH 34/44] Create library readme Haven't finished it yet, but wanted to save for now --- src/README.md | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..7d84944 --- /dev/null +++ b/src/README.md @@ -0,0 +1,198 @@ +# Library +The core of this library can be found between 2 namespaces: +- BFT (BufferTools) +- DFT (DirectoryTools) + +## BufferTools +Buffer Tools exposes 5 useful functions: +- [CompressBuffer](#CompressBuffer) +- [DecompressBuffer](#DecompressBuffer) +- [DiffBuffers](#DiffBuffers) +- [PatchBuffer](#PatchBuffer) +- [HashBuffer](#HashBuffer) + +## DirectoryTools +Directory Tools exposes 4 similar functions, serving more as macros, applying to an entire directory: +- [CompressDirectory](#CompressDirectory) +- [DecompressDirectory](#DecompressDirectory) +- [DiffDirectory](#DiffDirectory) +- [PatchDirectory](#PatchDirectory) + +### CompressBuffer +Compresses a source buffer into an equal or smaller-sized destination buffer. +After compression, it applies a small header describing how large the uncompressed buffer is. +```c++ +bool CompressBuffer( + char * sourceBuffer, + const size_t & sourceSize, + char ** destinationBuffer, + size_t & destinationSize +); +``` +Parameter: `sourceBuffer` the original buffer to read from. +Parameter: `sourceSize` the size of the source buffer in bytes. +Parameter: `destinationBuffer` pointer to the destination buffer, which will hold compressed contents. +Parameter: `destinationSize` reference updated with the size in bytes of the compressed destinationBuffer. +Return: true if compression success, false otherwise. + +Example Usage: +```c++ +// Create a buffer, fill it with data +char * buffer = new char[100]; + +// Compress +char * compressedBuffer(nullptr); +size_t compressedSize(0); +bool result = BFT::CompressBuffer(buffer, 100, &compressedBuffer, compressedSize); +delete[] buffer; +if (result) { + // Do something with compressedBuffer (size of compressedSize) + delete[] compressedBuffer; +} +``` + +### DecompressBuffer +Decompressess a source buffer into an equal or larger-sized destination buffer. +Prior to decompression, it reads from a small header describing how large of a buffer it needs to allocate. +```c++ +bool CompressBuffer( + char * sourceBuffer, + const size_t & sourceSize, + char ** destinationBuffer, + size_t & destinationSize +); +``` +Parameter: `sourceBuffer` the original buffer to read from. +Parameter: `sourceSize` the size of the source buffer in bytes. +Parameter: `destinationBuffer` pointer to the destination buffer, which will hold decompressed contents. +Parameter: `destinationSize` reference updated with the size in bytes of the decompressed destinationBuffer. +Return: true if decompression success, false otherwise. + +Example Usage: +```c++ +// Create a buffer, fill it with data +char * compressedBuffer = new char[size_of_compressed_buffer]; + +// Decompress +char * buffer(nullptr); +size_t bufferSize(0); +bool result = BFT::DecompressBuffer(compressedBuffer, size_of_compressed_buffer, &buffer, bufferSize); +delete[] compressedBuffer; +if (result) { + // Do something with decompressedBuffer (size of bufferSize) + delete[] buffer; +} +``` + +### DiffBuffers +Processes two input buffers, diffing them. +Generates a compressed instruction set dictating how to get from the old buffer to the new buffer. +```c++ +bool DiffBuffers( + char * buffer_old, + const size_t & size_old, + char * buffer_new, + const size_t & size_new, + char ** buffer_diff, + size_t & size_diff, + size_t * instructionCount = nullptr +); +``` +Parameter: `buffer_old` the older of the 2 buffers. +Parameter: `size_old` the size of the old buffer. +Parameter: `buffer_new` the newer of the 2 buffers. +Parameter: `size_new` the size of the new buffer. +Parameter: `buffer_diff` pointer to store the diff buffer at. +Parameter: `size_diff` reference updated with the size of the compressed diff buffer. +Parameter: `instructionCount` (optional) pointer to update with the number of instructions processed. +Return: true if diff success, false otherwise. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * fileB = new char[255]; + +// ...Fill the buffers with some content... // + +// Diff Files (generate instruction set from fileA - fileB) +char * diffBuffer(nullptr); +size_t diffSize(0), instructionCount(0); +bool result = BFT::DiffBuffers(fileA, 100, fileB, 255, &diffBuffer, diffSize, &instructionCount); +delete[] fileA; +delete[] fileB; +if (result) { + // Do something with diffBuffer (size of diffSize) + delete[] diffBuffer; +} +``` + +### PatchBuffer +Reads from a compressed instruction set, uses it to patch the 'older' buffer into the 'newer' buffer +```c++ +bool PatchBuffer( + char * buffer_old, + const size_t & size_old, + char ** buffer_new, + size_t & size_new, + char * buffer_diff, + const size_t & size_diff, + size_t * instructionCount = nullptr +); +``` +Parameter: `buffer_old` the older of the 2 buffers. +Parameter: `size_old` the size of the old buffer. +Parameter: `buffer_new` pointer to store the newer of the 2 buffers. +Parameter: `size_new` reference updated with the size of the new buffer. +Parameter: `buffer_diff` the compressed diff buffer (instruction set). +Parameter: `size_diff` the size of the compressed diff buffer. +Parameter: `instructionCount` (optional) pointer to update with the number of instructions processed. +Return: true if patch success, false otherwise. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * diffBuffer = new char[size_of_diff]; + +// ...Fill the buffers with some content... // + +// Patch File (generate fileB from fileA + diffBuffer instructions) +char * fileB(nullptr); +size_t fileBSize(0), instructionCount(0); +bool result = BFT::PatchBuffer(fileA, 100, &fileB, fileBSize, diffBuffer, size_of_diff, &instructionCount); +delete[] fileA; +delete[] diffBuffer; +if (result) { + // Do something with fileB (size of fileBSize) + delete[] fileB; +} +``` + +### HashBuffer +Generates a hash value for the buffer provided, using the buffers' contents. +```c++ +size_t HashBuffer( + char * buffer, + const size_t & size +); +``` +Parameter: `buffer` the older of the 2 buffers. +Parameter: `size` the size of the old buffer. +Return: hash value for the buffer. + +Example Usage: +```c++ +// Buffers for two input files +char * fileA = new char[100]; +char * fileB = new char[255]; + +// ...Fill the buffers with some content... // + +size_t hashA = BFT::HashBuffer(fileA, 100); +size_t hashB = BFT::HashBuffer(fileB, 255); + +if (hashA != hashB) { + // Diff the buffers or do something with the knowledge that they differ +} +``` From 11c1b08b938ac430f53ca145989a812ae0b7a34d Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sun, 21 Apr 2019 12:21:39 -0500 Subject: [PATCH 35/44] Updating documentation, readme, and task logger for unpacker Updating documentation, readme, and task logger for unpacker --- src/BufferTools.h | 18 ++++++++++-------- src/DirectoryTools.cpp | 6 +++++- src/README.md | 2 +- src/Unpacker/Unpacker.cpp | 32 ++++++++++++++++++++------------ src/Updater/Updater.cpp | 2 ++ src/nSuite/nSuite.cpp | 4 +++- 6 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/BufferTools.h b/src/BufferTools.h index b6f07d5..2e5724b 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -5,7 +5,8 @@ /** Namespace to keep buffer-related operations grouped together. */ namespace BFT { - /** Compresses a source buffer into an equal or smaller sized destination buffer. + /** Compresses a source buffer into an equal or smaller-sized destination buffer. + After compression, it applies a small header describing how large the uncompressed buffer is. --------------- | buffer data | --------------- @@ -14,19 +15,20 @@ namespace BFT { | compression header | compressed data | ----------------------------------------- @param sourceBuffer the original buffer to read from. - @param sourceSize the size in bytes of the source buffer. + @param sourceSize the size of the source buffer in bytes. @param destinationBuffer pointer to the destination buffer, which will hold compressed contents. @param destinationSize reference updated with the size in bytes of the compressed destinationBuffer. @return true if compression success, false otherwise. */ bool CompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); - /** Decompressess a source buffer into an equal or larger sized destination buffer. + /** Decompressess a source buffer into an equal or larger-sized destination buffer. @param sourceBuffer the original buffer to read from. - @param sourceSize the size in bytes of the source buffer. + @param sourceSize the size of the source buffer in bytes. @param destinationBuffer pointer to the destination buffer, which will hold decompressed contents. @param destinationSize reference updated with the size in bytes of the decompressed destinationBuffer. @return true if decompression success, false otherwise. */ bool DecompressBuffer(char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, size_t & destinationSize); - /** Processes both input buffers, differentiating them, generating an output (compressed) diff-buffer, ready to be written to disk. + /** Processes two input buffers, diffing them. + Generates a compressed instruction set dictating how to get from the old buffer to the new buffer. @note caller expected to clean-up buffer_diff on their own @param buffer_old the older of the 2 buffers. @param size_old the size of the old buffer. @@ -34,16 +36,16 @@ namespace BFT { @param size_new the size of the new buffer. @param buffer_diff pointer to store the diff buffer at. @param size_diff reference updated with the size of the compressed diff buffer. - @param instructionCount pointer to update with the number of instructions processed. (optional) + @param instructionCount (optional) pointer to update with the number of instructions processed. @return true if diff success, false otherwise. */ bool DiffBuffers(char * buffer_old, const size_t & size_old, char * buffer_new, const size_t & size_new, char ** buffer_diff, size_t & size_diff, size_t * instructionCount = nullptr); - /** Uses a compressed diff buffer to patch a source buffer into an updated destination buffer + /** Reads from a compressed instruction set, uses it to patch the 'older' buffer into the 'newer' buffer @note caller expected to clean-up buffer_new on their own @param buffer_old the older of the 2 buffers. @param size_old the size of the old buffer. @param buffer_new pointer to store the newer of the 2 buffers. @param size_new reference updated with the size of the new buffer. - @param buffer_diff the compressed diff buffer. + @param buffer_diff the compressed diff buffer (instruction set). @param size_diff the size of the compressed diff buffer. @param instructionCount pointer to update with the number of instructions processed. (optional) @return true if patch success, false otherwise. */ diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 1d69c7c..2b29d1c 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -545,7 +545,11 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Patch buffer TaskLogger::PushText("patching file \"" + file.path + "\"\r\n"); size_t newSize(0ull); - BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); + if (!BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed)) { + TaskLogger::PushText("Critical failure: patching failed!.\r\n"); + return false; + } + const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match diff --git a/src/README.md b/src/README.md index 7d84944..a55f788 100644 --- a/src/README.md +++ b/src/README.md @@ -12,7 +12,7 @@ Buffer Tools exposes 5 useful functions: - [HashBuffer](#HashBuffer) ## DirectoryTools -Directory Tools exposes 4 similar functions, serving more as macros, applying to an entire directory: +Directory Tools exposes 4 similar functions that effectively map BFT functions to an entire directory: - [CompressDirectory](#CompressDirectory) - [DecompressDirectory](#DecompressDirectory) - [DiffDirectory](#DiffDirectory) diff --git a/src/Unpacker/Unpacker.cpp b/src/Unpacker/Unpacker.cpp index 222429a..ea98a2f 100644 --- a/src/Unpacker/Unpacker.cpp +++ b/src/Unpacker/Unpacker.cpp @@ -7,19 +7,22 @@ /** Entry point. */ int main() { - // Check command line arguments - const std::string dstDirectory(get_current_directory()); + // Tap-in to the log, have it redirect to the console + TaskLogger::AddCallback_TextAdded([&](const std::string & message) { + std::cout << message; + }); - // Report an overview of supplied procedure - std::cout << + TaskLogger::PushText( " ~\r\n" " Unpackager /\r\n" " ~------------------~\r\n" " /\r\n" - "~\r\n\r\n"; + "~\r\n\r\n" + ); // Acquire archive resource const auto start = std::chrono::system_clock::now(); + const std::string dstDirectory(get_current_directory()); size_t fileCount(0ull), byteCount(0ull); Resource archive(IDR_ARCHIVE, "ARCHIVE"); if (!archive.exists()) @@ -30,11 +33,13 @@ int main() const auto folderSize = *reinterpret_cast(packBufferOffset); packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); const auto folderName = std::string(reinterpret_cast(packBufferOffset), folderSize); - // Report an overview of supplied procedure - std::cout << + + // Report where we're unpacking to + TaskLogger::PushText( "Unpacking to the following directory:\r\n" "\t> " + dstDirectory + "\\" + folderName + - "\r\n"; + "\r\n" + ); // Unpackage using the resource file if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) @@ -43,10 +48,13 @@ int main() // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; - std::cout - << "Files written: " << fileCount << "\r\n" - << "Bytes written: " << byteCount << "\r\n" - << "Total duration: " << elapsed_seconds.count() << " seconds\r\n\r\n"; + TaskLogger::PushText( + "Files written: " + std::to_string(fileCount) + "\r\n" + + "Bytes written: " + std::to_string(byteCount) + "\r\n" + + "Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n" + ); + + // Pause and exit system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/Updater/Updater.cpp b/src/Updater/Updater.cpp index 0305e6e..336fd7e 100644 --- a/src/Updater/Updater.cpp +++ b/src/Updater/Updater.cpp @@ -20,6 +20,7 @@ static auto get_patches(const std::string & srcDirectory) /** Entry point. */ int main() { + // Tap-in to the log, have it redirect to the console TaskLogger::AddCallback_TextAdded([&](const std::string & message) { std::cout << message; }); @@ -81,6 +82,7 @@ int main() ); } + // Pause and exit system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file diff --git a/src/nSuite/nSuite.cpp b/src/nSuite/nSuite.cpp index 55eed46..57bff6c 100644 --- a/src/nSuite/nSuite.cpp +++ b/src/nSuite/nSuite.cpp @@ -51,10 +51,12 @@ int main(int argc, char *argv[]) // Command exists in command map, execute it commandMap.at(argv[1])->execute(argc, argv); - // Output results and finish + // Success, report results const auto end = std::chrono::system_clock::now(); const std::chrono::duration elapsed_seconds = end - start; TaskLogger::PushText("Total duration: " + std::to_string(elapsed_seconds.count()) + " seconds\r\n\r\n"); + + // Pause and exit system("pause"); exit(EXIT_SUCCESS); } \ No newline at end of file From fe677747d82586d63257a1edd2f9913f65460a99 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Sun, 21 Apr 2019 12:30:35 -0500 Subject: [PATCH 36/44] Version updates Version updates --- src/Installer/Installer.rc | Bin 3418 -> 3418 bytes src/README.md | 2 +- src/Uninstaller/Uninstaller.rc | Bin 3030 -> 3030 bytes src/Unpacker/Unpacker.rc | Bin 3002 -> 3002 bytes src/Updater/Updater.rc | Bin 2932 -> 2932 bytes 5 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Installer/Installer.rc b/src/Installer/Installer.rc index d4b7cc5a7555b35008291abc022f59e5b1c242bc..b7ae4c72c3adff35dda6bd8874f31b0a2f532b52 100644 GIT binary patch delta 21 bcmca5bxUeP4m+da=3I7uCLmSId6yXgQOyRy delta 21 bcmca5bxUeP4m+d4=3I7uCLmSId6yXgQMv}d diff --git a/src/README.md b/src/README.md index a55f788..fbb07ec 100644 --- a/src/README.md +++ b/src/README.md @@ -12,7 +12,7 @@ Buffer Tools exposes 5 useful functions: - [HashBuffer](#HashBuffer) ## DirectoryTools -Directory Tools exposes 4 similar functions that effectively map BFT functions to an entire directory: +Directory Tools exposes 4 similar functions which effectively maps BFT functions to an entire directory: - [CompressDirectory](#CompressDirectory) - [DecompressDirectory](#DecompressDirectory) - [DiffDirectory](#DiffDirectory) diff --git a/src/Uninstaller/Uninstaller.rc b/src/Uninstaller/Uninstaller.rc index 0099a1a83bfe89b7a887aa80eeb2cc3a823c1a31..c97483cea9ee4f1341cb30349827d2b270af642c 100644 GIT binary patch delta 29 lcmca6eocJC9acue&39R)nHUWx-({7bti>U;S&L&0GXSB334s6r delta 29 lcmca6eocJC9actz&39R)nHUWw-({7bti>U;S&L&0GXSAr34Z_p diff --git a/src/Unpacker/Unpacker.rc b/src/Unpacker/Unpacker.rc index 42c58347b723bf81d029c118809ef9f898df03ca..29c05a7de2bb4f197173502cf472dc790e5c4435 100644 GIT binary patch delta 33 pcmdlbzDs<=C00hm$#>c1CkwE0OfKNy+swgsg^|&4^Ii74%mBhC3i<#5 delta 33 pcmdlbzDs<=C00g*$#>c1CkwE0OfKNy+swgsg^|%<^Ii74%mBg&3itp3 diff --git a/src/Updater/Updater.rc b/src/Updater/Updater.rc index adf55e4483861161ebf279775f16a6dac02f6c20..a497b8909dcbef60930871d6dd423bd3499fa0ad 100644 GIT binary patch delta 53 zcmV-50LuUL7W5XdIt34K0AT=h0Am1X0A&Dj04|fE1uc_y1tI}4lbr`2lVAlFv(5$1 L0Rb_yod?$gscR4n delta 53 zcmV-50LuUL7W5XdIt34P0B`_g0AT=h0A&Dj04|fE1uc_y1tI}3lbr`2lVAlFv(5$1 L0Rb?xod?$gtCSEJ From 117d1e17fca16aeee5d3806d8f8302037cb1fcb2 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 13:22:26 -0500 Subject: [PATCH 37/44] Update README.md --- src/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/README.md b/src/README.md index fbb07ec..a9e44fd 100644 --- a/src/README.md +++ b/src/README.md @@ -3,26 +3,26 @@ The core of this library can be found between 2 namespaces: - BFT (BufferTools) - DFT (DirectoryTools) -## BufferTools -Buffer Tools exposes 5 useful functions: +#### The BufferTools namespace provides the following 5 functions: - [CompressBuffer](#CompressBuffer) - [DecompressBuffer](#DecompressBuffer) - [DiffBuffers](#DiffBuffers) - [PatchBuffer](#PatchBuffer) - [HashBuffer](#HashBuffer) -## DirectoryTools -Directory Tools exposes 4 similar functions which effectively maps BFT functions to an entire directory: +### The DirectoryTools namespace exposes 4 similar helper functions: - [CompressDirectory](#CompressDirectory) - [DecompressDirectory](#DecompressDirectory) - [DiffDirectory](#DiffDirectory) - [PatchDirectory](#PatchDirectory) +# Functions +## BFT namespace ### CompressBuffer Compresses a source buffer into an equal or smaller-sized destination buffer. After compression, it applies a small header describing how large the uncompressed buffer is. ```c++ -bool CompressBuffer( +bool BFT::CompressBuffer( char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, @@ -55,7 +55,7 @@ if (result) { Decompressess a source buffer into an equal or larger-sized destination buffer. Prior to decompression, it reads from a small header describing how large of a buffer it needs to allocate. ```c++ -bool CompressBuffer( +bool BFT::DecompressBuffer( char * sourceBuffer, const size_t & sourceSize, char ** destinationBuffer, @@ -88,7 +88,7 @@ if (result) { Processes two input buffers, diffing them. Generates a compressed instruction set dictating how to get from the old buffer to the new buffer. ```c++ -bool DiffBuffers( +bool BFT::DiffBuffers( char * buffer_old, const size_t & size_old, char * buffer_new, @@ -130,7 +130,7 @@ if (result) { ### PatchBuffer Reads from a compressed instruction set, uses it to patch the 'older' buffer into the 'newer' buffer ```c++ -bool PatchBuffer( +bool BFT::PatchBuffer( char * buffer_old, const size_t & size_old, char ** buffer_new, @@ -172,7 +172,7 @@ if (result) { ### HashBuffer Generates a hash value for the buffer provided, using the buffers' contents. ```c++ -size_t HashBuffer( +size_t BFT::HashBuffer( char * buffer, const size_t & size ); From 1bb044343ec59fa744e023cd90163b8211d4dda0 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 14:04:13 -0500 Subject: [PATCH 38/44] Update README.md --- src/README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/README.md b/src/README.md index a9e44fd..9e6bbf6 100644 --- a/src/README.md +++ b/src/README.md @@ -10,14 +10,13 @@ The core of this library can be found between 2 namespaces: - [PatchBuffer](#PatchBuffer) - [HashBuffer](#HashBuffer) -### The DirectoryTools namespace exposes 4 similar helper functions: +#### The DirectoryTools namespace exposes 4 similar helper functions: - [CompressDirectory](#CompressDirectory) - [DecompressDirectory](#DecompressDirectory) - [DiffDirectory](#DiffDirectory) - [PatchDirectory](#PatchDirectory) # Functions -## BFT namespace ### CompressBuffer Compresses a source buffer into an equal or smaller-sized destination buffer. After compression, it applies a small header describing how large the uncompressed buffer is. @@ -196,3 +195,71 @@ if (hashA != hashB) { // Diff the buffers or do something with the knowledge that they differ } ``` + +### CompressDirectory +Compresses all disk contents found within a source directory into an .npack - package formatted buffer. +After compression, it applies a small header dictating packaged folders' name. +```c++ +bool DFT::CompressDirectory( + const std::string & srcDirectory, + char ** packBuffer, + size_t & packSize, + size_t * byteCount = nullptr, + size_t * fileCount = nullptr, + const std::vector & exclusions = std::vector() +); +``` +Parameter: `srcDirectory` the absolute path to the directory to compress. +Parameter: `packBuffer` pointer to the destination buffer, which will hold compressed contents. +Parameter: `packSize` reference updated with the size in bytes of the compressed packBuffer. +Parameter: `byteCount` (optional) pointer updated with the number of bytes written into the package. +Parameter: `fileCount` (optional) pointer updated with the number of files written into the package. +Parameter: `exclusions` (optional) list of filenames/types to skip "\\string" match relative path, ".ext" match extension. +Return: true if compression success, false otherwise. + +Example Usage: +```c++ +std::string directory_to_compress = "C:\\some directory" + +// Compress +char * packageBuffer(nullptr); +size_t packageSize(0ull), maxSize(0ull), fileCount(0ull); +bool result = DRT::CompressDirectory(directory_to_compress, &packageBuffer, packageSize, &maxSize, &fileCount, {"\\cache.txt", ".cache"}); +if (result) { + // Do something with packageBuffer (size of packageSize) + delete[] packageBuffer; +} +``` + +### DecompressDirectory +Decompresses an .npack - package formatted buffer into its component files in the destination directory. +```c++ +bool DFT::DecompressDirectory( + const std::string & dstDirectory, + char * packBuffer, + const size_t & packSize, + size_t * byteCount = nullptr, + size_t * fileCount = nullptr +); +``` +Parameter: `dstDirectory` the absolute path to the directory to compress. +Parameter: `packBuffer` the buffer containing the compressed package contents. +Parameter: `packSize` the size of the buffer in bytes. +Parameter: `byteCount` (optional) pointer updated with the number of bytes written to disk. +Parameter: `fileCount` (optional) pointer updated with the number of files written to disk. +Return: true if decompression success, false otherwise. + +Example Usage: +```c++ +std::string directory_to_write = "C:\\some directory" +char * packageBuffer = get_package_buffer(); +size_t packageSize = get_package_size(); + +// Compress +size_t byteCount(0ull), fileCount(0ull); +bool result = DRT::DecompressDirectory(directory_to_write, packageBuffer, packageSize, &byteCount, &fileCount); +delete[] packageBuffer; +if (result) { + // Do something knowing that the package has extracted +} +``` From 8acbc9fdac076881ac7de0a932a0683e006a52a5 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 14:18:13 -0500 Subject: [PATCH 39/44] Update README.md --- src/README.md | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/README.md b/src/README.md index 9e6bbf6..6b8ae79 100644 --- a/src/README.md +++ b/src/README.md @@ -13,7 +13,7 @@ The core of this library can be found between 2 namespaces: #### The DirectoryTools namespace exposes 4 similar helper functions: - [CompressDirectory](#CompressDirectory) - [DecompressDirectory](#DecompressDirectory) -- [DiffDirectory](#DiffDirectory) +- [DiffDirectories](#DiffDirectories) - [PatchDirectory](#PatchDirectory) # Functions @@ -219,7 +219,7 @@ Return: true if compression success, false otherwise. Example Usage: ```c++ -std::string directory_to_compress = "C:\\some directory" +std::string directory_to_compress = "C:\\some directory"; // Compress char * packageBuffer(nullptr); @@ -251,7 +251,7 @@ Return: true if decompression success, false otherwise. Example Usage: ```c++ -std::string directory_to_write = "C:\\some directory" +std::string directory_to_write = "C:\\some directory"; char * packageBuffer = get_package_buffer(); size_t packageSize = get_package_size(); @@ -263,3 +263,36 @@ if (result) { // Do something knowing that the package has extracted } ``` + +### DiffDirectories +Processes two input directories and generates a compressed instruction set for transforming the old directory into the new directory. +```c++ +bool DFT::DiffDirectories( + const std::string & oldDirectory, + const std::string & newDirectory, + char ** diffBuffer, + size_t & diffSize, + size_t & instructionCount +); +``` +Parameter: `oldDirectory` the older directory, or path to an .npack file. +Parameter: `newDirectory` the newer directory, or path to an .npack file. +Parameter: `diffBuffer` pointer to the diff buffer, which will hold compressed diff instructions. +Parameter: `diffSize` reference updated with the size in bytes of the diff buffer. +Parameter: `instructionCount` (optional) pointer updated with the number of instructions compressed into the diff buffer. +Return: true if diff success, false otherwise. + +Example Usage: +```c++ +std::string oldDirectory = "C:\\old software"; +std::string newDirectory = "C:\\new software"; + +// Diff the 2 directories +char * diffBuffer(nullptr); +size_t diffSize(0ull), instructionCount(0ull); +bool result = DRT::DiffDirectories(oldDirectory, newDirectory, &diffBuffer, diffSize, &instructionCount); +if (result) { + // Do something with the diffBuffer (size of diffSize) + delete[] diffBuffer; +} +``` From a0cd42f4fd3d40fd7ec3694251f520df6c2f52ce Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 14:30:01 -0500 Subject: [PATCH 40/44] Update README.md --- src/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/README.md b/src/README.md index 6b8ae79..2bd56ad 100644 --- a/src/README.md +++ b/src/README.md @@ -296,3 +296,37 @@ if (result) { delete[] diffBuffer; } ``` + +### PatchDirectory +Decompresses and executes the instructions contained within a previously-generated diff buffer. +Transforms the contents of an 'old' directory into that of the 'new' directory. +```c++ +bool DFT::PatchDirectory( + const std::string & dstDirectory, + char * diffBuffer, + const size_t & diffSizeComp, + size_t * bytesWritten, + size_t * instructionsUsed = nullptr +); +``` +Parameter: `dstDirectory` the destination directory to transform. +Parameter: `diffBuffer` the buffer containing the compressed diff instructions. +Parameter: `diffSize` the size in bytes of the compressed diff buffer. +Parameter: `bytesWritten` (optional) pointer updated with the number of bytes written to disk. +Parameter: `instructionCount` (optional) pointer updated with the number of instructions executed. +Return: true if patch success, false otherwise. + +Example Usage: +```c++ +std::string dstDirectory = "C:\\old software"; +char * diffBuffer = get_diff_buffer(); +size_t diffSize = get_diff_size(); + +// Diff the 2 directories +size_t bytesWritten(0ull), instructionsExecuted(0ull); +bool result = DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten, &instructionsExecuted); +delete[] diffBuffer; +if (result) { + // Do something with the knowledge that the destination directory just updated to a new version +} +``` From e5c912ab4ddc81f6ea9a9ae3f1dab5f1bff4cb88 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 14:40:13 -0500 Subject: [PATCH 41/44] Updated documentation, code cleanup - Updated documentation - Made more parameters into bft/dft optional --- src/BufferTools.h | 4 +- src/Common.h | 1 + src/DirectoryTools.cpp | 48 +++++++++++++++++------- src/DirectoryTools.h | 45 +++++++++++----------- src/Installer/Installer.cpp | 3 +- src/Installer/Screens/Agreement.h | 2 - src/Installer/Screens/Directory.cpp | 11 ------ src/TaskLogger.h | 6 +-- src/Unpacker/Unpacker.cpp | 2 +- src/Updater/Updater.cpp | 4 +- src/nSuite/Commands/DiffCommand.cpp | 2 +- src/nSuite/Commands/InstallerCommand.cpp | 9 +++-- src/nSuite/Commands/PackCommand.cpp | 9 +++-- src/nSuite/Commands/PackagerCommand.cpp | 11 +++--- src/nSuite/Commands/PatchCommand.cpp | 2 +- src/nSuite/Commands/UnpackCommand.cpp | 4 +- 16 files changed, 88 insertions(+), 75 deletions(-) diff --git a/src/BufferTools.h b/src/BufferTools.h index 2e5724b..5202ec4 100644 --- a/src/BufferTools.h +++ b/src/BufferTools.h @@ -47,10 +47,10 @@ namespace BFT { @param size_new reference updated with the size of the new buffer. @param buffer_diff the compressed diff buffer (instruction set). @param size_diff the size of the compressed diff buffer. - @param instructionCount pointer to update with the number of instructions processed. (optional) + @param instructionCount (optional) pointer to update with the number of instructions processed. @return true if patch success, false otherwise. */ bool PatchBuffer(char * buffer_old, const size_t & size_old, char ** buffer_new, size_t & size_new, char * buffer_diff, const size_t & size_diff, size_t * instructionCount = nullptr); - /** Generate a hash value for the buffer provided. + /** Generates a hash value for the buffer provided, using the buffers' contents. @param buffer pointer to the buffer to hash. @param size the size of the buffer. @return hash value for the buffer. */ diff --git a/src/Common.h b/src/Common.h index 0fa2e41..aa2cb65 100644 --- a/src/Common.h +++ b/src/Common.h @@ -13,6 +13,7 @@ #include #include + /** Changes an input string to lower case, and returns it. @param string the input string. @return lower case version of the string. */ diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 2b29d1c..483b956 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -10,7 +10,7 @@ #include -bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount, const std::vector & exclusions) +bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t * byteCount, size_t * fileCount, const std::vector & exclusions) { // Variables Threader threader; @@ -69,7 +69,12 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer files.push_back({ entry.path().string(), path, entry.file_size(), unitSize }); } } - fileCount = files.size(); + + // Update optional parameters + if (byteCount != nullptr) + *byteCount = archiveSize; + if (fileCount != nullptr) + *fileCount = files.size(); // Create buffer for final file data char * filebuffer = new char[archiveSize]; @@ -132,7 +137,7 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer return true; } -bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount) +bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t * byteCount, size_t * fileCount) { Threader threader; if (packSize <= 0ull) { @@ -202,14 +207,18 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe threader.shutdown(); TaskLogger::SetProgress(bytesRead + 100); + // Update optional parameters + if (byteCount != nullptr) + *byteCount = bytesRead; + if (fileCount != nullptr) + *fileCount = filesWritten; + // Success - fileCount = filesWritten; - byteCount = bytesRead; delete[] decompressedBuffer; return true; } -bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t & instructionCount) +bool DRT::DiffDirectories(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t * instructionCount) { // Declarations that will only be used here struct File { @@ -379,6 +388,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne vecBuffer.reserve(reserveSize); // These files are common, maybe some have changed + size_t instructionNum(0ull); for each (const auto & cFiles in commonFiles) { char * oldBuffer(nullptr), * newBuffer(nullptr); size_t oldHash(0ull), newHash(0ull); @@ -387,7 +397,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne // Files are different versions char * buffer(nullptr); size_t size(0ull); - if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionCount)) { + if (BFT::DiffBuffers(oldBuffer, cFiles.first->size, newBuffer, cFiles.second->size, &buffer, size, &instructionNum)) { TaskLogger::PushText("Diffing file: \"" + cFiles.first->relative + "\"\r\n"); writeInstructions(cFiles.first->relative, oldHash, newHash, buffer, size, 'U', vecBuffer); } @@ -408,7 +418,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne if (nFile->open(&newBuffer, newHash)) { char * buffer(nullptr); size_t size(0ull); - if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionCount)) { + if (BFT::DiffBuffers(nullptr, 0ull, newBuffer, nFile->size, &buffer, size, &instructionNum)) { TaskLogger::PushText("Adding file: \"" + nFile->relative + "\"\r\n"); writeInstructions(nFile->relative, 0ull, newHash, buffer, size, 'N', vecBuffer); } @@ -425,7 +435,7 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne char * oldBuffer(nullptr); size_t oldHash(0ull); if (oFile->open(&oldBuffer, oldHash)) { - instructionCount++; + instructionNum++; TaskLogger::PushText("Removing file: \"" + oFile->relative + "\"\r\n"); writeInstructions(oFile->relative, oldHash, 0ull, nullptr, 0ull, 'D', vecBuffer); } @@ -440,10 +450,14 @@ bool DRT::DiffDirectory(const std::string & oldDirectory, const std::string & ne TaskLogger::PushText("Critical failure: cannot compress diff file.\r\n"); return false; } + + // Update optional parameters + if (instructionCount != nullptr) + *instructionCount = instructionNum; return true; } -bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t & bytesWritten, size_t & instructionsUsed) +bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferCompressed, const size_t & diffSizeCompressed, size_t * bytesWritten, size_t * instructionsUsed) { char * diffBuffer(nullptr); size_t diffSize(0ull); @@ -523,6 +537,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp } // Patch all files first + size_t byteNum(0ull), instructionNum(0ull); for each (const auto & file in diffFiles) { // Try to read the target file char *oldBuffer(nullptr), *newBuffer(nullptr); @@ -545,7 +560,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Patch buffer TaskLogger::PushText("patching file \"" + file.path + "\"\r\n"); size_t newSize(0ull); - if (!BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed)) { + if (!BFT::PatchBuffer(oldBuffer, oldSize, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionNum)) { TaskLogger::PushText("Critical failure: patching failed!.\r\n"); return false; } @@ -566,7 +581,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp } newFile.write(newBuffer, std::streamsize(newSize)); newFile.close(); - bytesWritten += newSize; + byteNum += newSize; } // Cleanup and finish @@ -584,7 +599,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp // Remember that we use the diff/patch function to add new files too char * newBuffer(nullptr); size_t newSize(0ull); - BFT::PatchBuffer(nullptr, 0ull, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionsUsed); + BFT::PatchBuffer(nullptr, 0ull, &newBuffer, newSize, file.instructionSet, file.instructionSize, &instructionNum); const size_t newHash = BFT::HashBuffer(newBuffer, newSize); // Confirm new hashes match @@ -601,7 +616,7 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp } newFile.write(newBuffer, std::streamsize(newSize)); newFile.close(); - bytesWritten += newSize; + byteNum += newSize; // Cleanup and finish delete[] file.instructionSet; @@ -631,5 +646,10 @@ bool DRT::PatchDirectory(const std::string & dstDirectory, char * diffBufferComp delete[] oldBuffer; } + // Update optional parameters + if (bytesWritten != nullptr) + *bytesWritten = byteNum; + if (instructionsUsed != nullptr) + *instructionsUsed = instructionNum; return true; } \ No newline at end of file diff --git a/src/DirectoryTools.h b/src/DirectoryTools.h index 4226aa7..25540f3 100644 --- a/src/DirectoryTools.h +++ b/src/DirectoryTools.h @@ -8,43 +8,46 @@ /** Namespace to keep directory-related operations grouped together. */ namespace DRT { - /** Compresses a source directory into an equal or smaller sized destination buffer. + /** Compresses all disk contents found within a source directory into an .npack - package formatted buffer. + After compression, it applies a small header dictating packaged folders' name. ------------------------------------------------------ | Directory name header | compressed directory data | ------------------------------------------------------ @note caller is responsible for cleaning-up packBuffer. @param srcDirectory the absolute path to the directory to compress. @param packBuffer pointer to the destination buffer, which will hold compressed contents. - @param packSize reference updated with the size in bytes of the compressed destinationBuffer. - @param fileCount reference updated with the number of files compressed into the package. - @param exclusions optional list of filenames/types to avoid packaging. Strings starting with "\\" match an entire path, "." match extension. + @param packSize reference updated with the size in bytes of the compressed packBuffer. + @param byteCount (optional) pointer updated with the number of bytes written into the package + @param fileCount (optional) pointer updated with the number of files written into the package. + @param exclusions (optional) list of filenames/types to skip. "\\string" match relative path, ".ext" match extension. @return true if compression success, false otherwise. */ - bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t & fileCount, const std::vector & exclusions = std::vector()); - /** Decompresses a packaged buffer into a destination directory. - @param dstDirectory the absolute path to the directory to compress. - @param packBuffer the buffer containing the compressed contents. + bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t * byteCount = nullptr, size_t * fileCount = nullptr, const std::vector & exclusions = std::vector()); + /** Decompresses an .npack - package formatted buffer into its component files in the destination directory. + @param dstDirectory the absolute path to the directory to decompress. + @param packBuffer the buffer containing the compressed package contents. @param packSize the size of the buffer in bytes. - @param byteCount reference updated with the number of bytes written to disk. - @param fileCount reference updated with the number of files written to disk. + @param byteCount (optional) pointer updated with the number of bytes written to disk. + @param fileCount (optional) pointer updated with the number of files written to disk. @return true if decompression success, false otherwise. */ - bool DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t & byteCount, size_t & fileCount); + bool DecompressDirectory(const std::string & dstDirectory, char * packBuffer, const size_t & packSize, size_t * byteCount = nullptr, size_t * fileCount = nullptr); /** Processes two input directories and generates a compressed instruction set for transforming the old directory into the new directory. @note caller is responsible for cleaning-up diffBuffer. - @param oldDirectory the older directory. - @param newDirectory the newer directory. + @param oldDirectory the older directory or path to an .npack file. + @param newDirectory the newer directory or path to an .npack file. @param diffBuffer pointer to the diff buffer, which will hold compressed diff instructions. @param diffSize reference updated with the size in bytes of the diff buffer. - @param instructionCount reference updated with the number of instructions compressed into the diff buffer. + @param instructionCount (optional) pointer updated with the number of instructions compressed into the diff buffer. @return true if diff success, false otherwise. */ - bool DiffDirectory(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t & instructionCount); - /** Using a previously-generated diff file, applies the patching procedure to transform an input directory to the 'newer' state. + bool DiffDirectories(const std::string & oldDirectory, const std::string & newDirectory, char ** diffBuffer, size_t & diffSize, size_t * instructionCount = nullptr); + /** Decompresses and executes the instructions contained within a previously - generated diff buffer. + Transforms the contents of an 'old' directory into that of the 'new' directory. @param dstDirectory the destination directory to transform. - @param diffBufferComp the buffer containing the compressed diff instructions. - @param diffSizeComp the size in bytes of the compressed diff buffer. - @param bytesWritten reference updated with the number of bytes written to disk. - @param instructionsUsed reference updated with the number of instructions executed. + @param diffBuffer the buffer containing the compressed diff instructions. + @param diffSize the size in bytes of the compressed diff buffer. + @param bytesWritten (optional) pointer updated with the number of bytes written to disk. + @param instructionsUsed (optional) pointer updated with the number of instructions executed. @return true if patch success, false otherwise. */ - bool PatchDirectory(const std::string & dstDirectory, char * diffBufferComp, const size_t & diffSizeComp, size_t & bytesWritten, size_t & instructionsUsed); + bool PatchDirectory(const std::string & dstDirectory, char * diffBuffer, const size_t & diffSize, size_t * bytesWritten = nullptr, size_t * instructionsUsed = nullptr); }; #endif // DIRECTORY_TOOLS_H \ No newline at end of file diff --git a/src/Installer/Installer.cpp b/src/Installer/Installer.cpp index 6691dac..233eef1 100644 --- a/src/Installer/Installer.cpp +++ b/src/Installer/Installer.cpp @@ -209,10 +209,9 @@ void Installer::beginInstallation() } else { // Unpackage using the rest of the resource file - size_t byteCount(0ull), fileCount(0ull); auto directory = getDirectory(); sanitize_path(directory); - if (!DRT::DecompressDirectory(directory, reinterpret_cast(m_archive.getPtr()), m_archive.getSize(), byteCount, fileCount)) + if (!DRT::DecompressDirectory(directory, reinterpret_cast(m_archive.getPtr()), m_archive.getSize())) invalidate(); else { // Write uninstaller to disk diff --git a/src/Installer/Screens/Agreement.h b/src/Installer/Screens/Agreement.h index 34195d1..a9364f7 100644 --- a/src/Installer/Screens/Agreement.h +++ b/src/Installer/Screens/Agreement.h @@ -21,8 +21,6 @@ class Agreement : public Screen { // Public Methods /** Check the 'I agree' button. */ void checkYes(); - /** Check the 'I doon't agree' button. */ - void checkNo(); /** Switch to the previous state. */ void goPrevious(); /** Switch to the next state. */ diff --git a/src/Installer/Screens/Directory.cpp b/src/Installer/Screens/Directory.cpp index 701f8a4..d9f2f77 100644 --- a/src/Installer/Screens/Directory.cpp +++ b/src/Installer/Screens/Directory.cpp @@ -11,17 +11,6 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); static HRESULT CreateDialogEventHandler(REFIID, void **); static HRESULT OpenFileDialog(std::string &); -static std::string directory_and_package(const std::string & dir, const std::string & pack) -{ - std::string directory = dir; - // Ensure last character is a backslash - if (dir.size() && dir[dir.size() - 1ull] != '\\') - directory += '\\' + pack; - else - directory += pack; - return directory; -} - Directory::~Directory() { UnregisterClass("DIRECTORY_SCREEN", m_hinstance); diff --git a/src/TaskLogger.h b/src/TaskLogger.h index 1002ca1..a198483 100644 --- a/src/TaskLogger.h +++ b/src/TaskLogger.h @@ -1,6 +1,6 @@ #pragma once -#ifndef LOGGER_H -#define LOGGER_H +#ifndef TASK_LOGGER_H +#define TASK_LOGGER_H #include #include @@ -109,4 +109,4 @@ class TaskLogger { std::vector> m_progressCallbacks; }; -#endif // LOGGER_H \ No newline at end of file +#endif // TASK_LOGGER_H \ No newline at end of file diff --git a/src/Unpacker/Unpacker.cpp b/src/Unpacker/Unpacker.cpp index ea98a2f..7bb316f 100644 --- a/src/Unpacker/Unpacker.cpp +++ b/src/Unpacker/Unpacker.cpp @@ -42,7 +42,7 @@ int main() ); // Unpackage using the resource file - if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), byteCount, fileCount)) + if (!DRT::DecompressDirectory(dstDirectory, reinterpret_cast(archive.getPtr()), archive.getSize(), &byteCount, &fileCount)) exit_program("Cannot decompress embedded package resource, aborting...\r\n"); // Success, report results diff --git a/src/Updater/Updater.cpp b/src/Updater/Updater.cpp index 336fd7e..3933ca5 100644 --- a/src/Updater/Updater.cpp +++ b/src/Updater/Updater.cpp @@ -44,7 +44,7 @@ int main() // Begin updating const auto start = std::chrono::system_clock::now(); - size_t bytesWritten(0ull), instructionsUsed(0ull), patchesApplied(0ull); + size_t bytesWritten(0ull), patchesApplied(0ull); for each (const auto & patch in patches) { // Open diff file std::ifstream diffFile(patch, std::ios::binary | std::ios::beg); @@ -58,7 +58,7 @@ int main() char * diffBuffer = new char[diffSize]; diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); - if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) { + if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten)) { TaskLogger::PushText("skipping patch...\r\n"); delete[] diffBuffer; continue; diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 0355b93..830db64 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -55,7 +55,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const // Diff the 2 directories specified char * diffBuffer(nullptr); size_t diffSize(0ull), instructionCount(0ull); - if (!DRT::DiffDirectory(oldDirectory, newDirectory, &diffBuffer, diffSize, instructionCount)) + if (!DRT::DiffDirectories(oldDirectory, newDirectory, &diffBuffer, diffSize, &instructionCount)) exit_program("aborting...\r\n"); // Create diff file diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index 6786a1a..25a1feb 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -45,8 +45,8 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); - size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount, {"\\manifest.nman"})) + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount, {"\\manifest.nman"})) exit_program("Cannot create installer from the directory specified, aborting...\r\n"); // Acquire installer resource @@ -88,7 +88,8 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const // Output results TaskLogger::PushText( - "Files packaged: " + std::to_string(fileCount) + "\r\n" + - "Bytes packaged: " + std::to_string(packSize) + "\r\n" + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index 9d5eb03..1d442c5 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -44,8 +44,8 @@ void PackCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); - size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount)) exit_program("Cannot create package from the directory specified, aborting...\r\n"); // Write package to disk @@ -59,7 +59,8 @@ void PackCommand::execute(const int & argc, char * argv[]) const // Output results TaskLogger::PushText( - "Files packaged: " + std::to_string(fileCount) + "\r\n" + - "Bytes packaged: " + std::to_string(packSize) + "\r\n" + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PackagerCommand.cpp b/src/nSuite/Commands/PackagerCommand.cpp index f2bbcba..e9b2501 100644 --- a/src/nSuite/Commands/PackagerCommand.cpp +++ b/src/nSuite/Commands/PackagerCommand.cpp @@ -45,9 +45,9 @@ void PackagerCommand::execute(const int & argc, char * argv[]) const // Compress the directory specified char * packBuffer(nullptr); - size_t packSize(0ull), fileCount(0ull); - if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, fileCount)) - exit_program("Cannot create installer from the directory specified, aborting...\r\n"); + size_t packSize(0ull), maxSize(0ull), fileCount(0ull); + if (!DRT::CompressDirectory(srcDirectory, &packBuffer, packSize, &maxSize, &fileCount)) + exit_program("Cannot create package from the directory specified, aborting...\r\n"); // Acquire installer resource Resource unpacker(IDR_UNPACKER, "UNPACKER"); @@ -71,7 +71,8 @@ void PackagerCommand::execute(const int & argc, char * argv[]) const // Output results TaskLogger::PushText( - "Files packaged: " + std::to_string(fileCount) + "\r\n" + - "Bytes packaged: " + std::to_string(packSize) + "\r\n" + "Files packaged: " + std::to_string(fileCount) + "\r\n" + + "Bytes packaged: " + std::to_string(maxSize) + "\r\n" + + "Compressed Size: " + std::to_string(packSize) + "\r\n" ); } \ No newline at end of file diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 091bb6e..2b8be39 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -45,7 +45,7 @@ void PatchCommand::execute(const int & argc, char * argv[]) const diffFile.read(diffBuffer, std::streamsize(diffSize)); diffFile.close(); size_t bytesWritten(0ull), instructionsUsed(0ull); - if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, bytesWritten, instructionsUsed)) + if (!DRT::PatchDirectory(dstDirectory, diffBuffer, diffSize, &bytesWritten, &instructionsUsed)) exit_program("aborting...\r\n"); delete[] diffBuffer; diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index 91b9613..914282c 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -46,8 +46,8 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const packFile.close(); // Unpackage using the resource file - size_t fileCount(0ull), byteCount(0ull); - if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, byteCount, fileCount)) + size_t byteCount(0ull), fileCount(0ull); + if (!DRT::DecompressDirectory(dstDirectory, packBuffer, packSize, &byteCount, &fileCount)) exit_program("Cannot decompress package file, aborting...\r\n"); delete[] packBuffer; From c280a68338795aad94a2c75701fb54dd8009a2a1 Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 17:03:23 -0500 Subject: [PATCH 42/44] Code cleanup, disk directory safetying Safetied paths chosen by user in regards to trailing slashes. --- src/Common.h | 19 +++++++------ src/DirectoryTools.cpp | 34 ++++++++++++------------ src/DirectoryTools.h | 2 +- src/Installer/Installer.cpp | 3 +-- src/Uninstaller/Uninstaller.cpp | 3 +-- src/Unpacker/Unpacker.cpp | 2 +- src/Updater/Updater.cpp | 2 +- src/nSuite/Commands/DiffCommand.cpp | 11 +++----- src/nSuite/Commands/InstallerCommand.cpp | 8 +++--- src/nSuite/Commands/PackCommand.cpp | 6 ++--- src/nSuite/Commands/PackagerCommand.cpp | 6 ++--- src/nSuite/Commands/PatchCommand.cpp | 7 +++-- src/nSuite/Commands/UnpackCommand.cpp | 8 ++++-- 13 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/Common.h b/src/Common.h index aa2cb65..3df359f 100644 --- a/src/Common.h +++ b/src/Common.h @@ -94,14 +94,17 @@ inline static void * PTR_ADD(void *const ptr, const size_t & offset) return static_cast(ptr) + offset; }; -/** Cleans up a target string representing a file path, removing leading and trailing quotes. -@param path reference to the path to be sanitized. */ -inline static void sanitize_path(std::string & path) +/** Cleans up a target string representing a file path +@param path reference to the path to be sanitized. +@return sanitized version of path. */ +inline static std::string sanitize_path(const std::string & path) { - if (path.front() == '"' || path.front() == '\'') - path.erase(0ull, 1ull); - if (path.back() == '"' || path.back() == '\'') - path.erase(path.size() - 1ull); + std::string cpy(path); + while (cpy.front() == '"' || cpy.front() == '\'' || cpy.front() == '\"' || cpy.front() == '\\') + cpy.erase(0ull, 1ull); + while (cpy.back() == '"' || cpy.back() == '\'' || cpy.back() == '\"' || cpy.back() == '\\') + cpy.erase(cpy.size() - 1ull); + return cpy; } /** Return file-info for all files within the directory specified. @@ -163,7 +166,7 @@ inline static void exit_program(const char * message) @param message pause message to show the user. */ inline static void pause_program(const char * message) { - TaskLogger::PushText(message + ' '); + TaskLogger::PushText(std::string(message) + ' '); system("pause"); std::printf("\033[A\33[2K\r"); std::printf("\033[A\33[2K\r\n"); diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index 483b956..bc6b7d5 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -24,12 +24,16 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer files.reserve(directoryArray.size()); // Get path name - const auto srcPath = std::filesystem::path(srcDirectory); - if (!std::filesystem::is_directory(srcPath) || !srcPath.has_stem()) { + if (!directoryArray.size()) { TaskLogger::PushText("Critical failure: the source path specified \"" + srcDirectory + "\" is not a (useable) directory.\r\n"); return false; } - const auto folderName = srcPath.stem().string(); + std::filesystem::path srcPath = std::filesystem::path(srcDirectory); + std::string folderName = srcPath.stem().string(); + while (folderName.empty()) { + srcPath = srcPath.parent_path(); + folderName = srcPath.stem().string(); + }; // Calculate final file size using all the files in this directory, // and make a list containing all relevant files and their attributes @@ -42,20 +46,16 @@ bool DRT::CompressDirectory(const std::string & srcDirectory, char ** packBuffer for each (const auto & excl in exclusions) { if (excl.empty()) continue; - if (excl[0] == '\\') { - // Compare Paths - if (path == excl) { - useEntry = false; - break; - } - } - else if (excl[0] == '.') { - // Compare Extensions - if (extension == excl) { - useEntry = false; - break; - } + // Compare Paths + if (path == excl) { + useEntry = false; + break; } + // Compare Extensions + else if (extension == excl) { + useEntry = false; + break; + } } if (useEntry) { const auto pathSize = path.size(); @@ -150,7 +150,7 @@ bool DRT::DecompressDirectory(const std::string & dstDirectory, char * packBuffe const auto folderSize = *reinterpret_cast(packBufferOffset); packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); const char * folderArray = reinterpret_cast(packBufferOffset); - const auto finalDestionation = dstDirectory + "\\" + std::string(folderArray, folderSize); + const auto finalDestionation = sanitize_path(dstDirectory + "\\" + std::string(folderArray, folderSize)); packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); char * decompressedBuffer(nullptr); diff --git a/src/DirectoryTools.h b/src/DirectoryTools.h index 25540f3..2050d23 100644 --- a/src/DirectoryTools.h +++ b/src/DirectoryTools.h @@ -19,7 +19,7 @@ namespace DRT { @param packSize reference updated with the size in bytes of the compressed packBuffer. @param byteCount (optional) pointer updated with the number of bytes written into the package @param fileCount (optional) pointer updated with the number of files written into the package. - @param exclusions (optional) list of filenames/types to skip. "\\string" match relative path, ".ext" match extension. + @param exclusions (optional) list of filenames/types to skip. "string" match relative path, ".ext" match extension. @return true if compression success, false otherwise. */ bool CompressDirectory(const std::string & srcDirectory, char ** packBuffer, size_t & packSize, size_t * byteCount = nullptr, size_t * fileCount = nullptr, const std::vector & exclusions = std::vector()); /** Decompresses an .npack - package formatted buffer into its component files in the destination directory. diff --git a/src/Installer/Installer.cpp b/src/Installer/Installer.cpp index 233eef1..fb88f1b 100644 --- a/src/Installer/Installer.cpp +++ b/src/Installer/Installer.cpp @@ -209,8 +209,7 @@ void Installer::beginInstallation() } else { // Unpackage using the rest of the resource file - auto directory = getDirectory(); - sanitize_path(directory); + auto directory = sanitize_path(getDirectory()); if (!DRT::DecompressDirectory(directory, reinterpret_cast(m_archive.getPtr()), m_archive.getSize())) invalidate(); else { diff --git a/src/Uninstaller/Uninstaller.cpp b/src/Uninstaller/Uninstaller.cpp index 6f42f9b..8111f3e 100644 --- a/src/Uninstaller/Uninstaller.cpp +++ b/src/Uninstaller/Uninstaller.cpp @@ -150,9 +150,8 @@ std::wstring Uninstaller::getDirectory() const void Uninstaller::beginUninstallation() { m_threader.addJob([&]() { - const auto directory = from_wideString(m_directory); - // Find all installed files + const auto directory = sanitize_path(from_wideString(m_directory)); const auto entries = get_file_paths(directory); // Find all shortcuts diff --git a/src/Unpacker/Unpacker.cpp b/src/Unpacker/Unpacker.cpp index 7bb316f..b86aa02 100644 --- a/src/Unpacker/Unpacker.cpp +++ b/src/Unpacker/Unpacker.cpp @@ -22,7 +22,7 @@ int main() // Acquire archive resource const auto start = std::chrono::system_clock::now(); - const std::string dstDirectory(get_current_directory()); + const auto dstDirectory = sanitize_path(get_current_directory()); size_t fileCount(0ull), byteCount(0ull); Resource archive(IDR_ARCHIVE, "ARCHIVE"); if (!archive.exists()) diff --git a/src/Updater/Updater.cpp b/src/Updater/Updater.cpp index 3933ca5..7a92e32 100644 --- a/src/Updater/Updater.cpp +++ b/src/Updater/Updater.cpp @@ -26,7 +26,7 @@ int main() }); // Find all patch files? - const auto dstDirectory(get_current_directory()); + const auto dstDirectory = sanitize_path(get_current_directory()); const auto patches = get_patches(dstDirectory); // Report an overview of supplied procedure diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 830db64..63101fa 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -22,11 +22,11 @@ void DiffCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-old=") - oldDirectory = std::string(&argv[x][5]); + oldDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-new=") - newDirectory = std::string(&argv[x][5]); + newDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -42,10 +42,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const const auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::tm bt; localtime_s(&bt, &time); - dstDirectory = - dstDirectory - + "\\" - + std::to_string(bt.tm_year) + std::to_string(bt.tm_mon) + std::to_string(bt.tm_mday) + std::to_string(bt.tm_hour) + std::to_string(bt.tm_min) + std::to_string(bt.tm_sec); + dstDirectory = sanitize_path(dstDirectory) + "\\" + std::to_string(bt.tm_year) + std::to_string(bt.tm_mon) + std::to_string(bt.tm_mday) + std::to_string(bt.tm_hour) + std::to_string(bt.tm_min) + std::to_string(bt.tm_sec); } // Ensure a file-extension is chosen diff --git a/src/nSuite/Commands/InstallerCommand.cpp b/src/nSuite/Commands/InstallerCommand.cpp index 25a1feb..9fa9ab3 100644 --- a/src/nSuite/Commands/InstallerCommand.cpp +++ b/src/nSuite/Commands/InstallerCommand.cpp @@ -23,9 +23,9 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -36,8 +36,8 @@ void InstallerCommand::execute(const int & argc, char * argv[]) const } // If user provides a directory only, append a filename - if (std::filesystem::is_directory(dstDirectory)) - dstDirectory += "\\installer.exe"; + if (std::filesystem::is_directory(dstDirectory)) + dstDirectory = sanitize_path(dstDirectory) + "\\installer.exe"; // Ensure a file-extension is chosen if (!std::filesystem::path(dstDirectory).has_extension()) diff --git a/src/nSuite/Commands/PackCommand.cpp b/src/nSuite/Commands/PackCommand.cpp index 1d442c5..b6b246d 100644 --- a/src/nSuite/Commands/PackCommand.cpp +++ b/src/nSuite/Commands/PackCommand.cpp @@ -22,9 +22,9 @@ void PackCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -36,7 +36,7 @@ void PackCommand::execute(const int & argc, char * argv[]) const // If user provides a directory only, append a filename if (std::filesystem::is_directory(dstDirectory)) - dstDirectory += "\\" + std::filesystem::path(srcDirectory).stem().generic_string() + ".npack"; + dstDirectory = sanitize_path(dstDirectory) + "\\" + std::filesystem::path(srcDirectory).stem().string() + ".npack"; // Ensure a file-extension is chosen if (!std::filesystem::path(dstDirectory).has_extension()) diff --git a/src/nSuite/Commands/PackagerCommand.cpp b/src/nSuite/Commands/PackagerCommand.cpp index e9b2501..11762c1 100644 --- a/src/nSuite/Commands/PackagerCommand.cpp +++ b/src/nSuite/Commands/PackagerCommand.cpp @@ -23,9 +23,9 @@ void PackagerCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -37,7 +37,7 @@ void PackagerCommand::execute(const int & argc, char * argv[]) const // If user provides a directory only, append a filename if (std::filesystem::is_directory(dstDirectory)) - dstDirectory += "\\package.exe"; + dstDirectory = sanitize_path(dstDirectory) + "\\package.exe"; // Ensure a file-extension is chosen if (!std::filesystem::path(dstDirectory).has_extension()) diff --git a/src/nSuite/Commands/PatchCommand.cpp b/src/nSuite/Commands/PatchCommand.cpp index 2b8be39..41d27d9 100644 --- a/src/nSuite/Commands/PatchCommand.cpp +++ b/src/nSuite/Commands/PatchCommand.cpp @@ -22,9 +22,9 @@ void PatchCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -33,6 +33,9 @@ void PatchCommand::execute(const int & argc, char * argv[]) const "\r\n" ); } + // Ensure a file-extension is chosen + if (!std::filesystem::path(dstDirectory).has_extension()) + dstDirectory += ".ndiff"; // Open diff file std::ifstream diffFile(srcDirectory, std::ios::binary | std::ios::beg); diff --git a/src/nSuite/Commands/UnpackCommand.cpp b/src/nSuite/Commands/UnpackCommand.cpp index 914282c..ab6d92e 100644 --- a/src/nSuite/Commands/UnpackCommand.cpp +++ b/src/nSuite/Commands/UnpackCommand.cpp @@ -22,9 +22,9 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const for (int x = 2; x < argc; ++x) { std::string command = string_to_lower(std::string(argv[x], 5)); if (command == "-src=") - srcDirectory = std::string(&argv[x][5]); + srcDirectory = sanitize_path(std::string(&argv[x][5])); else if (command == "-dst=") - dstDirectory = std::string(&argv[x][5]); + dstDirectory = sanitize_path(std::string(&argv[x][5])); else exit_program( " Arguments Expected:\r\n" @@ -34,6 +34,10 @@ void UnpackCommand::execute(const int & argc, char * argv[]) const ); } + // Ensure a file-extension is chosen + if (!std::filesystem::path(srcDirectory).has_extension()) + srcDirectory += ".npack"; + // Open pack file std::ifstream packFile(srcDirectory, std::ios::binary | std::ios::beg); const size_t packSize = std::filesystem::file_size(srcDirectory); From 99e90737ecb2ac2d2bc7eb724bd505f845dd07bb Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Mon, 22 Apr 2019 18:17:15 -0500 Subject: [PATCH 43/44] Fixed diffing against packages, added diffing against both applications with packages - Fixed diffing against packages - Added diffing against any applications that happen to have the archive/package format --- src/DirectoryTools.cpp | 94 ++++++++++++++++++----------- src/nSuite/Commands/DiffCommand.cpp | 2 +- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/DirectoryTools.cpp b/src/DirectoryTools.cpp index bc6b7d5..3ea696d 100644 --- a/src/DirectoryTools.cpp +++ b/src/DirectoryTools.cpp @@ -2,6 +2,7 @@ #include "BufferTools.h" #include "Common.h" #include "Instructions.h" +#include "Resource.h" #include "Threader.h" #include "TaskLogger.h" #include @@ -259,52 +260,75 @@ bool DRT::DiffDirectories(const std::string & oldDirectory, const std::string & size += srcFile.file_size(); files.push_back(new File(path, getRelativePath(path, directory), srcFile.file_size())); } + return true; + } + // See if the file is actually a program with an embedded archive + char * packBuffer(nullptr); + size_t packSize(0ull); + bool canLoad = LoadLibraryA(directory.c_str()); + auto handle = GetModuleHandle(directory.c_str()); + Resource fileResource(IDR_ARCHIVE, "ARCHIVE", handle); + if (canLoad && handle != NULL && fileResource.exists()) { + // Extract the archive + packBuffer = reinterpret_cast(fileResource.getPtr()); + packSize = fileResource.getSize(); } else { - // Treat as a snapshot file if it isn't a directory + // Last resort, treat as a snapshot file if it isn't a directory // Open diff file std::ifstream packFile(directory, std::ios::binary | std::ios::beg); - const size_t packSize = std::filesystem::file_size(directory); + packSize = std::filesystem::file_size(directory); if (!packFile.is_open()) { TaskLogger::PushText("Critical failure: cannot read package file.\r\n"); return false; } - char * compBuffer = new char[packSize]; - packFile.read(compBuffer, std::streamsize(packSize)); + packBuffer = new char[packSize]; + packFile.read(packBuffer, std::streamsize(packSize)); packFile.close(); + } + // We don't care about the package's header (folder name + name size) + char * packBufferOffset = packBuffer; + const auto folderSize = *reinterpret_cast(packBuffer); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, size_t(sizeof(size_t)))); + packBufferOffset = reinterpret_cast(PTR_ADD(packBufferOffset, folderSize)); + + // Decompress + size_t snapSize(0ull); + if (!BFT::DecompressBuffer(packBufferOffset, packSize - (size_t(sizeof(size_t)) + folderSize), snapshot, snapSize)) { + TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); + return false; + } - // Decompress - size_t snapSize(0ull); - if (!BFT::DecompressBuffer(compBuffer, packSize, snapshot, snapSize)) { - TaskLogger::PushText("Critical failure: cannot decompress package file.\r\n"); - return false; - } - delete[] compBuffer; - - // Get lists of all files involved - size_t bytesRead(0ull); - void * ptr = *snapshot; - while (bytesRead < snapSize) { - // Read the total number of characters from the path string, from the archive - const auto pathSize = *reinterpret_cast(ptr); - ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); - - // Read the file path string, from the archive - const char * path_array = reinterpret_cast(ptr); - const auto path = std::string(path_array, pathSize); - ptr = PTR_ADD(ptr, pathSize); - - // Read the file size in bytes, from the archive - const auto fileSize = *reinterpret_cast(ptr); - ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); - - files.push_back(new FileMem(path, path, fileSize, ptr)); - ptr = PTR_ADD(ptr, fileSize); - bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; - size += fileSize; - } + // Check if we need to delete the packBuffer, or if it is an embedded resource + // If it's an embedded resource, the fileResource's destructor will satisfy, else do below + if (!canLoad || handle == NULL || !fileResource.exists()) + delete[] packBuffer; + else if (canLoad && handle != NULL) + FreeLibrary(handle); + + // Get lists of all files involved + size_t bytesRead(0ull); + void * ptr = *snapshot; + while (bytesRead < snapSize) { + // Read the total number of characters from the path string, from the archive + const auto pathSize = *reinterpret_cast(ptr); + ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); + + // Read the file path string, from the archive + const char * path_array = reinterpret_cast(ptr); + const auto path = std::string(path_array, pathSize); + ptr = PTR_ADD(ptr, pathSize); + + // Read the file size in bytes, from the archive + const auto fileSize = *reinterpret_cast(ptr); + ptr = PTR_ADD(ptr, size_t(sizeof(size_t))); + + files.push_back(new FileMem(path, path, fileSize, ptr)); + ptr = PTR_ADD(ptr, fileSize); + bytesRead += size_t(sizeof(size_t)) + pathSize + size_t(sizeof(size_t)) + fileSize; + size += fileSize; } - return true; + return true; }; static constexpr auto getFileLists = [](const std::string & oldDirectory, char ** oldSnapshot, const std::string & newDirectory, char ** newSnapshot, size_t & reserveSize, PathPairList & commonFiles, PathList & addFiles, PathList & delFiles) { // Get files diff --git a/src/nSuite/Commands/DiffCommand.cpp b/src/nSuite/Commands/DiffCommand.cpp index 63101fa..0b585fa 100644 --- a/src/nSuite/Commands/DiffCommand.cpp +++ b/src/nSuite/Commands/DiffCommand.cpp @@ -53,7 +53,7 @@ void DiffCommand::execute(const int & argc, char * argv[]) const char * diffBuffer(nullptr); size_t diffSize(0ull), instructionCount(0ull); if (!DRT::DiffDirectories(oldDirectory, newDirectory, &diffBuffer, diffSize, &instructionCount)) - exit_program("aborting...\r\n"); + exit_program("Cannot diff the two paths chosen, aborting...\r\n"); // Create diff file std::filesystem::create_directories(std::filesystem::path(dstDirectory).parent_path()); From b28efc67f8a4f2f37e6e361a5c5c3c820a1fd9fd Mon Sep 17 00:00:00 2001 From: Troy Lowry Date: Tue, 23 Apr 2019 13:00:22 -0500 Subject: [PATCH 44/44] Added new icons Added new install and uninstall icons. They're not great, bit they're better than nothing, and probably better than using the package icon for all 3 --- src/Installer/icon.ico | Bin 32888 -> 50922 bytes src/Uninstaller/icon.ico | Bin 32888 -> 38922 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Installer/icon.ico b/src/Installer/icon.ico index 24e5ebbf3aa6312a3144ab637e7305ce87e54763..cf8fcecfb3c6515a8457cf4a3cef58f5b57d0da8 100644 GIT binary patch literal 50922 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 Op!Z_(Qq)NYtN1_W`7y`< diff --git a/src/Uninstaller/icon.ico b/src/Uninstaller/icon.ico index 24e5ebbf3aa6312a3144ab637e7305ce87e54763..ddd6009a3d40e1a0828f889256918004987bc048 100644 GIT binary patch literal 38922 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 Op!Z_(Qq)NYtN1_W`7y`<