Compare commits

...

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
43b2b4d5da Improve pipe-based IPC with better error handling and timeouts
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-08-29 07:18:40 +00:00
copilot-swe-agent[bot]
5c48124e4c Implement pipe-based IPC for File Locksmith to fix Russian К character issue
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-08-29 07:14:52 +00:00
copilot-swe-agent[bot]
98063b17a7 Initial plan 2025-08-29 07:00:37 +00:00
Copilot
eb35b3a249 Fix grammatical error in Awake taskbar context menu: "1 hours" → "1 hour" (#41454)
This PR fixes a grammatical mistake in the PowerToys Awake taskbar
context menu where "1 hours" was displayed instead of the correct "1
hour".

## Problem
When right-clicking the Awake icon in the taskbar and hovering over
"Keep awake on interval", the menu incorrectly showed "1 hours" for the
one-hour option, which is grammatically incorrect in English.

![Before fix showing "1
hours"](https://github.com/user-attachments/assets/bd78b3b1-d076-4c84-8de0-bcd6d1ebefd8)

## Root Cause
The code always used the `AWAKE_HOURS` resource string (`"{0} hours"`)
regardless of the value, even when the count was 1. This resulted in
grammatically incorrect text like "1 hours".

## Solution
Added proper singular/plural handling by:

1. **Added new singular resources:**
   - `AWAKE_HOUR`: `"{0} hour"` for singular form
   - `AWAKE_MINUTE`: `"{0} minute"` for completeness and future-proofing

2. **Updated the logic in `Manager.cs`:**
- Modified `GetDefaultTrayOptions()` to use `AwakeHour` (singular) when
the value is 1
- Preserved existing behavior for all other values (30 minutes, 2 hours,
etc.)

3. **Generated corresponding code in `Resources.Designer.cs`** to expose
the new resource properties

## Impact
-  "1 hours" → "1 hour" (grammatically correct)
-  "2 hours" remains unchanged (still correct)
-  "30 minutes" behavior preserved
-  No breaking changes to existing functionality
-  Future-proofed for potential 1-minute custom intervals

The fix follows established patterns in the PowerToys codebase (similar
to `TimeRemainingConverter.cs` in ImageResizer) and makes minimal,
surgical changes to address only the reported issue.

Fixes #41220.

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/modules/awake/Awake/Awake.csproj` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-08-29 13:36:46 +08:00
Jiří Polášek
5daec13bc4 CmdPal | Bug Report Tool: Allow collection of Command Palette events from Windows Event Logs by Bug Report Tool (#41400)
## Summary of the Pull Request

Adds `Microsoft.CmdPal.UI.exe` to the list of processes, so Bug Report
Tool can pick Applications Logs for it when generating its report.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41399
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **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
2025-08-28 12:48:30 -05:00
12 changed files with 311 additions and 9 deletions

View File

@@ -117,7 +117,11 @@ public:
HRESULT result;
if (!RunNonElevatedEx(path.c_str(), L"", get_module_folderpath(g_hInst)))
// Get pipe name from writer if using pipes
std::wstring pipe_name = writer.get_pipe_name();
std::wstring command_args = pipe_name.empty() ? L"" : pipe_name;
if (!RunNonElevatedEx(path.c_str(), command_args.c_str(), get_module_folderpath(g_hInst)))
{
result = E_FAIL;
Trace::InvokedRet(result);

View File

@@ -287,7 +287,17 @@ HRESULT ExplorerCommand::LaunchUI(CMINVOKECOMMANDINFO* pici, ipc::Writer* writer
PROCESS_INFORMATION processInformation;
std::wstring command_line = L"\"";
command_line += exe_path;
command_line += L"\"\0";
command_line += L"\"";
// Add pipe name as command line argument if using pipes
std::wstring pipe_name = writer->get_pipe_name();
if (!pipe_name.empty())
{
command_line += L" ";
command_line += pipe_name;
}
command_line += L"\0";
CreateProcessW(
NULL,

View File

@@ -4,13 +4,17 @@
#include "Constants.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/logger_helper.h>
#include <thread>
#include <sstream>
#include <rpc.h>
constexpr DWORD DefaultPipeBufferSize = 8192;
constexpr DWORD DefaultPipeTimeoutMillis = 200;
namespace ipc
{
Writer::Writer()
Writer::Writer() : m_pipe_handle(INVALID_HANDLE_VALUE), m_use_pipes(true)
{
start();
}
@@ -22,6 +26,32 @@ namespace ipc
HRESULT Writer::start()
{
// Try to use pipes first, fall back to file-based IPC if needed
if (m_use_pipes)
{
// Generate unique pipe name similar to PowerRename
UUID temp_uuid;
wchar_t* uuid_chars = nullptr;
if (UuidCreate(&temp_uuid) == RPC_S_OK &&
UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) == RPC_S_OK)
{
m_pipe_name = L"\\\\.\\pipe\\powertoys_filelocksmith_input_";
m_pipe_name += uuid_chars;
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
HRESULT hr = create_pipe_server();
if (SUCCEEDED(hr))
{
return hr;
}
}
// If pipe creation failed, fall back to file-based IPC
m_use_pipes = false;
}
// File-based IPC fallback
std::wstring path = PTSettingsHelper::get_module_save_folder_location(constants::nonlocalizable::PowerToyName);
path += L"\\";
path += constants::nonlocalizable::LastRunPath;
@@ -37,8 +67,70 @@ namespace ipc
}
}
HRESULT Writer::create_pipe_server()
{
m_pipe_handle = CreateNamedPipe(
m_pipe_name.c_str(),
PIPE_ACCESS_DUPLEX | WRITE_DAC,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
DefaultPipeBufferSize,
DefaultPipeBufferSize,
DefaultPipeTimeoutMillis,
NULL);
if (m_pipe_handle == NULL || m_pipe_handle == INVALID_HANDLE_VALUE)
{
return E_FAIL;
}
start_pipe_server_thread();
return S_OK;
}
void Writer::start_pipe_server_thread()
{
m_pipe_thread = std::thread([this]() {
// This call blocks until a client process connects to the pipe
BOOL connected = ConnectNamedPipe(m_pipe_handle, NULL);
if (!connected)
{
DWORD error = GetLastError();
if (error != ERROR_PIPE_CONNECTED)
{
// Connection failed
CloseHandle(m_pipe_handle);
m_pipe_handle = INVALID_HANDLE_VALUE;
}
// ERROR_PIPE_CONNECTED means client connected before ConnectNamedPipe was called
}
});
}
HRESULT Writer::add_path(LPCWSTR path)
{
if (m_use_pipes && m_pipe_handle != INVALID_HANDLE_VALUE)
{
// Wait for pipe connection to be established
if (m_pipe_thread.joinable())
{
m_pipe_thread.join();
}
if (m_pipe_handle != INVALID_HANDLE_VALUE)
{
// Create delimited path string
std::wstring delimited_path = path;
delimited_path += L"?"; // Use '?' as delimiter like PowerRename
DWORD path_length_bytes = static_cast<DWORD>(delimited_path.length() * sizeof(WCHAR));
DWORD bytes_written;
BOOL result = WriteFile(m_pipe_handle, delimited_path.c_str(), path_length_bytes, &bytes_written, NULL);
return result ? S_OK : E_FAIL;
}
}
// File-based IPC fallback
int length = lstrlenW(path);
if (!m_stream.write(reinterpret_cast<const char*>(path), length * sizeof(WCHAR)))
{
@@ -56,7 +148,24 @@ namespace ipc
void Writer::finish()
{
add_path(L"");
m_stream.close();
if (m_use_pipes)
{
if (m_pipe_thread.joinable())
{
m_pipe_thread.join();
}
if (m_pipe_handle != INVALID_HANDLE_VALUE)
{
CloseHandle(m_pipe_handle);
m_pipe_handle = INVALID_HANDLE_VALUE;
}
}
else
{
// File-based IPC
add_path(L"");
m_stream.close();
}
}
}

View File

@@ -3,6 +3,8 @@
#include "pch.h"
#include <fstream>
#include <thread>
#include <string>
namespace ipc
{
@@ -15,8 +17,16 @@ namespace ipc
HRESULT add_path(LPCWSTR path);
void finish();
HANDLE get_read_handle();
std::wstring get_pipe_name() const { return m_pipe_name; }
private:
std::ofstream m_stream;
HRESULT create_pipe_server();
void start_pipe_server_thread();
std::ofstream m_stream; // Keep for backwards compatibility
HANDLE m_pipe_handle;
std::wstring m_pipe_name;
std::thread m_pipe_thread;
bool m_use_pipes;
};
}

View File

@@ -2,6 +2,7 @@
#include "NativeMethods.h"
#include "FileLocksmith.h"
#include "../FileLocksmithLib/Constants.h"
#include <sstream>
namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
{
@@ -95,6 +96,99 @@ namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
}
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
}
com_array<hstring> NativeMethods::ReadPathsFromPipe(hstring const& pipeName)
{
std::vector<std::wstring> result_cpp;
if (pipeName.empty())
{
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
}
std::wstring pipe_name_str = pipeName.c_str();
HANDLE hPipe = INVALID_HANDLE_VALUE;
// Try to connect to the pipe for up to 5 seconds
for (int attempts = 0; attempts < 50 && hPipe == INVALID_HANDLE_VALUE; attempts++)
{
hPipe = CreateFile(
pipe_name_str.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hPipe != INVALID_HANDLE_VALUE)
break;
if (GetLastError() != ERROR_PIPE_BUSY)
break;
if (!WaitNamedPipe(pipe_name_str.c_str(), 100))
break;
}
if (hPipe == INVALID_HANDLE_VALUE)
{
// Fall back to file-based reading
return ReadPathsFromFile();
}
// Set read timeout to prevent hanging
COMMTIMEOUTS timeouts = { 0 };
timeouts.ReadIntervalTimeout = 100;
timeouts.ReadTotalTimeoutConstant = 3000; // 3 second timeout
timeouts.ReadTotalTimeoutMultiplier = 0;
SetCommTimeouts(hPipe, &timeouts);
// Read from pipe - collect all data first
const DWORD BUFSIZE = 4096;
std::wstring allData;
WCHAR chBuf[BUFSIZE];
DWORD dwRead;
BOOL bSuccess;
// Read with timeout protection
int read_attempts = 0;
const int max_read_attempts = 30; // Maximum 30 read attempts
for (;;)
{
bSuccess = ReadFile(hPipe, chBuf, BUFSIZE * sizeof(WCHAR), &dwRead, NULL);
if (!bSuccess || dwRead == 0 || ++read_attempts > max_read_attempts)
break;
// Append to accumulated data
allData.append(chBuf, dwRead / sizeof(WCHAR));
if (!bSuccess)
break;
}
CloseHandle(hPipe);
// Parse all data using delimiter
if (!allData.empty())
{
std::wstringstream ss(allData);
std::wstring item;
wchar_t delimiter = L'?';
while (std::getline(ss, item, delimiter))
{
if (!item.empty())
{
result_cpp.push_back(item);
}
}
}
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
}
bool NativeMethods::StartAsElevated(array_view<hstring const> paths)
{

View File

@@ -10,6 +10,7 @@ namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
static com_array<winrt::PowerToys::FileLocksmithLib::Interop::ProcessResult> FindProcessesRecursive(array_view<hstring const> paths);
static hstring PidToFullPath(uint32_t pid);
static com_array<hstring> ReadPathsFromFile();
static com_array<hstring> ReadPathsFromPipe(hstring const& pipeName);
static bool StartAsElevated(array_view<hstring const> paths);
static bool SetDebugPrivilege();
static bool IsProcessElevated();

View File

@@ -16,6 +16,8 @@ namespace FileLocksmithUI
/// </summary>
public partial class App : Application
{
public static string[] CommandLineArgs { get; private set; } = Array.Empty<string>();
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code
@@ -43,6 +45,10 @@ namespace FileLocksmithUI
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Store command line arguments for access by other classes
string[] commandArgs = Environment.GetCommandLineArgs();
CommandLineArgs = commandArgs.Length > 1 ? commandArgs.Skip(1).ToArray() : Array.Empty<string>();
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredFileLocksmithEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");

View File

@@ -79,8 +79,47 @@ namespace PowerToys.FileLocksmithUI.ViewModels
public MainViewModel()
{
paths = NativeMethods.ReadPathsFromFile();
Logger.LogInfo($"Starting FileLocksmith with {paths.Length} files selected.");
// Check if pipe name was passed as command line argument
string[] args = FileLocksmithUI.App.CommandLineArgs;
string pipeName = null;
// Look for pipe name in command line arguments
if (args != null && args.Length > 0)
{
foreach (string arg in args)
{
if (arg.StartsWith(@"\\.\pipe\"))
{
pipeName = arg;
break;
}
}
}
// Try to read from pipe first, fall back to file if needed
if (!string.IsNullOrEmpty(pipeName))
{
try
{
Logger.LogInfo($"Attempting to read from pipe: {pipeName}");
paths = NativeMethods.ReadPathsFromPipe(pipeName);
Logger.LogInfo($"Successfully read {paths.Length} files from pipe.");
}
catch (Exception ex)
{
Logger.LogError($"Failed to read from pipe {pipeName}: {ex.Message}");
Logger.LogInfo("Falling back to file-based IPC.");
paths = NativeMethods.ReadPathsFromFile();
Logger.LogInfo($"Fallback: Read {paths.Length} files from file.");
}
}
else
{
Logger.LogInfo("No pipe name provided, using file-based IPC.");
paths = NativeMethods.ReadPathsFromFile();
Logger.LogInfo($"Read {paths.Length} files from file.");
}
LoadProcessesCommand = new AsyncRelayCommand(LoadProcessesAsync);
}

View File

@@ -49,7 +49,9 @@ namespace Awake.Core
private static DateTimeOffset ExpireAt { get; set; }
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
@@ -451,7 +453,7 @@ namespace Awake.Core
Dictionary<string, uint> optionsList = new()
{
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
};
return optionsList;

View File

@@ -159,6 +159,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} hour.
/// </summary>
internal static string AWAKE_HOUR {
get {
return ResourceManager.GetString("AWAKE_HOUR", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} hours.
/// </summary>
@@ -240,6 +249,15 @@ namespace Awake.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} minute.
/// </summary>
internal static string AWAKE_MINUTE {
get {
return ResourceManager.GetString("AWAKE_MINUTE", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} minutes.
/// </summary>

View File

@@ -123,6 +123,10 @@
<data name="AWAKE_EXIT" xml:space="preserve">
<value>Exit</value>
</data>
<data name="AWAKE_HOUR" xml:space="preserve">
<value>{0} hour</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_HOURS" xml:space="preserve">
<value>{0} hours</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -142,6 +146,10 @@
<value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment>
</data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_MINUTES" xml:space="preserve">
<value>{0} minutes</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>

View File

@@ -51,4 +51,5 @@ std::vector<std::wstring> processes =
L"PowerToys.WorkspacesWindowArranger.exe",
L"PowerToys.WorkspacesEditor.exe",
L"PowerToys.ZoomIt.exe",
L"Microsoft.CmdPal.UI.exe",
};