mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
added usage status record in awake
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
This commit is contained in:
@@ -2,7 +2,13 @@
|
||||
// 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.PowerToys.Settings.UI.Library;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace PowerToys.MCPServer.Tools
|
||||
@@ -13,5 +19,103 @@ namespace PowerToys.MCPServer.Tools
|
||||
[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.json). Parameters: top (default 10), days (default 7). Returns JSON.")]
|
||||
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 usageFile = Path.Combine(directory, "usage.json");
|
||||
|
||||
if (!File.Exists(usageFile))
|
||||
{
|
||||
return JsonSerializer.Serialize(new { error = "usage.json not found", path = usageFile });
|
||||
}
|
||||
|
||||
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 = Math.Round(e.GetPropertyOrDefault("totalSeconds", 0.0), 1),
|
||||
lastUpdatedUtc = e.GetPropertyOrDefaultDateTime("lastUpdatedUtc"),
|
||||
firstSeenUtc = e.GetPropertyOrDefaultDateTime("firstSeenUtc"),
|
||||
})
|
||||
.Where(r => r.lastUpdatedUtc >= cutoff)
|
||||
.OrderByDescending(r => r.totalSeconds)
|
||||
.Take(top)
|
||||
.Select(r => new
|
||||
{
|
||||
r.process,
|
||||
r.totalSeconds,
|
||||
totalHours = Math.Round(r.totalSeconds / 3600.0, 2),
|
||||
r.firstSeenUtc,
|
||||
r.lastUpdatedUtc,
|
||||
});
|
||||
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonSerializer.Serialize(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +17,9 @@ 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;
|
||||
@@ -56,6 +59,9 @@ 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();
|
||||
@@ -412,6 +418,16 @@ 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
459
src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs
Normal file
459
src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks foreground application usage time (simple active window focus durations) with idle suppression.
|
||||
/// </summary>
|
||||
internal sealed class ForegroundUsageTracker : IDisposable
|
||||
{
|
||||
private const uint EventSystemForeground = 0x0003;
|
||||
private const uint WinEventOutOfContext = 0x0000;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = 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 Dictionary<string, AppUsageRecord> _usage = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly string _storePath;
|
||||
private readonly Timer _flushTimer;
|
||||
private readonly TimeSpan _idleThreshold = TimeSpan.FromSeconds(60);
|
||||
private readonly Timer _pollTimer; // Fallback polling when WinEvent hook not firing
|
||||
|
||||
private string? _activeProcess;
|
||||
private DateTime _activeSince;
|
||||
private bool _disposed;
|
||||
private IntPtr _hook = IntPtr.Zero;
|
||||
private WinEventDelegate? _hookDelegate;
|
||||
private int _retentionDays;
|
||||
private IntPtr _lastHwnd = IntPtr.Zero;
|
||||
|
||||
private const double CommitThresholdSeconds = 0.25;
|
||||
|
||||
internal bool Enabled { get; private set; }
|
||||
|
||||
public ForegroundUsageTracker(string storePath, int retentionDays)
|
||||
{
|
||||
_storePath = storePath;
|
||||
_retentionDays = retentionDays;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(storePath)!);
|
||||
|
||||
LoadState();
|
||||
|
||||
_flushTimer = new Timer(30000)
|
||||
{
|
||||
AutoReset = true,
|
||||
};
|
||||
_flushTimer.Elapsed += (_, _) => FlushInternal();
|
||||
|
||||
_pollTimer = new Timer(1000)
|
||||
{
|
||||
AutoReset = true,
|
||||
};
|
||||
_pollTimer.Elapsed += (_, _) => PollForeground();
|
||||
}
|
||||
|
||||
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 (fallback polling only).");
|
||||
CaptureInitialForeground();
|
||||
_flushTimer.Start();
|
||||
_pollTimer.Start();
|
||||
Logger.LogInfo("[AwakeUsage] Started foreground usage tracking.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_flushTimer.Stop();
|
||||
_pollTimer.Stop();
|
||||
if (_hook != IntPtr.Zero)
|
||||
{
|
||||
UnhookWinEvent(_hook);
|
||||
_hook = IntPtr.Zero;
|
||||
}
|
||||
|
||||
CommitActiveSpan();
|
||||
FlushInternal();
|
||||
Logger.LogInfo("[AwakeUsage] Stopped foreground usage tracking.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureInitialForeground()
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr hwnd = GetForegroundWindow();
|
||||
if (hwnd == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogDebug("[AwakeUsage] Initial foreground hwnd == 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryResolveProcess(hwnd, out string? procName))
|
||||
{
|
||||
_activeProcess = procName;
|
||||
_activeSince = DateTime.UtcNow;
|
||||
_lastHwnd = hwnd;
|
||||
EnsurePlaceholder(_activeProcess);
|
||||
Logger.LogInfo("[AwakeUsage] Initial foreground captured: " + _activeProcess);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("[AwakeUsage] Failed to capture initial foreground: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string SafeResolveProcessName(Process proc)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Path.GetFileName(proc.MainModule?.FileName) ?? proc.ProcessName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return proc.ProcessName;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveProcess(IntPtr hwnd, out string? processName)
|
||||
{
|
||||
processName = null;
|
||||
|
||||
if (hwnd == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
uint pid;
|
||||
uint tid = GetWindowThreadProcessId(hwnd, out pid);
|
||||
if (tid == 0 || pid == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using Process p = Process.GetProcessById((int)pid);
|
||||
processName = SafeResolveProcessName(p);
|
||||
return !string.IsNullOrWhiteSpace(processName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug("[AwakeUsage] TryResolveProcess failed: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePlaceholder(string? process)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(process))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_usage.ContainsKey(process))
|
||||
{
|
||||
_usage[process] = new AppUsageRecord
|
||||
{
|
||||
ProcessName = process,
|
||||
FirstSeenUtc = DateTime.UtcNow,
|
||||
LastUpdatedUtc = DateTime.UtcNow,
|
||||
TotalSeconds = 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WinEventCallback(IntPtr hWinEventHook, uint evt, IntPtr hwnd, int idObj, int idChild, uint thread, uint time)
|
||||
{
|
||||
if (_disposed || !Enabled || evt != EventSystemForeground)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[AwakeUsage] WinEvent foreground change: hwnd=0x{hwnd.ToInt64():X}");
|
||||
HandleForegroundChange(hwnd, source: "hook");
|
||||
}
|
||||
|
||||
private void PollForeground()
|
||||
{
|
||||
if (_disposed || !Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IntPtr hwnd = GetForegroundWindow();
|
||||
if (hwnd == IntPtr.Zero || hwnd == _lastHwnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[AwakeUsage] Poll detected change: hwnd=0x{hwnd.ToInt64():X}");
|
||||
HandleForegroundChange(hwnd, source: "poll");
|
||||
}
|
||||
|
||||
private void HandleForegroundChange(IntPtr hwnd, string source)
|
||||
{
|
||||
try
|
||||
{
|
||||
CommitActiveSpan();
|
||||
if (!TryResolveProcess(hwnd, out string? procName))
|
||||
{
|
||||
_activeProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeProcess = procName;
|
||||
_activeSince = DateTime.UtcNow;
|
||||
_lastHwnd = hwnd;
|
||||
EnsurePlaceholder(_activeProcess);
|
||||
Logger.LogDebug($"[AwakeUsage] Active process set ({source}): {_activeProcess}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("[AwakeUsage] HandleForegroundChange failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void CommitActiveSpan()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_activeProcess))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IdleTime.GetIdleTime() > _idleThreshold)
|
||||
{
|
||||
_activeProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
double seconds = (DateTime.UtcNow - _activeSince).TotalSeconds;
|
||||
if (seconds < CommitThresholdSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_usage.TryGetValue(_activeProcess!, out AppUsageRecord? rec))
|
||||
{
|
||||
rec = new AppUsageRecord
|
||||
{
|
||||
ProcessName = _activeProcess!,
|
||||
FirstSeenUtc = DateTime.UtcNow,
|
||||
LastUpdatedUtc = DateTime.UtcNow,
|
||||
TotalSeconds = 0,
|
||||
};
|
||||
_usage[_activeProcess!] = rec;
|
||||
}
|
||||
|
||||
rec.TotalSeconds += seconds;
|
||||
rec.LastUpdatedUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_activeSince = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void LoadState()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_storePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(_storePath);
|
||||
List<AppUsageRecord>? list = JsonSerializer.Deserialize<List<AppUsageRecord>>(json);
|
||||
if (list == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime cutoff = DateTime.UtcNow.AddDays(-_retentionDays);
|
||||
foreach (AppUsageRecord rec in list.Where(r => r.LastUpdatedUtc >= cutoff))
|
||||
{
|
||||
_usage[rec.ProcessName] = rec;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("[AwakeUsage] Failed to load usage store: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private double GetLiveActiveSeconds()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_activeProcess))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (IdleTime.GetIdleTime() > _idleThreshold)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Max(0, (DateTime.UtcNow - _activeSince).TotalSeconds);
|
||||
}
|
||||
|
||||
private void FlushInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
CommitActiveSpan();
|
||||
|
||||
List<AppUsageRecord> snapshot;
|
||||
double liveSeconds = GetLiveActiveSeconds();
|
||||
string? liveProcess = _activeProcess;
|
||||
lock (_lock)
|
||||
{
|
||||
DateTime cutoff = DateTime.UtcNow.AddDays(-_retentionDays);
|
||||
foreach (string key in _usage.Values.Where(v => v.LastUpdatedUtc < cutoff).Select(v => v.ProcessName).ToList())
|
||||
{
|
||||
_usage.Remove(key);
|
||||
}
|
||||
|
||||
snapshot = _usage.Values
|
||||
.Select(r => new AppUsageRecord
|
||||
{
|
||||
ProcessName = r.ProcessName,
|
||||
TotalSeconds = r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase) ? r.TotalSeconds + liveSeconds : r.TotalSeconds,
|
||||
FirstSeenUtc = r.FirstSeenUtc,
|
||||
LastUpdatedUtc = r.LastUpdatedUtc,
|
||||
})
|
||||
.OrderByDescending(r => r.TotalSeconds)
|
||||
.ToList();
|
||||
|
||||
if (liveProcess != null && !_usage.ContainsKey(liveProcess) && liveSeconds > 0)
|
||||
{
|
||||
snapshot.Add(new AppUsageRecord
|
||||
{
|
||||
ProcessName = liveProcess,
|
||||
TotalSeconds = liveSeconds,
|
||||
FirstSeenUtc = DateTime.UtcNow,
|
||||
LastUpdatedUtc = DateTime.UtcNow,
|
||||
});
|
||||
snapshot = snapshot.OrderByDescending(r => r.TotalSeconds).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
string json = JsonSerializer.Serialize(snapshot, SerializerOptions);
|
||||
File.WriteAllText(_storePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("[AwakeUsage] Flush failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<AppUsageRecord> GetSummary(int top, int days)
|
||||
{
|
||||
CommitActiveSpan();
|
||||
double liveSeconds = GetLiveActiveSeconds();
|
||||
string? liveProcess = _activeProcess;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days));
|
||||
List<AppUsageRecord> list = _usage.Values
|
||||
.Where(r => r.LastUpdatedUtc >= cutoff)
|
||||
.Select(r => new AppUsageRecord
|
||||
{
|
||||
ProcessName = r.ProcessName,
|
||||
TotalSeconds = r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase) ? r.TotalSeconds + liveSeconds : r.TotalSeconds,
|
||||
FirstSeenUtc = r.FirstSeenUtc,
|
||||
LastUpdatedUtc = r.LastUpdatedUtc,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (liveProcess != null && list.All(r => !r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase)) && liveSeconds > 0)
|
||||
{
|
||||
list.Add(new AppUsageRecord
|
||||
{
|
||||
ProcessName = liveProcess,
|
||||
TotalSeconds = liveSeconds,
|
||||
FirstSeenUtc = DateTime.UtcNow,
|
||||
LastUpdatedUtc = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
|
||||
return list
|
||||
.OrderByDescending(r => r.TotalSeconds)
|
||||
.Take(top)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
Configure(false, _retentionDays);
|
||||
_flushTimer.Dispose();
|
||||
_pollTimer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ 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;
|
||||
@@ -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()
|
||||
@@ -361,6 +391,7 @@ namespace Awake
|
||||
SetupFileSystemWatcher(settingsPath);
|
||||
InitializeSettings();
|
||||
ProcessSettings();
|
||||
InitializeUsageTracking(); // after initial settings load
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -406,6 +437,7 @@ namespace Awake
|
||||
{
|
||||
Logger.LogInfo("Detected a settings file change. Updating configuration...");
|
||||
ProcessSettings();
|
||||
InitializeUsageTracking(); // re-evaluate usage tracking on config change
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
IntervalMinutes = 1;
|
||||
ExpirationDateTime = DateTimeOffset.Now;
|
||||
CustomTrayTimes = [];
|
||||
|
||||
// New usage tracking defaults (opt-in, disabled by default)
|
||||
// Need to add this to settings, set it to true for testing
|
||||
TrackUsageEnabled = true;
|
||||
UsageRetentionDays = 14; // two weeks default retention
|
||||
}
|
||||
|
||||
[JsonPropertyName("keepDisplayOn")]
|
||||
@@ -40,5 +45,13 @@ 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user