From 03aec1a9a76a253dd57487a6e4642d24f942d6eb Mon Sep 17 00:00:00 2001 From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:59:37 +0800 Subject: [PATCH] [File Locksmith] Add CLI support (#44047) ## Summary of the Pull Request This pull request introduces a new command-line interface (CLI) project for the File Locksmith module, enabling users to interact with File Locksmith functionality directly from the command line. The changes include project and build configuration, CLI implementation, and supporting code to integrate with the existing FileLocksmith library. ## Commands and Options | Command / Option | Alias | Description | | :--- | :--- | :--- | | `` | N/A | **Required**. One or more file or directory paths to check. You can specify multiple paths separated by spaces. | | `--kill` | N/A | Terminates (kills) all processes that are currently locking the specified files. | | `--json` | N/A | Outputs the results in structured **JSON** format instead of human-readable text. Useful for automation and scripts. | | `--wait` | N/A | **Blocks execution** and waits until the specified files are released. The command will not exit until the files are unlocked. | | `--help` | N/A | Displays the help message with usage instructions. | ## Usage Examples ### 1. Basic check (Human-readable output) Check which processes are locking a specific file: ```powershell FileLocksmithCLI.exe "C:\Users\Docs\report.docx" ``` ### 2. Check multiple files and output JSON Check multiple files and get the output in JSON format for parsing: ```powershell FileLocksmithCLI.exe --json "C:\File1.txt" "C:\Folder\File2.dll" ``` ### 3. Wait for a file to be unlocked Block script execution until a file is released (useful in build scripts): ```powershell FileLocksmithCLI.exe --wait "C:\bin\output.exe" ``` ### 4. Force unlock a file Kill all processes that are locking a specific file: ```powershell FileLocksmithCLI.exe --kill "C:\LockedFile.dat" ``` ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Signed-off-by: Shawn Yuan (from Dev Box) --- .pipelines/ESRPSigning_core.json | 1 + PowerToys.slnx | 4 + .../FileLocksmithCLI/CLILogic.cpp | 248 ++++++++++++++++++ .../FileLocksmith/FileLocksmithCLI/CLILogic.h | 31 +++ .../FileLocksmithCLI/FileLocksmithCLI.rc | 19 ++ .../FileLocksmithCLI/FileLocksmithCLI.vcxproj | 119 +++++++++ .../FileLocksmithCLI.vcxproj.filters | 42 +++ .../FileLocksmith/FileLocksmithCLI/main.cpp | 71 +++++ .../FileLocksmithCLI/packages.config | 4 + .../FileLocksmith/FileLocksmithCLI/pch.cpp | 1 + .../FileLocksmith/FileLocksmithCLI/pch.h | 22 ++ .../FileLocksmith/FileLocksmithCLI/resource.h | 16 ++ .../tests/FileLocksmithCLITests.cpp | 130 +++++++++ .../tests/FileLocksmithCLIUnitTests.vcxproj | 84 ++++++ .../FileLocksmithCLI/tests/packages.config | 4 + .../FileLocksmithCLI/tests/pch.cpp | 1 + .../FileLocksmithCLI/tests/pch.h | 9 + .../FileLocksmithLib/FileLocksmith.h | 9 + .../FileLocksmithLib/FileLocksmithLib.vcxproj | 12 +- .../FileLocksmithLib.vcxproj.filters | 9 + .../FileLocksmithLib/ProcessResult.h | 12 + .../FileLocksmithLib/Settings.cpp | 1 + .../FileLocksmith/FileLocksmithLib/pch.h | 13 - .../FileLocksmithLibInterop/pch.h | 2 + 24 files changed, 846 insertions(+), 18 deletions(-) create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.rc create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj.filters create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/main.cpp create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/packages.config create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/pch.cpp create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/pch.h create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/resource.h create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/tests/packages.config create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.cpp create mode 100644 src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.h create mode 100644 src/modules/FileLocksmith/FileLocksmithLib/FileLocksmith.h create mode 100644 src/modules/FileLocksmith/FileLocksmithLib/ProcessResult.h delete mode 100644 src/modules/FileLocksmith/FileLocksmithLib/pch.h diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index b417597184..72840bebbd 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -117,6 +117,7 @@ "WinUI3Apps\\PowerToys.FileLocksmithUI.dll", "WinUI3Apps\\PowerToys.FileLocksmithContextMenu.dll", "FileLocksmithContextMenuPackage.msix", + "FileLocksmithCLI.exe", "WinUI3Apps\\Peek.Common.dll", "WinUI3Apps\\Peek.FilePreviewer.dll", diff --git a/PowerToys.slnx b/PowerToys.slnx index 8bcadb8f1c..ac006fdbf4 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -420,6 +420,7 @@ + @@ -429,6 +430,9 @@ + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp new file mode 100644 index 0000000000..17015e1ea3 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp @@ -0,0 +1,248 @@ +#include "pch.h" +#include "CLILogic.h" +#include +#include +#include +#include +#include "resource.h" +#include +#include +#include + +template +DWORD_PTR ToDwordPtr(T val) +{ + if constexpr (std::is_pointer_v) + { + return reinterpret_cast(val); + } + else + { + return static_cast(val); + } +} + +template +std::wstring FormatString(IStringProvider& strings, UINT id, Args... args) +{ + std::wstring format = strings.GetString(id); + if (format.empty()) return L""; + + DWORD_PTR arguments[] = { ToDwordPtr(args)..., 0 }; + + LPWSTR buffer = nullptr; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY, + format.c_str(), + 0, + 0, + reinterpret_cast(&buffer), + 0, + reinterpret_cast(arguments)); + + if (buffer) + { + std::wstring result(buffer); + LocalFree(buffer); + return result; + } + return L""; +} + +std::wstring get_usage(IStringProvider& strings) +{ + return strings.GetString(IDS_USAGE); +} + +std::wstring get_json(const std::vector& results) +{ + json::JsonObject root; + json::JsonArray processes; + + for (const auto& result : results) + { + json::JsonObject process; + process.SetNamedValue(L"pid", json::JsonValue::CreateNumberValue(result.pid)); + process.SetNamedValue(L"name", json::JsonValue::CreateStringValue(result.name)); + process.SetNamedValue(L"user", json::JsonValue::CreateStringValue(result.user)); + + json::JsonArray files; + for (const auto& file : result.files) + { + files.Append(json::JsonValue::CreateStringValue(file)); + } + process.SetNamedValue(L"files", files); + + processes.Append(process); + } + + root.SetNamedValue(L"processes", processes); + return root.Stringify().c_str(); +} + +std::wstring get_text(const std::vector& results, IStringProvider& strings) +{ + std::wstringstream ss; + if (results.empty()) + { + ss << strings.GetString(IDS_NO_PROCESSES); + return ss.str(); + } + + ss << strings.GetString(IDS_HEADER); + for (const auto& result : results) + { + ss << result.pid << L"\t" + << result.user << L"\t" + << result.name << std::endl; + } + return ss.str(); +} + +std::wstring kill_processes(const std::vector& results, IProcessTerminator& terminator, IStringProvider& strings) +{ + std::wstringstream ss; + for (const auto& result : results) + { + if (terminator.terminate(result.pid)) + { + ss << FormatString(strings, IDS_TERMINATED, result.pid, result.name.c_str()); + } + else + { + ss << FormatString(strings, IDS_FAILED_TERMINATE, result.pid, result.name.c_str()); + } + } + return ss.str(); +} + +CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IProcessTerminator& terminator, IStringProvider& strings) +{ + Logger::info("Parsing arguments"); + if (argc < 2) + { + Logger::warn("No arguments provided"); + return { 1, get_usage(strings) }; + } + + bool json_output = false; + bool kill = false; + bool wait = false; + int timeout_ms = -1; + std::vector paths; + + for (int i = 1; i < argc; ++i) + { + std::wstring arg = argv[i]; + if (arg == L"--json") + { + json_output = true; + } + else if (arg == L"--kill") + { + kill = true; + } + else if (arg == L"--wait") + { + wait = true; + } + else if (arg == L"--timeout") + { + if (i + 1 < argc) + { + try + { + timeout_ms = std::stoi(argv[++i]); + } + catch (...) + { + Logger::error("Invalid timeout value"); + return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT) }; + } + } + else + { + Logger::error("Timeout argument missing"); + return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG) }; + } + } + else if (arg == L"--help") + { + return { 0, get_usage(strings) }; + } + else + { + paths.push_back(arg); + } + } + + if (paths.empty()) + { + Logger::error("No paths specified"); + return { 1, strings.GetString(IDS_ERROR_NO_PATHS) }; + } + + Logger::info("Processing {} paths", paths.size()); + + if (wait) + { + std::wstringstream ss; + if (json_output) + { + Logger::warn("Wait is incompatible with JSON output"); + ss << strings.GetString(IDS_WARN_JSON_WAIT); + json_output = false; + } + + ss << strings.GetString(IDS_WAITING); + auto start_time = std::chrono::steady_clock::now(); + while (true) + { + auto results = finder.find(paths); + if (results.empty()) + { + Logger::info("Files unlocked"); + ss << strings.GetString(IDS_UNLOCKED); + break; + } + + if (timeout_ms >= 0) + { + auto current_time = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(current_time - start_time).count(); + if (elapsed > timeout_ms) + { + Logger::warn("Timeout waiting for files to be unlocked"); + ss << strings.GetString(IDS_TIMEOUT); + return { 1, ss.str() }; + } + } + + Sleep(200); + } + return { 0, ss.str() }; + } + + auto results = finder.find(paths); + Logger::info("Found {} processes locking the files", results.size()); + std::wstringstream output_ss; + + if (kill) + { + Logger::info("Killing processes"); + output_ss << kill_processes(results, terminator, strings); + // Re-check after killing + results = finder.find(paths); + Logger::info("Remaining processes: {}", results.size()); + } + + if (json_output) + { + output_ss << get_json(results) << std::endl; + } + else + { + output_ss << get_text(results, strings); + } + + return { 0, output_ss.str() }; +} diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h new file mode 100644 index 0000000000..c8f519592f --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include "FileLocksmithLib/FileLocksmith.h" +#include + +struct CommandResult +{ + int exit_code; + std::wstring output; +}; + +struct IProcessFinder +{ + virtual std::vector find(const std::vector& paths) = 0; + virtual ~IProcessFinder() = default; +}; + +struct IProcessTerminator +{ + virtual bool terminate(DWORD pid) = 0; + virtual ~IProcessTerminator() = default; +}; + +struct IStringProvider +{ + virtual std::wstring GetString(UINT id) = 0; + virtual ~IStringProvider() = default; +}; + +CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IProcessTerminator& terminator, IStringProvider& strings); diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.rc b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.rc new file mode 100644 index 0000000000..641c19fb49 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.rc @@ -0,0 +1,19 @@ +#include "resource.h" +#include + +STRINGTABLE +BEGIN + IDS_USAGE "Usage: FileLocksmithCLI.exe [options] [path2] ...\nOptions:\n --kill Kill processes locking the files\n --json Output results in JSON format\n --wait Wait for files to be unlocked\n --timeout Timeout in milliseconds for --wait\n --help Show this help message\n" + IDS_NO_PROCESSES "No processes found locking the file(s).\n" + IDS_HEADER "PID\tUser\tProcess\n" + IDS_TERMINATED "Terminated process %1!d! (%2)\n" + IDS_FAILED_TERMINATE "Failed to terminate process %1!d! (%2)\n" + IDS_FAILED_OPEN "Failed to open process %1!d! (%2)\n" + IDS_ERROR_NO_PATHS "Error: No paths specified.\n" + IDS_WARN_JSON_WAIT "Warning: --wait is incompatible with --json. Ignoring --json.\n" + IDS_WAITING "Waiting for files to be unlocked...\n" + IDS_UNLOCKED "Files unlocked.\n" + IDS_TIMEOUT "Timeout waiting for files to be unlocked.\n" + IDS_ERROR_INVALID_TIMEOUT "Error: Invalid timeout value.\n" + IDS_ERROR_TIMEOUT_ARG "Error: --timeout requires an argument.\n" +END diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj new file mode 100644 index 0000000000..ca85b58d28 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj @@ -0,0 +1,119 @@ + + + + + 17.0 + Win32Proj + {49D456D3-F485-45AF-8875-45B44F193DDC} + FileLocksmithCLI + 10.0 + FileLocksmithCLI + + + + + Application + true + v143 + Unicode + + + Application + false + v143 + Unicode + + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration) + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + false + $(ProjectDir)..;$(ProjectDir)..\..\..;$(ProjectDir)..\..;%(AdditionalIncludeDirectories) + Use + pch.h + false + + + Console + true + shlwapi.lib;ntdll.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + false + $(ProjectDir)..;$(ProjectDir)..\..\..;$(ProjectDir)..\..;%(AdditionalIncludeDirectories) + Use + pch.h + false + + + Console + true + true + true + shlwapi.lib;ntdll.lib;%(AdditionalDependencies) + + + + + + + Create + + + + + + + + + + + + + {9d52fd25-ef90-4f9a-a015-91efc5daf54f} + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {1248566c-272a-43c0-88d6-e6675d569a09} + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj.filters b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj.filters new file mode 100644 index 0000000000..e8a641d95d --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp new file mode 100644 index 0000000000..67a4304b4e --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp @@ -0,0 +1,71 @@ +#include "pch.h" +#include "CLILogic.h" +#include "FileLocksmithLib/FileLocksmith.h" +#include +#include "resource.h" +#include +#include + +struct RealProcessFinder : IProcessFinder +{ + std::vector find(const std::vector& paths) override + { + return find_processes_recursive(paths); + } +}; + +struct RealProcessTerminator : IProcessTerminator +{ + bool terminate(DWORD pid) override + { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (hProcess) + { + bool result = TerminateProcess(hProcess, 0); + CloseHandle(hProcess); + return result; + } + return false; + } +}; + +struct RealStringProvider : IStringProvider +{ + std::wstring GetString(UINT id) override + { + wchar_t buffer[4096]; + int len = LoadStringW(GetModuleHandle(NULL), id, buffer, ARRAYSIZE(buffer)); + if (len > 0) + { + return std::wstring(buffer, len); + } + return L""; + } +}; + +#ifndef UNIT_TEST +int wmain(int argc, wchar_t* argv[]) +{ + winrt::init_apartment(); + LoggerHelpers::init_logger(L"FileLocksmithCLI", L"", LogSettings::fileLocksmithLoggerName); + Logger::info("FileLocksmithCLI started"); + + RealProcessFinder finder; + RealProcessTerminator terminator; + RealStringProvider strings; + + auto result = run_command(argc, argv, finder, terminator, strings); + + if (result.exit_code != 0) + { + Logger::error("Command failed with exit code {}", result.exit_code); + } + else + { + Logger::info("Command succeeded"); + } + + std::wcout << result.output; + return result.exit_code; +} +#endif diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/packages.config b/src/modules/FileLocksmith/FileLocksmithCLI/packages.config new file mode 100644 index 0000000000..2e5039eb82 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/pch.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/pch.cpp new file mode 100644 index 0000000000..1d9f38c57d --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/pch.h b/src/modules/FileLocksmith/FileLocksmithCLI/pch.h new file mode 100644 index 0000000000..6099342b41 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/pch.h @@ -0,0 +1,22 @@ +#pragma once + +#ifndef PCH_H +#define PCH_H + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#endif // PCH_H diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/resource.h b/src/modules/FileLocksmith/FileLocksmithCLI/resource.h new file mode 100644 index 0000000000..be12cac3ac --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/resource.h @@ -0,0 +1,16 @@ +// resource.h +#pragma once + +#define IDS_USAGE 101 +#define IDS_NO_PROCESSES 102 +#define IDS_HEADER 103 +#define IDS_TERMINATED 104 +#define IDS_FAILED_TERMINATE 105 +#define IDS_FAILED_OPEN 106 +#define IDS_ERROR_NO_PATHS 107 +#define IDS_WARN_JSON_WAIT 108 +#define IDS_WAITING 109 +#define IDS_UNLOCKED 110 +#define IDS_TIMEOUT 111 +#define IDS_ERROR_INVALID_TIMEOUT 112 +#define IDS_ERROR_TIMEOUT_ARG 113 diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp new file mode 100644 index 0000000000..a67e42badf --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp @@ -0,0 +1,130 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "../CLILogic.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace FileLocksmithCLIUnitTests +{ + struct MockProcessFinder : IProcessFinder + { + std::vector results; + std::vector find(const std::vector& paths) override + { + (void)paths; + return results; + } + }; + + struct MockProcessTerminator : IProcessTerminator + { + bool shouldSucceed = true; + std::vector terminatedPids; + bool terminate(DWORD pid) override + { + terminatedPids.push_back(pid); + return shouldSucceed; + } + }; + + struct MockStringProvider : IStringProvider + { + std::map strings; + std::wstring GetString(UINT id) override + { + if (strings.count(id)) return strings[id]; + return L"String_" + std::to_wstring(id); + } + }; + + TEST_CLASS(CLITests) + { + public: + + TEST_METHOD(TestNoArgs) + { + MockProcessFinder finder; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe" }; + auto result = run_command(1, argv, finder, terminator, strings); + + Assert::AreEqual(1, result.exit_code); + } + + TEST_METHOD(TestHelp) + { + MockProcessFinder finder; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"--help" }; + auto result = run_command(2, argv, finder, terminator, strings); + + Assert::AreEqual(0, result.exit_code); + } + + TEST_METHOD(TestFindProcesses) + { + MockProcessFinder finder; + finder.results = { { L"process", 123, L"user", { L"file1" } } }; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1" }; + auto result = run_command(2, argv, finder, terminator, strings); + + Assert::AreEqual(0, result.exit_code); + Assert::IsTrue(result.output.find(L"123") != std::wstring::npos); + Assert::IsTrue(result.output.find(L"process") != std::wstring::npos); + } + + TEST_METHOD(TestJsonOutput) + { + MockProcessFinder finder; + finder.results = { { L"process", 123, L"user", { L"file1" } } }; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--json" }; + auto result = run_command(3, argv, finder, terminator, strings); + + Microsoft::VisualStudio::CppUnitTestFramework::Logger::WriteMessage(result.output.c_str()); + + Assert::AreEqual(0, result.exit_code); + Assert::IsTrue(result.output.find(L"\"pid\"") != std::wstring::npos); + Assert::IsTrue(result.output.find(L"123") != std::wstring::npos); + } + + TEST_METHOD(TestKill) + { + MockProcessFinder finder; + finder.results = { { L"process", 123, L"user", { L"file1" } } }; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--kill" }; + auto result = run_command(3, argv, finder, terminator, strings); + + Assert::AreEqual(0, result.exit_code); + Assert::AreEqual((size_t)1, terminator.terminatedPids.size()); + Assert::AreEqual((DWORD)123, terminator.terminatedPids[0]); + } + + TEST_METHOD(TestTimeout) + { + MockProcessFinder finder; + // Always return results so it waits + finder.results = { { L"process", 123, L"user", { L"file1" } } }; + MockProcessTerminator terminator; + MockStringProvider strings; + + wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--wait", (wchar_t*)L"--timeout", (wchar_t*)L"100" }; + auto result = run_command(5, argv, finder, terminator, strings); + + Assert::AreEqual(1, result.exit_code); + } + }; +} diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj new file mode 100644 index 0000000000..4b423c0183 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj @@ -0,0 +1,84 @@ + + + + + {A1B2C3D4-E5F6-7890-1234-567890ABCDEF} + Win32Proj + FileLocksmithCLIUnitTests + FileLocksmithCLI.UnitTests + 10.0 + + + + + v143 + DynamicLibrary + Unicode + + + + + + + + + + + + ..\..\..\..\..\$(Platform)\$(Configuration)\tests\FileLocksmithCLI\ + + + + ..\;..\..\;..\..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;UNIT_TEST;%(PreprocessorDefinitions) + true + Use + pch.h + 26466;%(DisableSpecificWarnings) + + + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + shlwapi.lib;ntdll.lib;%(AdditionalDependencies) + + + + + + + + Create + + + + NotUsing + + + + + {9d52fd25-ef90-4f9a-a015-91efc5daf54f} + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {1248566c-272a-43c0-88d6-e6675d569a09} + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/packages.config b/src/modules/FileLocksmith/FileLocksmithCLI/tests/packages.config new file mode 100644 index 0000000000..2e5039eb82 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.cpp new file mode 100644 index 0000000000..1d9f38c57d --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.h b/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.h new file mode 100644 index 0000000000..7041294e28 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/pch.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "CppUnitTest.h" diff --git a/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmith.h b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmith.h new file mode 100644 index 0000000000..087df84a68 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmith.h @@ -0,0 +1,9 @@ +#pragma once + +#include "ProcessResult.h" + +// Second version, checks handles towards files and all subfiles and folders of given dirs, if any. +std::vector find_processes_recursive(const std::vector& paths); + +// Gives the full path of the executable, given the process id +std::wstring pid_to_full_path(DWORD pid); diff --git a/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj index ebbeb20895..fdfb0c666f 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj +++ b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj @@ -34,9 +34,9 @@ Level3 true - WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + WIN32;_DEBUG;_LIB;FILELOCKSMITH_LIB_STATIC;%(PreprocessorDefinitions) true - ../../..;../..; + ..\FileLocksmithLibInterop;../../..;../..; @@ -50,9 +50,9 @@ true true true - WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + WIN32;NDEBUG;_LIB;FILELOCKSMITH_LIB_STATIC;%(PreprocessorDefinitions) true - ../../..;../..; + ..\FileLocksmithLibInterop;../../..;../..; @@ -68,13 +68,15 @@ - + + + Create diff --git a/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj.filters b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj.filters index ed6b2674fc..c38c9b2d2e 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj.filters +++ b/src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj.filters @@ -38,6 +38,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + Source Files diff --git a/src/modules/FileLocksmith/FileLocksmithLib/ProcessResult.h b/src/modules/FileLocksmith/FileLocksmithLib/ProcessResult.h new file mode 100644 index 0000000000..614e38a209 --- /dev/null +++ b/src/modules/FileLocksmith/FileLocksmithLib/ProcessResult.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include + +struct ProcessResult +{ + std::wstring name; + DWORD pid; + std::wstring user; + std::vector files; +}; diff --git a/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp b/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp index de997144ca..c30387df5d 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp +++ b/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp @@ -2,6 +2,7 @@ #include "Settings.h" #include "Constants.h" +#include #include #include diff --git a/src/modules/FileLocksmith/FileLocksmithLib/pch.h b/src/modules/FileLocksmith/FileLocksmithLib/pch.h deleted file mode 100644 index 885d5d62e4..0000000000 --- a/src/modules/FileLocksmith/FileLocksmithLib/pch.h +++ /dev/null @@ -1,13 +0,0 @@ -// pch.h: This is a precompiled header file. -// Files listed below are compiled only once, improving build performance for future builds. -// This also affects IntelliSense performance, including code completion and many code browsing features. -// However, files listed here are ALL re-compiled if any one of them is updated between builds. -// Do not add files here that you will be updating frequently as this negates the performance advantage. - -#ifndef PCH_H -#define PCH_H - -// add headers that you want to pre-compile here -#include "framework.h" - -#endif //PCH_H diff --git a/src/modules/FileLocksmith/FileLocksmithLibInterop/pch.h b/src/modules/FileLocksmith/FileLocksmithLibInterop/pch.h index f2449b1578..4f47976891 100644 --- a/src/modules/FileLocksmith/FileLocksmithLibInterop/pch.h +++ b/src/modules/FileLocksmith/FileLocksmithLibInterop/pch.h @@ -18,4 +18,6 @@ #include #include +#ifndef FILELOCKSMITH_LIB_STATIC #include +#endif