mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-18 01:00:06 +01:00
Compare commits
3 Commits
async-cpp-
...
shawn/mcps
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01e7b61efb | ||
|
|
be334fa0df | ||
|
|
1aeed1699e |
@@ -61,6 +61,7 @@
|
|||||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
<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" />
|
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||||
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
|
|||||||
@@ -805,6 +805,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
|
|||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|ARM64 = Debug|ARM64
|
Debug|ARM64 = Debug|ARM64
|
||||||
@@ -2923,6 +2927,14 @@ Global
|
|||||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
|
{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.ActiveCfg = Release|x64
|
||||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -3243,6 +3255,8 @@ Global
|
|||||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {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}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||||
|
|||||||
47
src/modules/MCPServer/MCPServer/MCPServer.csproj
Normal file
47
src/modules/MCPServer/MCPServer/MCPServer.csproj
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<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>
|
||||||
34
src/modules/MCPServer/MCPServer/Program.cs
Normal file
34
src/modules/MCPServer/MCPServer/Program.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs
Normal file
184
src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/modules/MCPServer/MCPServer/Tools/EchoTool.cs
Normal file
17
src/modules/MCPServer/MCPServer/Tools/EchoTool.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/modules/MCPServer/MCPServer/appsettings.json
Normal file
18
src/modules/MCPServer/MCPServer/appsettings.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
EXPORTS
|
||||||
|
powertoy_create
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?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>
|
||||||
298
src/modules/MCPServer/MCPServerModuleInterface/dllmain.cpp
Normal file
298
src/modules/MCPServer/MCPServerModuleInterface/dllmain.cpp
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#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();
|
||||||
|
}
|
||||||
1
src/modules/MCPServer/MCPServerModuleInterface/pch.cpp
Normal file
1
src/modules/MCPServer/MCPServerModuleInterface/pch.cpp
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "pch.h"
|
||||||
14
src/modules/MCPServer/MCPServerModuleInterface/pch.h
Normal file
14
src/modules/MCPServer/MCPServerModuleInterface/pch.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#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>
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
<PackageReference Include="System.CommandLine" />
|
<PackageReference Include="System.CommandLine" />
|
||||||
<PackageReference Include="System.Reactive" />
|
<PackageReference Include="System.Reactive" />
|
||||||
<PackageReference Include="System.Runtime.Caching" />
|
<PackageReference Include="System.Runtime.Caching" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Awake.Core.Models;
|
using Awake.Core.Models;
|
||||||
using Awake.Core.Native;
|
using Awake.Core.Native;
|
||||||
|
|
||||||
|
// New usage tracking namespace
|
||||||
|
using Awake.Core.Usage;
|
||||||
using Awake.Properties;
|
using Awake.Properties;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library;
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
@@ -56,6 +59,9 @@ namespace Awake.Core
|
|||||||
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
||||||
private static CancellationTokenSource _tokenSource;
|
private static CancellationTokenSource _tokenSource;
|
||||||
|
|
||||||
|
// Foreground usage tracker instance (lifecycle managed by Program)
|
||||||
|
internal static ForegroundUsageTracker? UsageTracker { get; set; }
|
||||||
|
|
||||||
static Manager()
|
static Manager()
|
||||||
{
|
{
|
||||||
_tokenSource = new CancellationTokenSource();
|
_tokenSource = new CancellationTokenSource();
|
||||||
@@ -412,6 +418,16 @@ namespace Awake.Core
|
|||||||
Bridge.DestroyWindow(TrayHelper.WindowHandle);
|
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);
|
Bridge.PostQuitMessage(exitCode);
|
||||||
Environment.Exit(exitCode);
|
Environment.Exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/modules/awake/Awake/Core/Native/IdleTime.cs
Normal file
44
src/modules/awake/Awake/Core/Native/IdleTime.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
364
src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs
Normal file
364
src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/modules/awake/Awake/Core/Usage/IUsageStore.cs
Normal file
21
src/modules/awake/Awake/Core/Usage/IUsageStore.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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
|
||||||
24
src/modules/awake/Awake/Core/Usage/Models/AppUsageRecord.cs
Normal file
24
src/modules/awake/Awake/Core/Usage/Models/AppUsageRecord.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/modules/awake/Awake/Core/Usage/SqliteUsageStore.cs
Normal file
145
src/modules/awake/Awake/Core/Usage/SqliteUsageStore.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// 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
|
||||||
@@ -18,6 +18,9 @@ using System.Threading.Tasks;
|
|||||||
using Awake.Core;
|
using Awake.Core;
|
||||||
using Awake.Core.Models;
|
using Awake.Core.Models;
|
||||||
using Awake.Core.Native;
|
using Awake.Core.Native;
|
||||||
|
|
||||||
|
// Usage tracking
|
||||||
|
using Awake.Core.Usage;
|
||||||
using Awake.Properties;
|
using Awake.Properties;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library;
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
@@ -342,6 +345,33 @@ 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()
|
private static void AllocateLocalConsole()
|
||||||
@@ -361,6 +391,7 @@ namespace Awake
|
|||||||
SetupFileSystemWatcher(settingsPath);
|
SetupFileSystemWatcher(settingsPath);
|
||||||
InitializeSettings();
|
InitializeSettings();
|
||||||
ProcessSettings();
|
ProcessSettings();
|
||||||
|
InitializeUsageTracking(); // after initial settings load
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -406,6 +437,7 @@ namespace Awake
|
|||||||
{
|
{
|
||||||
Logger.LogInfo("Detected a settings file change. Updating configuration...");
|
Logger.LogInfo("Detected a settings file change. Updating configuration...");
|
||||||
ProcessSettings();
|
ProcessSettings();
|
||||||
|
InitializeUsageTracking(); // re-evaluate usage tracking on config change
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
IntervalMinutes = 1;
|
IntervalMinutes = 1;
|
||||||
ExpirationDateTime = DateTimeOffset.Now;
|
ExpirationDateTime = DateTimeOffset.Now;
|
||||||
CustomTrayTimes = [];
|
CustomTrayTimes = [];
|
||||||
|
|
||||||
|
// Usage tracking defaults (opt-in, disabled by default)
|
||||||
|
TrackUsageEnabled = false; // default off
|
||||||
|
UsageRetentionDays = 14; // two weeks default retention
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("keepDisplayOn")]
|
[JsonPropertyName("keepDisplayOn")]
|
||||||
@@ -40,5 +44,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonPropertyName("customTrayTimes")]
|
[JsonPropertyName("customTrayTimes")]
|
||||||
[CmdConfigureIgnore]
|
[CmdConfigureIgnore]
|
||||||
public Dictionary<string, uint> CustomTrayTimes { get; set; }
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
KeepDisplayOn = Properties.KeepDisplayOn,
|
KeepDisplayOn = Properties.KeepDisplayOn,
|
||||||
IntervalMinutes = Properties.IntervalMinutes,
|
IntervalMinutes = Properties.IntervalMinutes,
|
||||||
IntervalHours = Properties.IntervalHours,
|
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,
|
ExpirationDateTime = Properties.ExpirationDateTime.Year < 2 ? DateTimeOffset.Now : Properties.ExpirationDateTime,
|
||||||
|
TrackUsageEnabled = Properties.TrackUsageEnabled,
|
||||||
|
UsageRetentionDays = Properties.UsageRetentionDays,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,22 @@
|
|||||||
IsEnabled="{x:Bind ViewModel.IsScreenConfigurationPossibleEnabled, Mode=OneWay}">
|
IsEnabled="{x:Bind ViewModel.IsScreenConfigurationPossibleEnabled, Mode=OneWay}">
|
||||||
<ToggleSwitch IsOn="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}" />
|
<ToggleSwitch IsOn="{x:Bind ViewModel.KeepDisplayOn, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
|
|
||||||
|
<!-- Usage tracking settings -->
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
Name="AwakeUsageTrackingEnabledCard"
|
||||||
|
x:Uid="Awake_UsageTrackingEnabledCard"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
|
<ToggleSwitch IsOn="{x:Bind ViewModel.TrackUsageEnabled, Mode=TwoWay}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
Name="AwakeUsageRetentionCard"
|
||||||
|
x:Uid="Awake_UsageRetentionCard"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
|
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>
|
</controls:SettingsGroup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsPageControl.ModuleContent>
|
</controls:SettingsPageControl.ModuleContent>
|
||||||
|
|||||||
@@ -527,6 +527,22 @@ opera.exe</value>
|
|||||||
<value>Awake</value>
|
<value>Awake</value>
|
||||||
<comment>{Locked}</comment>
|
<comment>{Locked}</comment>
|
||||||
</data>
|
</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 (1–365)</value>
|
||||||
|
<comment>Description: retention range hint (Awake)</comment>
|
||||||
|
</data>
|
||||||
<data name="Shell_PowerLauncher.Content" xml:space="preserve">
|
<data name="Shell_PowerLauncher.Content" xml:space="preserve">
|
||||||
<value>PowerToys Run</value>
|
<value>PowerToys Run</value>
|
||||||
<comment>Product name: Navigation view item name for PowerToys Run</comment>
|
<comment>Product name: Navigation view item name for PowerToys Run</comment>
|
||||||
|
|||||||
@@ -198,6 +198,33 @@ 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)
|
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
{
|
{
|
||||||
Logger.LogInfo($"Changed the property {propertyName}");
|
Logger.LogInfo($"Changed the property {propertyName}");
|
||||||
@@ -219,6 +246,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
OnPropertyChanged(nameof(IntervalHours));
|
OnPropertyChanged(nameof(IntervalHours));
|
||||||
OnPropertyChanged(nameof(IntervalMinutes));
|
OnPropertyChanged(nameof(IntervalMinutes));
|
||||||
OnPropertyChanged(nameof(ExpirationDateTime));
|
OnPropertyChanged(nameof(ExpirationDateTime));
|
||||||
|
OnPropertyChanged(nameof(TrackUsageEnabled));
|
||||||
|
OnPropertyChanged(nameof(UsageRetentionDays));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _enabledStateIsGPOConfigured;
|
private bool _enabledStateIsGPOConfigured;
|
||||||
|
|||||||
Reference in New Issue
Block a user