Compare commits

..

2 Commits

Author SHA1 Message Date
Leilei Zhang
ba2d1fc3f5 make rot 2025-09-16 16:15:40 +08:00
Leilei Zhang
f382b724a8 use rot 2025-09-16 12:55:01 +08:00
33 changed files with 631 additions and 1462 deletions

View File

@@ -61,7 +61,6 @@
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" />

View File

@@ -805,10 +805,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCPServer", "MCPServer", "{B637E6DD-FB81-4595-BB9C-01168556EA9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCPServer", "src\modules\MCPServer\MCPServer\MCPServer.csproj", "{20CBF173-9E8D-3236-6664-5B9C303794A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2927,14 +2923,6 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|ARM64.Build.0 = Debug|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.ActiveCfg = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Debug|x64.Build.0 = Debug|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.ActiveCfg = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|ARM64.Build.0 = Release|ARM64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.ActiveCfg = Release|x64
{20CBF173-9E8D-3236-6664-5B9C303794A3}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3255,8 +3243,6 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{B637E6DD-FB81-4595-BB9C-01168556EA9E} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{20CBF173-9E8D-3236-6664-5B9C303794A3} = {B637E6DD-FB81-4595-BB9C-01168556EA9E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,225 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Threading;
#nullable enable
#pragma warning disable IL2050 // Suppress COM interop trimming warnings for ROT hosting P/Invokes (desktop only scenario)
namespace ManagedCommon
{
/// <summary>
/// Generic helper to host a single COM-visible automation object in the Running Object Table (ROT)
/// without registry/CLSID class factory registration. Used for lightweight cross-process automation.
/// Pattern: create instance -> register with moniker -> wait until Stop.
/// Threading: spins up a dedicated STA thread so objects needing STA semantics are safe.
/// </summary>
public sealed class RotSingletonHost : IDisposable
{
private readonly Lock _sync = new();
private readonly Func<object> _factory;
private readonly string _monikerName;
private readonly string _threadName;
private readonly ManualResetEvent _shutdown = new(false);
private Thread? _thread;
private int _rotCookie;
private object? _instance; // keep alive
private IMoniker? _moniker;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="RotSingletonHost"/> class.
/// </summary>
/// <param name="monikerName">Moniker name (logical unique id), e.g. "Awake.Automation".</param>
/// <param name="factory">Factory that creates the object to expose. Should return a COM-visible object.</param>
/// <param name="threadName">Optional thread name for diagnostics.</param>
public RotSingletonHost(string monikerName, Func<object> factory, string? threadName = null)
{
_monikerName = string.IsNullOrWhiteSpace(monikerName) ? throw new ArgumentException("Moniker required", nameof(monikerName)) : monikerName;
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_threadName = threadName ?? $"RotHost:{_monikerName}";
}
public bool IsRunning => _thread != null;
public string MonikerName => _monikerName;
public void Start()
{
lock (_sync)
{
if (_disposed)
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
if (_thread != null)
{
return; // already running
}
_thread = new Thread(ThreadMain)
{
IsBackground = true,
Name = _threadName,
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
Logger.LogInfo($"ROT host starting for moniker '{_monikerName}'");
}
}
public void Stop()
{
lock (_sync)
{
if (_thread == null)
{
return;
}
_shutdown.Set();
}
_thread?.Join(3000);
_thread = null;
_shutdown.Reset();
}
private void ThreadMain()
{
int hr = Ole32.CoInitializeEx(IntPtr.Zero, Ole32.CoinitApartmentThreaded);
if (hr < 0)
{
Logger.LogError($"CoInitializeEx failed: 0x{hr:X8}");
return;
}
try
{
hr = Ole32.GetRunningObjectTable(0, out var rot);
if (hr < 0 || rot == null)
{
Logger.LogError($"GetRunningObjectTable failed: 0x{hr:X8}");
return;
}
hr = Ole32.CreateItemMoniker("!", _monikerName, out _moniker);
if (hr < 0 || _moniker == null)
{
Logger.LogError($"CreateItemMoniker failed: 0x{hr:X8}");
return;
}
_instance = _factory();
var unk = Marshal.GetIUnknownForObject(_instance);
try
{
hr = rot.Register(0x1 /* ROTFLAGS_REGISTRATIONKEEPSALIVE */, _instance, _moniker, out _rotCookie);
if (hr < 0)
{
Logger.LogError($"IRunningObjectTable.Register failed: 0x{hr:X8}");
return;
}
}
finally
{
Marshal.Release(unk);
}
Logger.LogInfo($"ROT registered: '{_monikerName}'");
WaitHandle.WaitAny(new WaitHandle[] { _shutdown });
}
catch (Exception ex)
{
Logger.LogError($"ROT host exception: {ex}");
}
finally
{
try
{
if (_rotCookie != 0 && Ole32.GetRunningObjectTable(0, out var rot2) == 0 && rot2 != null)
{
rot2.Revoke(_rotCookie);
_rotCookie = 0;
}
}
catch (Exception ex)
{
Logger.LogWarning($"Exception revoking ROT registration: {ex.Message}");
}
Ole32.CoUninitialize();
Logger.LogInfo($"ROT host stopped: '{_monikerName}'");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
Stop();
_disposed = true;
}
private static class Ole32
{
internal const int CoinitApartmentThreaded = 0x2;
#pragma warning disable IL2050 // Suppress trimming warnings for COM interop P/Invokes; ROT hosting not used in trimmed scenarios.
[DllImport("ole32.dll")]
internal static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);
[DllImport("ole32.dll")]
internal static extern void CoUninitialize();
[DllImport("ole32.dll")]
internal static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable? prot);
[DllImport("ole32.dll")]
internal static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem, out IMoniker? ppmk);
#pragma warning restore IL2050
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000010-0000-0000-C000-000000000046")]
private interface IRunningObjectTable
{
int Register(int grfFlags, [MarshalAs(UnmanagedType.IUnknown)] object punkObject, IMoniker pmkObjectName, out int pdwRegister);
int Revoke(int dwRegister);
void IsRunning(IMoniker pmkObjectName);
int GetObject(IMoniker pmkObjectName, [MarshalAs(UnmanagedType.IUnknown)] out object? ppunkObject);
void NoteChangeTime(int dwRegister, ref FileTime pfiletime);
int GetTimeOfLastChange(IMoniker pmkObjectName, ref FileTime pfiletime);
int EnumRunning(out object ppenumMoniker);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("0000000f-0000-0000-C000-000000000046")]
private interface IMoniker
{
}
[StructLayout(LayoutKind.Sequential)]
private struct FileTime
{
public uint DwLowDateTime;
public uint DwHighDateTime;
}
}
}

View File

@@ -1,47 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>false</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.MCPServer</AssemblyName>
<AssemblyDescription>PowerToys MCP Server for Model Context Protocol</AssemblyDescription>
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<NoWin32Manifest>true</NoWin32Manifest>
<RootNamespace>PowerToys.MCPServer</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- CsWinRT configuration -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<!-- Core dependencies -->
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol " />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
<!-- PowerToys project references -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,34 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using PowerToys.MCPServer.Tools;
namespace MCPServer
{
internal sealed class Program
{
private static async Task<int> Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
return 0;
}
}
}

View File

@@ -1,184 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Data.Sqlite;
using Microsoft.PowerToys.Settings.UI.Library;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class AwakeTools
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string SetTimeTest(string message) => $"Hello {message}";
private sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
[McpServerTool]
[Description("Get top N foreground app usage entries recorded by Awake. Reads usage.sqlite if present (preferred) else legacy usage.json. Parameters: top (default 10), days (default 7). Returns JSON array.")]
public static string GetAwakeUsageSummary(int top = 10, int days = 7)
{
try
{
SettingsUtils utils = new();
string settingsPath = utils.GetSettingsFilePath("Awake");
string directory = Path.GetDirectoryName(settingsPath)!;
string sqlitePath = Path.Combine(directory, "usage.sqlite");
string legacyJson = Path.Combine(directory, "usage.json");
if (File.Exists(sqlitePath))
{
return QuerySqlite(sqlitePath, top, days);
}
// Fallback to legacy JSON if DB not yet created (tracking not enabled or not flushed).
if (File.Exists(legacyJson))
{
return QueryLegacyJson(legacyJson, top, days, note: "legacy-json");
}
return JsonSerializer.Serialize(new { error = "No usage data found", sqlite = sqlitePath, legacy = legacyJson });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
}
private static string QuerySqlite(string dbPath, int top, int days)
{
try
{
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = SqliteOpenMode.ReadOnly }.ToString());
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
var list = cmd.ExecuteReader()
.Cast<System.Data.Common.DbDataRecord>()
.Select(r => new AppUsageRecord
{
ProcessName = r.GetString(0),
TotalSeconds = r.GetDouble(1),
FirstSeenUtc = DateTime.Parse(r.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind),
LastUpdatedUtc = DateTime.Parse(r.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind),
})
.OrderByDescending(r => r.TotalSeconds)
.Select(r => new
{
process = r.ProcessName,
totalSeconds = Math.Round(r.TotalSeconds, 1),
totalHours = Math.Round(r.TotalSeconds / 3600.0, 2),
firstSeenUtc = r.FirstSeenUtc,
lastUpdatedUtc = r.LastUpdatedUtc,
source = "sqlite",
});
return JsonSerializer.Serialize(list);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "sqlite query failed", message = ex.Message, path = dbPath });
}
}
private static string QueryLegacyJson(string usageFile, int top, int days, string? note = null)
{
try
{
string json = File.ReadAllText(usageFile);
using JsonDocument doc = JsonDocument.Parse(json);
DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days));
var result = doc.RootElement
.EnumerateArray()
.Select(e => new
{
process = e.GetPropertyOrDefault("process", string.Empty),
totalSeconds = e.GetPropertyOrDefault("totalSeconds", 0.0),
lastUpdatedUtc = e.GetPropertyOrDefaultDateTime("lastUpdatedUtc"),
firstSeenUtc = e.GetPropertyOrDefaultDateTime("firstSeenUtc"),
})
.Where(r => r.lastUpdatedUtc >= cutoff)
.OrderByDescending(r => r.totalSeconds)
.Take(top)
.Select(r => new
{
r.process,
totalSeconds = Math.Round(r.totalSeconds, 1),
totalHours = Math.Round(r.totalSeconds / 3600.0, 2),
r.firstSeenUtc,
r.lastUpdatedUtc,
source = note ?? "json",
});
return JsonSerializer.Serialize(result);
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = "legacy json read failed", message = ex.Message, path = usageFile });
}
}
private static string GetPropertyOrDefault(this JsonElement element, string name, string defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.String)
{
return value.GetString() ?? defaultValue;
}
return defaultValue;
}
private static double GetPropertyOrDefault(this JsonElement element, string name, double defaultValue)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out double d))
{
return d;
}
return defaultValue;
}
private static DateTime GetPropertyOrDefaultDateTime(this JsonElement element, string name)
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value))
{
if (value.ValueKind == JsonValueKind.String && value.TryGetDateTime(out DateTime dt))
{
return dt;
}
}
return DateTime.MinValue;
}
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace PowerToys.MCPServer.Tools
{
[McpServerToolType]
public static class EchoTool
{
[McpServerTool]
[Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"Hello {message}";
}
}

View File

@@ -1,18 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"PowerToys.MCPServer": "Debug"
}
},
"MCPServer": {
"Port": 8080,
"MaxConcurrentConnections": 100,
"RequestTimeoutSeconds": 30,
"EnableTools": true,
"EnableResources": true,
"Transport": "http"
}
}

View File

@@ -1,2 +0,0 @@
EXPORTS
powertoy_create

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\Common.Cpp.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A8B8D654-8F2A-4E6C-9B4F-1234567890AB}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MCPServerModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<ModuleDefinitionFile>MCPServerModuleInterface.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<None Include="MCPServerModuleInterface.def" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\SettingsAPI\SettingsAPI.vcxproj" />
<ProjectReference Include="..\..\..\..\common\logger\logger.vcxproj" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -1,298 +0,0 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/resources.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/winapi_error.h>
#include <shellapi.h>
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"PowerToys.MCPServer.exe";
const wchar_t ModuleKey[] = L"MCPServer";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
class MCPServerModuleInterface : public PowertoyModuleIface
{
public:
virtual PCWSTR get_name() override
{
return app_name.c_str();
}
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_not_configured;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(L"MCP Server provides Model Context Protocol access to PowerToys functionality for AI assistants and tools");
settings.set_icon_key(L"pt-mcp-server");
// Port configuration
settings.add_int_spinner(
L"port",
L"Server Port",
m_port,
1024,
65535,
1);
// Auto start option
settings.add_bool_toggle(
L"auto_start",
L"Auto Start Server",
m_auto_start);
// Enable tools API
settings.add_bool_toggle(
L"enable_tools",
L"Enable Tools API",
m_enable_tools);
// Enable resources API
settings.add_bool_toggle(
L"enable_resources",
L"Enable Resources API",
m_enable_resources);
// Transport protocol
settings.add_dropdown(
L"transport",
L"Transport Protocol",
m_transport,
std::vector<std::pair<std::wstring, std::wstring>>{
{ L"http", L"HTTP" },
{ L"stdio", L"Standard I/O" },
{ L"tcp", L"TCP Socket" }
});
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
if (auto port = values.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = values.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = values.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = values.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = values.get_string_value(L"transport"))
{
m_transport = transport.value();
}
values.save_to_settings_file();
// If service is running, restart to apply new configuration
if (m_enabled && is_process_running())
{
StopMCPServer();
StartMCPServer();
}
}
catch (std::exception& e)
{
Logger::error("MCPServer configuration parsing failed: {}", std::string{ e.what() });
}
}
virtual void enable() override
{
Logger::info("MCPServer enabling");
m_enabled = true;
if (m_auto_start)
{
StartMCPServer();
}
}
virtual void disable() override
{
Logger::info("MCPServer disabling");
m_enabled = false;
StopMCPServer();
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual void destroy() override
{
StopMCPServer();
delete this;
}
MCPServerModuleInterface()
{
app_name = L"MCP Server";
app_key = NonLocalizable::ModuleKey;
m_port = 8080;
m_auto_start = true;
m_enable_tools = true;
m_enable_resources = true;
m_transport = L"http";
init_settings();
}
private:
void StartMCPServer()
{
if (m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
{
return; // Already running
}
std::wstring executable_args = L"--port=" + std::to_wstring(m_port);
if (!m_enable_tools)
{
executable_args += L" --disable-tools";
}
if (!m_enable_resources)
{
executable_args += L" --disable-resources";
}
if (!m_transport.empty())
{
executable_args += L" --transport=" + m_transport;
}
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = NonLocalizable::ModulePath;
sei.nShow = SW_HIDE;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
m_hProcess = sei.hProcess;
Logger::info("MCPServer started successfully on port {} with transport {}", m_port, std::string(m_transport.begin(), m_transport.end()));
}
else
{
Logger::error("Failed to start MCPServer");
auto message = get_last_error_message(GetLastError());
if (message.has_value())
{
Logger::error(message.value());
}
}
}
void StopMCPServer()
{
if (m_hProcess)
{
TerminateProcess(m_hProcess, 0);
CloseHandle(m_hProcess);
m_hProcess = nullptr;
Logger::info("MCPServer stopped");
}
}
bool is_process_running()
{
return m_hProcess && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
if (auto port = settings.get_int_value(L"port"))
{
m_port = port.value();
}
if (auto auto_start = settings.get_bool_value(L"auto_start"))
{
m_auto_start = auto_start.value();
}
if (auto enable_tools = settings.get_bool_value(L"enable_tools"))
{
m_enable_tools = enable_tools.value();
}
if (auto enable_resources = settings.get_bool_value(L"enable_resources"))
{
m_enable_resources = enable_resources.value();
}
if (auto transport = settings.get_string_value(L"transport"))
{
m_transport = transport.value();
}
}
catch (std::exception&)
{
Logger::warn(L"MCPServer settings file not found, using defaults");
}
}
std::wstring app_name;
std::wstring app_key;
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
int m_port = 8080;
bool m_auto_start = true;
bool m_enable_tools = true;
bool m_enable_resources = true;
std::wstring m_transport = L"http";
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MCPServerModuleInterface();
}

View File

@@ -1 +0,0 @@
#include "pch.h"

View File

@@ -1,14 +0,0 @@
#pragma once
#include "targetver.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <unknwn.h>
#include <restrictederrorinfo.h>
#include <hstring.h>
#include <string>
#include <vector>

View File

@@ -1,8 +0,0 @@
#pragma once
// Including SDKDDKVer.h defines the highest available Windows platform.
// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
#include <SDKDDKVer.h>

View File

@@ -3525,10 +3525,6 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
}
if( destFile == nullptr ) {
if (stream) {
stream.Close();
stream = nullptr;
}
co_await file.DeleteAsync();
}
else {
@@ -3548,10 +3544,6 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
}
else {
if (stream) {
stream.Close();
stream = nullptr;
}
co_await file.DeleteAsync();
g_RecordingSession = nullptr;
}
@@ -4024,10 +4016,7 @@ LRESULT APIENTRY MainWndProc(
// Now copy crop or copy+save
if( LOWORD( wParam ) == SNIP_SAVE_HOTKEY )
{
// Hide cursor for screen capture
ShowCursor(false);
SendMessage( hWnd, WM_COMMAND, IDC_SAVE_CROP, ( zoomed ? 0 : SHALLOW_ZOOM ) );
ShowCursor(true);
}
else
{
@@ -4059,6 +4048,12 @@ LRESULT APIENTRY MainWndProc(
OutputDebug( L"Exiting liveDraw after snip\n" );
SendMessage( hWnd, WM_KEYDOWN, VK_ESCAPE, 0 );
}
else
{
// Set wparam to 1 to exit without animation
OutputDebug(L"Exiting zoom after snip\n" );
SendMessage( hWnd, WM_HOTKEY, ZOOM_HOTKEY, SHALLOW_DESTROY );
}
}
break;
}
@@ -5783,26 +5778,17 @@ LRESULT APIENTRY MainWndProc(
if( !g_DrawingShape ) {
// If the point has changed, draw a line to it
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam)) {
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
{
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
}
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
Gdiplus::GraphicsPath path;
pen.SetLineJoin(Gdiplus::LineJoinRound);
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
dstGraphics.DrawPath(&pen, &path);
}
// Draw a dot at the current point, if the point hasn't changed
else {
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
InvalidateRect(hWnd, NULL, FALSE);
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
if( ( GetWindowLong( g_hWndMain, GWL_EXSTYLE ) & WS_EX_LAYERED ) == 0 )
{
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
}
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
Gdiplus::GraphicsPath path;
pen.SetLineJoin(Gdiplus::LineJoinRound);
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
dstGraphics.DrawPath(&pen, &path);
prevPt.x = LOWORD( lParam );
prevPt.y = HIWORD( lParam );

View File

@@ -42,7 +42,6 @@
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Runtime.Caching" />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using ManagedCommon;
namespace Awake
{
/// <summary>
/// Automation object exposed via the Running Object Table. Intentionally minimal; methods may expand in future.
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("4F1C3769-8D28-4A2D-8A6A-AB2F4C0F5F11")]
public sealed class AwakeAutomation : IAwakeAutomation
{
public string Ping() => "pong";
public void SetIndefinite() => Logger.LogInfo("Automation: SetIndefinite");
public void SetTimed(int seconds) => Logger.LogInfo($"Automation: SetTimed {seconds}s");
public void SetExpirable(int minutes) => Logger.LogInfo($"Automation: SetExpirable {minutes}m");
public void SetPassive() => Logger.LogInfo("Automation: SetPassive");
public void Cancel() => Logger.LogInfo("Automation: Cancel");
public string GetStatusJson() => "{\"ok\":true}";
}
}

View File

@@ -17,9 +17,6 @@ using System.Text.Json;
using System.Threading;
using Awake.Core.Models;
using Awake.Core.Native;
// New usage tracking namespace
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -59,9 +56,6 @@ namespace Awake.Core
private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource;
// Foreground usage tracker instance (lifecycle managed by Program)
internal static ForegroundUsageTracker? UsageTracker { get; set; }
static Manager()
{
_tokenSource = new CancellationTokenSource();
@@ -418,16 +412,6 @@ namespace Awake.Core
Bridge.DestroyWindow(TrayHelper.WindowHandle);
}
// Dispose usage tracker (flushes data)
try
{
UsageTracker?.Dispose();
}
catch (Exception ex)
{
Logger.LogWarning($"Failed disposing UsageTracker: {ex.Message}");
}
Bridge.PostQuitMessage(exitCode);
Environment.Exit(exitCode);
}

View File

@@ -1,44 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Awake.Core.Native
{
internal static class IdleTime
{
// Keep original native field names but suppress StyleCop (interop requires exact names).
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
#pragma warning disable SA1307 // Interop field naming
public uint cbSize;
public uint dwTime;
#pragma warning restore SA1307
}
[DllImport("user32.dll")]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
public static TimeSpan GetIdleTime()
{
LASTINPUTINFO info = new()
{
cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
};
if (!GetLastInputInfo(ref info))
{
return TimeSpan.Zero;
}
// Calculate elapsed milliseconds since last input considering Environment.TickCount wrap.
uint lastInputTicks = info.dwTime;
uint nowTicks = (uint)Environment.TickCount;
uint delta = nowTicks >= lastInputTicks ? nowTicks - lastInputTicks : (uint.MaxValue - lastInputTicks) + nowTicks + 1;
return TimeSpan.FromMilliseconds(delta);
}
}
}

View File

@@ -1,364 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Timers;
using Awake.Core.Native;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Awake.Core.Usage
{
internal sealed class ForegroundUsageTracker : IDisposable
{
private const uint EventSystemForeground = 0x0003;
private const uint WinEventOutOfContext = 0x0000;
private const double CommitThresholdSeconds = 0.25;
private static readonly JsonSerializerOptions LegacySerializer = new()
{
WriteIndented = true,
};
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
private readonly object _lock = new();
private readonly string _legacyJsonPath;
private readonly string _dbPath;
private readonly Timer _flushTimer;
private readonly Timer _pollTimer;
private readonly TimeSpan _idleThreshold = TimeSpan.FromSeconds(60);
private readonly Dictionary<string, AppUsageRecord> _sessionCache = new(StringComparer.OrdinalIgnoreCase);
private IUsageStore _store;
private string? _activeProcess;
private DateTime _activeSince;
private IntPtr _hook;
private WinEventDelegate? _hookDelegate;
private IntPtr _lastHwnd;
private int _retentionDays;
private bool _disposed;
internal bool Enabled { get; private set; }
public ForegroundUsageTracker(string legacyJsonPath, int retentionDays)
{
_legacyJsonPath = legacyJsonPath;
_dbPath = Path.Combine(Path.GetDirectoryName(legacyJsonPath)!, "usage.sqlite");
_retentionDays = retentionDays;
_store = new SqliteUsageStore(_dbPath);
_flushTimer = new Timer(5000)
{
AutoReset = true,
};
_flushTimer.Elapsed += (_, _) => FlushInternal();
_pollTimer = new Timer(1000)
{
AutoReset = true,
};
_pollTimer.Elapsed += (_, _) => PollForeground();
TryImportLegacy();
}
private void TryImportLegacy()
{
try
{
if (!File.Exists(_legacyJsonPath))
{
return;
}
string json = File.ReadAllText(_legacyJsonPath);
List<AppUsageRecord> list = JsonSerializer.Deserialize<List<AppUsageRecord>>(json, LegacySerializer) ?? new();
foreach (AppUsageRecord r in list)
{
_store.AddSpan(r.ProcessName, r.TotalSeconds, r.FirstSeenUtc, r.LastUpdatedUtc, _retentionDays);
}
Logger.LogInfo("[AwakeUsage] Imported legacy usage.json into SQLite. Deleting old file.");
File.Delete(_legacyJsonPath);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Legacy import failed: " + ex.Message);
}
}
public void Configure(bool enabled, int retentionDays)
{
_retentionDays = Math.Max(1, retentionDays);
if (enabled == Enabled)
{
return;
}
Enabled = enabled;
if (Enabled)
{
_activeSince = DateTime.UtcNow;
_hookDelegate = WinEventCallback;
_hook = SetWinEventHook(EventSystemForeground, EventSystemForeground, IntPtr.Zero, _hookDelegate, 0, 0, WinEventOutOfContext);
Logger.LogInfo(_hook != IntPtr.Zero ? "[AwakeUsage] WinEvent hook installed." : "[AwakeUsage] WinEvent hook failed (poll fallback)");
CaptureInitialForeground();
_flushTimer.Start();
_pollTimer.Start();
Logger.LogInfo("[AwakeUsage] Tracking enabled (5s flush, sqlite store).");
}
else
{
_flushTimer.Stop();
_pollTimer.Stop();
if (_hook != IntPtr.Zero)
{
UnhookWinEvent(_hook);
_hook = IntPtr.Zero;
}
CommitActiveSpan();
FlushInternal(force: true);
Logger.LogInfo("[AwakeUsage] Tracking disabled.");
}
}
private void WinEventCallback(
IntPtr hWinEventHook,
uint evt,
IntPtr hwnd,
int idObj,
int idChild,
uint thread,
uint time)
{
if (_disposed || !Enabled || evt != EventSystemForeground)
{
return;
}
HandleForegroundChange(hwnd, "hook");
}
private void PollForeground()
{
if (_disposed || !Enabled)
{
return;
}
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero || hwnd == _lastHwnd)
{
return;
}
HandleForegroundChange(hwnd, "poll");
}
private void CaptureInitialForeground()
{
IntPtr hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero)
{
return;
}
if (TryResolveProcess(hwnd, out string? name))
{
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
}
private bool TryResolveProcess(IntPtr hwnd, out string? name)
{
name = null;
try
{
uint pid;
uint tid = GetWindowThreadProcessId(hwnd, out pid);
if (tid == 0 || pid == 0)
{
return false;
}
using Process p = Process.GetProcessById((int)pid);
name = SafeProcessName(p);
return !string.IsNullOrWhiteSpace(name);
}
catch
{
return false;
}
}
private static string SafeProcessName(Process p)
{
try
{
return Path.GetFileName(p.MainModule?.FileName) ?? p.ProcessName;
}
catch
{
return p.ProcessName;
}
}
private void HandleForegroundChange(IntPtr hwnd, string source)
{
try
{
CommitActiveSpan();
if (!TryResolveProcess(hwnd, out string? name))
{
_activeProcess = null;
return;
}
_activeProcess = name;
_activeSince = DateTime.UtcNow;
_lastHwnd = hwnd;
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] FG change failed: " + ex.Message);
}
}
private void CommitActiveSpan()
{
if (string.IsNullOrEmpty(_activeProcess))
{
return;
}
if (IdleTime.GetIdleTime() > _idleThreshold)
{
_activeProcess = null;
return;
}
double secs = (DateTime.UtcNow - _activeSince).TotalSeconds;
if (secs < CommitThresholdSeconds)
{
return;
}
lock (_lock)
{
if (!_sessionCache.TryGetValue(_activeProcess!, out AppUsageRecord? rec))
{
rec = new AppUsageRecord
{
ProcessName = _activeProcess!,
FirstSeenUtc = DateTime.UtcNow,
LastUpdatedUtc = DateTime.UtcNow,
TotalSeconds = 0,
};
_sessionCache[_activeProcess!] = rec;
}
rec.TotalSeconds += secs;
rec.LastUpdatedUtc = DateTime.UtcNow;
}
_activeSince = DateTime.UtcNow;
}
private void FlushInternal(bool force = false)
{
try
{
CommitActiveSpan();
Dictionary<string, AppUsageRecord> snapshot;
lock (_lock)
{
snapshot = _sessionCache.ToDictionary(k => k.Key, v => v.Value);
_sessionCache.Clear();
}
foreach (AppUsageRecord rec in snapshot.Values)
{
_store.AddSpan(rec.ProcessName, rec.TotalSeconds, rec.FirstSeenUtc, rec.LastUpdatedUtc, _retentionDays);
}
if (force)
{
_store.Prune(_retentionDays);
}
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Flush failed: " + ex.Message);
}
}
public IReadOnlyList<AppUsageRecord> GetSummary(int top, int days)
{
CommitActiveSpan();
FlushInternal();
try
{
return _store.Query(top, days);
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage] Query failed: " + ex.Message);
return Array.Empty<AppUsageRecord>();
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Configure(false, _retentionDays);
_store.Dispose();
_flushTimer.Dispose();
_pollTimer.Dispose();
}
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1516, SA1636
using System;
using System.Collections.Generic;
using Awake.Core.Usage.Models;
namespace Awake.Core.Usage
{
internal interface IUsageStore : IDisposable
{
void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays);
IReadOnlyList<AppUsageRecord> Query(int top, int days);
void Prune(int retentionDays);
}
}
#pragma warning restore SA1516, SA1636

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.Json.Serialization;
namespace Awake.Core.Usage.Models
{
internal sealed class AppUsageRecord
{
[JsonPropertyName("process")]
public string ProcessName { get; set; } = string.Empty;
[JsonPropertyName("totalSeconds")]
public double TotalSeconds { get; set; }
[JsonPropertyName("lastUpdatedUtc")]
public DateTime LastUpdatedUtc { get; set; }
[JsonPropertyName("firstSeenUtc")]
public DateTime FirstSeenUtc { get; set; }
}
}

View File

@@ -1,145 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1516, SA1210, SA1636
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using Awake.Core.Usage.Models;
using ManagedCommon;
using Microsoft.Data.Sqlite;
namespace Awake.Core.Usage
{
internal sealed class SqliteUsageStore : IUsageStore
{
private readonly string _dbPath;
private readonly string _connectionString;
public SqliteUsageStore(string dbPath)
{
_dbPath = dbPath;
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
_connectionString = new SqliteConnectionStringBuilder
{
DataSource = _dbPath,
Mode = SqliteOpenMode.ReadWriteCreate,
}.ToString();
Initialize();
}
private void Initialize()
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS process_usage (
process_name TEXT NOT NULL,
day_utc TEXT NOT NULL,
total_seconds REAL NOT NULL,
first_seen_utc TEXT NOT NULL,
last_updated_utc TEXT NOT NULL,
PRIMARY KEY(process_name, day_utc)
);";
cmd.ExecuteNonQuery();
}
public void AddSpan(string processName, double seconds, DateTime firstSeenUtc, DateTime lastUpdatedUtc, int retentionDays)
{
if (seconds <= 0)
{
return;
}
string day = DateTime.UtcNow.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteTransaction tx = conn.BeginTransaction();
using (SqliteCommand cmd = conn.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = @"INSERT INTO process_usage(process_name, day_utc, total_seconds, first_seen_utc, last_updated_utc)
VALUES($p,$d,$s,$f,$l)
ON CONFLICT(process_name,day_utc) DO UPDATE SET
total_seconds = total_seconds + excluded.total_seconds,
last_updated_utc = excluded.last_updated_utc;";
cmd.Parameters.AddWithValue("$p", processName);
cmd.Parameters.AddWithValue("$d", day);
cmd.Parameters.AddWithValue("$s", seconds);
cmd.Parameters.AddWithValue("$f", firstSeenUtc.ToString("o"));
cmd.Parameters.AddWithValue("$l", lastUpdatedUtc.ToString("o"));
cmd.ExecuteNonQuery();
}
using (SqliteCommand prune = conn.CreateCommand())
{
prune.Transaction = tx;
prune.CommandText = @"DELETE FROM process_usage WHERE day_utc < date('now', @retention);";
prune.Parameters.AddWithValue("@retention", $"-{Math.Max(1, retentionDays)} days");
prune.ExecuteNonQuery();
}
tx.Commit();
}
public IReadOnlyList<AppUsageRecord> Query(int top, int days)
{
List<AppUsageRecord> result = new();
int safeDays = Math.Max(1, days);
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT process_name, SUM(total_seconds) AS total_seconds, MIN(first_seen_utc) AS first_seen_utc, MAX(last_updated_utc) AS last_updated_utc
FROM process_usage
WHERE day_utc >= date('now', @cutoff)
GROUP BY process_name
ORDER BY total_seconds DESC
LIMIT @top;";
cmd.Parameters.AddWithValue("@cutoff", $"-{safeDays} days");
cmd.Parameters.AddWithValue("@top", top);
using SqliteDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
while (reader.Read())
{
try
{
string name = reader.GetString(0);
double secs = reader.GetDouble(1);
DateTime first = DateTime.Parse(reader.GetString(2), null, System.Globalization.DateTimeStyles.RoundtripKind);
DateTime last = DateTime.Parse(reader.GetString(3), null, System.Globalization.DateTimeStyles.RoundtripKind);
result.Add(new AppUsageRecord
{
ProcessName = name,
TotalSeconds = secs,
FirstSeenUtc = first,
LastUpdatedUtc = last,
});
}
catch (Exception ex)
{
Logger.LogWarning("[AwakeUsage][SQLite] Row parse failed: " + ex.Message);
}
}
return result;
}
public void Prune(int retentionDays)
{
using SqliteConnection conn = new(_connectionString);
conn.Open();
using SqliteCommand cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM process_usage WHERE day_utc < date('now', @cutoff);";
cmd.Parameters.AddWithValue("@cutoff", $"-{Math.Max(1, retentionDays)} days");
cmd.ExecuteNonQuery();
}
public void Dispose()
{
}
}
}
#pragma warning restore SA1516, SA1210, SA1636

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Awake
{
/// <summary>
/// COM automation interface exposed via ROT for controlling Awake.
/// </summary>
[ComVisible(true)]
[Guid("5CA92C1D-9D7E-4F6D-9B06-5B7B28BF4E21")]
public interface IAwakeAutomation
{
string Ping();
void SetIndefinite();
void SetTimed(int seconds);
void SetExpirable(int minutes);
void SetPassive();
void Cancel();
string GetStatusJson();
}
}

View File

@@ -18,9 +18,6 @@ using System.Threading.Tasks;
using Awake.Core;
using Awake.Core.Models;
using Awake.Core.Native;
// Usage tracking
using Awake.Core.Usage;
using Awake.Properties;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -99,6 +96,12 @@ namespace Awake
Logger.LogInfo($"OS: {Environment.OSVersion}");
Logger.LogInfo($"OS Build: {Manager.GetOperatingSystemBuild()}");
// Start background COM automation host (ROT) so any startup path exposes the automation surface.
// Uses default moniker; could be extended with a --rotname parameter if needed later.
var rotHost = new RotSingletonHost("Awake.Automation", () => new AwakeAutomation(), "AwakeAutomationRotThread");
rotHost.Start();
AppDomain.CurrentDomain.ProcessExit += (_, _) => rotHost.Stop();
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
Trace.WriteLine($"Task scheduler error: {args.Exception.Message}"); // somebody forgot to check!
@@ -345,33 +348,6 @@ namespace Awake
}
}
}
// Initialize usage tracking
InitializeUsageTracking();
}
private static void InitializeUsageTracking()
{
try
{
string settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName);
string directory = Path.GetDirectoryName(settingsPath)!;
string usageFile = Path.Combine(directory, "usage.json");
AwakeSettings settings = _settingsUtils.GetSettings<AwakeSettings>(Core.Constants.AppName) ?? new AwakeSettings();
if (Manager.UsageTracker == null)
{
Manager.UsageTracker = new ForegroundUsageTracker(usageFile, settings.Properties.UsageRetentionDays);
}
Manager.UsageTracker.Configure(settings.Properties.TrackUsageEnabled, settings.Properties.UsageRetentionDays);
Logger.LogInfo($"Usage tracking configured (enabled={settings.Properties.TrackUsageEnabled}, retentionDays={settings.Properties.UsageRetentionDays}).");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize usage tracking: {ex.Message}");
}
}
private static void AllocateLocalConsole()
@@ -391,7 +367,6 @@ namespace Awake
SetupFileSystemWatcher(settingsPath);
InitializeSettings();
ProcessSettings();
InitializeUsageTracking(); // after initial settings load
}
catch (Exception ex)
{
@@ -437,7 +412,6 @@ namespace Awake
{
Logger.LogInfo("Detected a settings file change. Updating configuration...");
ProcessSettings();
InitializeUsageTracking(); // re-evaluate usage tracking on config change
}
catch (Exception e)
{

View File

@@ -0,0 +1,131 @@
<#
AwakeRotMini.ps1 — Minimal ROT client for the Awake COM automation object.
Supported actions:
ping -> Calls Ping(), prints PING=pong
status -> Calls GetStatusJson(), prints JSON
cancel -> Calls Cancel(), prints CANCEL_OK
timed:<m> -> Calls SetTimed(<m * 60 seconds>), prints TIMED_OK (minutes input)
Assumptions:
- Server registered object in ROT via CreateItemMoniker("!", logicalName)
- We only need late binding (IDispatch InvokeMember) no type library.
Exit codes:
0 = success
2 = object not found in ROT
4 = call/parse error
NOTE: This script intentionally stays dependencylight and fast to start.
#>
param(
[string]$MonikerName = 'Awake.Automation',
[string]$Action = 'ping'
)
# ---------------------------------------------------------------------------
# Inline C# (single public class) handles:
# * ROT lookup via CreateItemMoniker("!", logicalName)
# * Late bound InvokeMember calls
# * Lightweight action dispatch
# ---------------------------------------------------------------------------
if (-not ('AwakeRotMiniClient' -as [type])) {
$code = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public static class AwakeRotMiniClient
{
// P/Invoke -------------------------------------------------------------
[DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ctx);
[DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int CreateItemMoniker(string delimiter, string item, out IMoniker mk);
// Internal helpers -----------------------------------------------------
private static void Open(out IRunningObjectTable rot, out IBindCtx ctx)
{
GetRunningObjectTable(0, out rot);
CreateBindCtx(0, out ctx);
}
private static object BindLogical(string logical)
{
Open(out var rot, out var ctx);
if (CreateItemMoniker("!", logical, out var mk) == 0)
{
try
{
rot.GetObject(mk, out var obj);
return obj;
}
catch
{
// Swallow treated as not found below.
}
}
return null;
}
private static object Call(object obj, string name, params object[] args)
{
var t = obj.GetType(); // System.__ComObject
return t.InvokeMember(
name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
null,
obj,
(args == null || args.Length == 0) ? null : args);
}
// Public entry ---------------------------------------------------------
public static string Exec(string logical, string action)
{
var obj = BindLogical(logical);
if (obj == null)
return "__NOT_FOUND__";
if (string.IsNullOrEmpty(action) || action == "ping")
{
try { return "PING=" + Call(obj, "Ping"); } catch (Exception ex) { return Err(ex); }
}
try
{
if (action == "status")
return (string)Call(obj, "GetStatusJson");
if (action == "cancel")
{
Call(obj, "Cancel");
return "CANCEL_OK";
}
if (action.StartsWith("timed:", StringComparison.OrdinalIgnoreCase))
{
var slice = action.Substring(6);
if (!int.TryParse(slice, out var minutes) || minutes < 0)
return "__ERR=Format:Invalid minutes";
Call(obj, "SetTimed", minutes * 60);
return "TIMED_OK";
}
return "UNKNOWN_ACTION";
}
catch (Exception ex)
{
return Err(ex);
}
}
private static string Err(Exception ex) => "__ERR=" + ex.GetType().Name + ":" + ex.Message;
}
'@
Add-Type -TypeDefinition $code -ErrorAction Stop | Out-Null
}
$result = [AwakeRotMiniClient]::Exec($MonikerName, $Action)
switch ($result) {
'__NOT_FOUND__' { exit 2 }
{ $_ -like '__ERR=*' } { $host.UI.WriteErrorLine($result); exit 4 }
default { Write-Output $result; exit 0 }
}

View File

@@ -0,0 +1,187 @@
<#
Minimal ROT test script for the runtimeregistered Awake automation object.
Usage:
.\AwakeRotTest.ps1 [-MonikerName Awake.Automation] -Action <action>
Actions:
ping -> PING=pong
status -> Returns JSON from GetStatusJson (placeholder now)
cancel -> Calls Cancel
timed:<m> -> Calls SetTimed(int minutes)
pingdbg -> Diagnostic: shows type info and invocation paths
Exit codes:
0 = success
2 = moniker not found
3 = (reserved for bind failure not currently used)
4 = method invocation failure
Notes:
- The automation object is registered with display name pattern: !<MonikerName>
- We late-bind via IDispatch InvokeMember because the object is surfaced as System.__ComObject.
- Keep this script selfcontained; avoid multiple Add-Type blocks to prevent type cache issues.
#>
param(
[string]$MonikerName = 'Awake.Automation',
[string]$Action
)
# ----------------------------
# Constants (exit codes)
# ----------------------------
Set-Variable -Name EXIT_OK -Value 0 -Option Constant
Set-Variable -Name EXIT_NOT_FOUND -Value 2 -Option Constant
Set-Variable -Name EXIT_CALL_FAIL -Value 4 -Option Constant
Write-Host '[AwakeRotTest] Start' -ForegroundColor Cyan
# ----------------------------
# C# helper (enumerate + bind + invoke)
# ----------------------------
if (-not ('AwakeRot.Client' -as [type])) {
$code = @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Collections.Generic;
using System.Text;
public static class AwakeRotClient
{
[DllImport("ole32.dll")] private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")] private static extern int CreateBindCtx(int reserved, out IBindCtx ctx);
private static (IRunningObjectTable rot, IBindCtx ctx) Open()
{
GetRunningObjectTable(0, out var rot);
CreateBindCtx(0, out var ctx);
return (rot, ctx);
}
// (Enumeration removed for simplification list action no longer supported.)
// Direct bind using CreateItemMoniker fast-path (avoids full enumeration).
[DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int CreateItemMoniker(string lpszDelim, string lpszItem, out IMoniker ppmk);
private static object Bind(string display)
{
var (rot, ctx) = Open();
if (display.Length > 1 && display[0] == '!')
{
string logical = display.Substring(1);
if (CreateItemMoniker("!", logical, out var mk) == 0 && mk != null)
{
try
{
rot.GetObject(mk, out var directObj);
return directObj; // may be null if not found
}
catch { return null; }
}
}
return null; // No fallback enumeration (intentionally removed for simplicity/perf determinism)
}
// Strong-typed interface (early binding) mirrors server's IAwakeAutomation definition.
// GUIDs copied from IAwakeAutomation / AwakeAutomation server code.
[ComImport]
[Guid("5CA92C1D-9D7E-4F6D-9B06-5B7B28BF4E21")] // interface GUID
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAwakeAutomation
{
[PreserveSig] string Ping();
void SetIndefinite();
void SetTimed(int seconds);
void SetExpirable(int minutes);
void SetPassive();
void Cancel();
[PreserveSig] string GetStatusJson();
}
// Fallback late-binding helper (kept for diagnostic / if cast fails)
private static object CallLate(object obj, string name, params object[] args)
{
var t = obj.GetType();
return t.InvokeMember(
name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
binder: null,
target: obj,
args: (args == null || args.Length == 0) ? null : args);
}
public static string Exec(string logical, string action)
{
string display = "!" + logical;
var obj = Bind(display);
if (obj == null)
return "__NOT_FOUND__";
var t = obj.GetType();
try
{
// Try strong-typed cast first.
IAwakeAutomation api = obj as IAwakeAutomation;
bool typed = api != null;
if (action == "pingdbg")
{
var sb = new StringBuilder();
sb.AppendLine("TYPE=" + t.FullName);
sb.AppendLine("StrongTypedCast=" + typed);
if (typed)
{
try { sb.AppendLine("TypedPing=" + api.Ping()); } catch (Exception ex) { sb.AppendLine("TypedPingErr=" + ex.Message); }
}
else
{
try { sb.AppendLine("LatePing=" + CallLate(obj, "Ping")); } catch (Exception ex) { sb.AppendLine("LatePingErr=" + ex.Message); }
}
return sb.ToString();
}
if (string.IsNullOrEmpty(action) || action == "demo")
{
var ping = typed ? api.Ping() : (string)CallLate(obj, "Ping");
return $"DEMO Ping={ping}";
}
if (action == "ping") return "PING=" + (typed ? api.Ping() : (string)CallLate(obj, "Ping"));
if (action == "status") return typed ? api.GetStatusJson() : (string)CallLate(obj, "GetStatusJson");
if (action == "cancel") { if (typed) api.Cancel(); else CallLate(obj, "Cancel"); return "CANCEL_OK"; }
if (action != null && action.StartsWith("timed:"))
{
var m = int.Parse(action.Substring(6));
// NOTE: Server SetTimed expects seconds (per interface). Action timed:<m> originally treated value as minutes -> semantic mismatch.
// For now keep behavior (treat number as minutes -> convert to seconds for strong typed call) to avoid breaking existing usage.
if (typed) api.SetTimed(m * 60); else CallLate(obj, "SetTimed", m * 60);
return "TIMED_OK";
}
return "UNKNOWN_ACTION";
}
catch (Exception ex)
{
return "__CALL_ERROR__" + ex.GetType().Name + ":" + ex.Message;
}
}
}
'@
Add-Type -TypeDefinition $code -ErrorAction Stop | Out-Null
}
# Quick list fast-path
if ($Action -eq 'list') {
[AwakeRotClient]::List() | ForEach-Object { $_ }
exit $EXIT_OK
}
$result = [AwakeRotClient]::Exec($MonikerName, $Action)
switch ($result) {
'__NOT_FOUND__' { Write-Host "Moniker !$MonikerName not found." -ForegroundColor Red; [Environment]::Exit($EXIT_NOT_FOUND) }
{ $_ -like '__CALL_ERROR__*' } { Write-Host "Call failed: $result" -ForegroundColor Red; [Environment]::Exit($EXIT_CALL_FAIL) }
default { Write-Host $result -ForegroundColor Green; [Environment]::Exit($EXIT_OK) }
}

View File

@@ -306,36 +306,6 @@
</controls:Case>
</controls:SwitchPresenter>
</controls:Case>
<controls:Case Value="True">
<StackPanel
Margin="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="4">
<cpcontrols:IconBox
x:Name="IconBorder"
Width="48"
Height="48"
Margin="8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
Margin="0,4,0,0"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>
</Grid>
</Page>

View File

@@ -20,10 +20,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IntervalMinutes = 1;
ExpirationDateTime = DateTimeOffset.Now;
CustomTrayTimes = [];
// Usage tracking defaults (opt-in, disabled by default)
TrackUsageEnabled = false; // default off
UsageRetentionDays = 14; // two weeks default retention
}
[JsonPropertyName("keepDisplayOn")]
@@ -44,13 +40,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("customTrayTimes")]
[CmdConfigureIgnore]
public Dictionary<string, uint> CustomTrayTimes { get; set; }
// New opt-in usage tracking flag
[JsonPropertyName("trackUsageEnabled")]
public bool TrackUsageEnabled { get; set; }
// Retention window for usage data (days)
[JsonPropertyName("usageRetentionDays")]
public int UsageRetentionDays { get; set; }
}
}

View File

@@ -38,9 +38,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
KeepDisplayOn = Properties.KeepDisplayOn,
IntervalMinutes = Properties.IntervalMinutes,
IntervalHours = Properties.IntervalHours,
// Fix old buggy default value that might be saved in Settings. Some components don't deal well with negative time zones and minimum time offsets.
ExpirationDateTime = Properties.ExpirationDateTime.Year < 2 ? DateTimeOffset.Now : Properties.ExpirationDateTime,
TrackUsageEnabled = Properties.TrackUsageEnabled,
UsageRetentionDays = Properties.UsageRetentionDays,
},
};
}

View File

@@ -99,22 +99,6 @@
IsEnabled="{x:Bind ViewModel.IsScreenConfigurationPossibleEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<!-- Usage tracking settings -->
<tkcontrols:SettingsCard
Name="AwakeUsageTrackingEnabledCard"
x:Uid="Awake_UsageTrackingEnabledCard"
HeaderIcon="{ui:FontIcon Glyph=&#xE11C;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.TrackUsageEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="AwakeUsageRetentionCard"
x:Uid="Awake_UsageRetentionCard"
HeaderIcon="{ui:FontIcon Glyph=&#xE823;}"
IsEnabled="{x:Bind ViewModel.TrackUsageEnabled, Mode=OneWay}">
<NumberBox Width="120" Minimum="1" Maximum="365" Value="{x:Bind ViewModel.UsageRetentionDays, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>

View File

@@ -527,22 +527,6 @@ opera.exe</value>
<value>Awake</value>
<comment>{Locked}</comment>
</data>
<data name="Awake_UsageTrackingEnabledCard.Header" xml:space="preserve">
<value>Track foreground app usage</value>
<comment>Header: enable/disable foreground process usage tracking (Awake)</comment>
</data>
<data name="Awake_UsageTrackingEnabledCard.Description" xml:space="preserve">
<value>Record the active foreground application's usage time locally. Data is stored only on this device.</value>
<comment>Description: explains what usage tracking does (Awake)</comment>
</data>
<data name="Awake_UsageRetentionCard.Header" xml:space="preserve">
<value>Usage data retention (days)</value>
<comment>Header: number of days to keep usage records (Awake)</comment>
</data>
<data name="Awake_UsageRetentionCard.Description" xml:space="preserve">
<value>Number of days to keep usage records (1365)</value>
<comment>Description: retention range hint (Awake)</comment>
</data>
<data name="Shell_PowerLauncher.Content" xml:space="preserve">
<value>PowerToys Run</value>
<comment>Product name: Navigation view item name for PowerToys Run</comment>

View File

@@ -198,33 +198,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool TrackUsageEnabled
{
get => ModuleSettings.Properties.TrackUsageEnabled;
set
{
if (ModuleSettings.Properties.TrackUsageEnabled != value)
{
ModuleSettings.Properties.TrackUsageEnabled = value;
NotifyPropertyChanged();
}
}
}
public int UsageRetentionDays
{
get => ModuleSettings.Properties.UsageRetentionDays;
set
{
int clamped = Math.Max(1, Math.Min(365, value));
if (ModuleSettings.Properties.UsageRetentionDays != clamped)
{
ModuleSettings.Properties.UsageRetentionDays = clamped;
NotifyPropertyChanged();
}
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
Logger.LogInfo($"Changed the property {propertyName}");
@@ -246,8 +219,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IntervalHours));
OnPropertyChanged(nameof(IntervalMinutes));
OnPropertyChanged(nameof(ExpirationDateTime));
OnPropertyChanged(nameof(TrackUsageEnabled));
OnPropertyChanged(nameof(UsageRetentionDays));
}
private bool _enabledStateIsGPOConfigured;