mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## 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 | | :--- | :--- | :--- | | `<path>` | 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" ``` <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **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 <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
249 lines
6.5 KiB
C++
249 lines
6.5 KiB
C++
#include "pch.h"
|
|
#include "CLILogic.h"
|
|
#include <common/utils/json.h>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <chrono>
|
|
#include "resource.h"
|
|
#include <common/logger/logger.h>
|
|
#include <common/utils/logger_helper.h>
|
|
#include <type_traits>
|
|
|
|
template<typename T>
|
|
DWORD_PTR ToDwordPtr(T val)
|
|
{
|
|
if constexpr (std::is_pointer_v<T>)
|
|
{
|
|
return reinterpret_cast<DWORD_PTR>(val);
|
|
}
|
|
else
|
|
{
|
|
return static_cast<DWORD_PTR>(val);
|
|
}
|
|
}
|
|
|
|
template<typename... Args>
|
|
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<LPWSTR>(&buffer),
|
|
0,
|
|
reinterpret_cast<va_list*>(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<ProcessResult>& 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<ProcessResult>& 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<ProcessResult>& 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<std::wstring> 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<std::chrono::milliseconds>(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() };
|
|
}
|